pax_global_header00006660000000000000000000000064152222141410014504gustar00rootroot0000000000000052 comment=bf2b079f04130d311c3365af65abbbdafd80cbea pokrok-0.2.1/000077500000000000000000000000001522221414100130115ustar00rootroot00000000000000pokrok-0.2.1/.github/000077500000000000000000000000001522221414100143515ustar00rootroot00000000000000pokrok-0.2.1/.github/workflows/000077500000000000000000000000001522221414100164065ustar00rootroot00000000000000pokrok-0.2.1/.github/workflows/ci.yml000066400000000000000000000016321522221414100175260ustar00rootroot00000000000000name: CI on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # setuptools-scm needs full history + tags - name: Install uv uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} - name: Sync dependencies run: uv sync - name: Lint run: uv run ruff check pokrok tests - name: Test run: uv run pytest --cov --cov-report term-missing build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Build sdist and wheel run: uv build pokrok-0.2.1/.github/workflows/release.yml000066400000000000000000000016021522221414100205500ustar00rootroot00000000000000name: Release on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # setuptools-scm needs full history + tags - name: Install uv uses: astral-sh/setup-uv@v5 - name: Build sdist and wheel run: uv build - name: Check the distributions run: uvx twine check dist/* - uses: actions/upload-artifact@v4 with: name: dist path: dist/ publish: needs: build runs-on: ubuntu-latest environment: pypi permissions: id-token: write # trusted publishing to PyPI via OIDC (no stored token) steps: - uses: actions/download-artifact@v4 with: name: dist path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 pokrok-0.2.1/.gitignore000066400000000000000000000002041522221414100147750ustar00rootroot00000000000000build/ dist/ *.egg-info/ __pycache__/ .idea/ .venv/ .pytest_cache/ .ruff_cache/ .coverage htmlcov/ # oh-my-claudecode scratch .omc/ pokrok-0.2.1/AGENTS.md000066400000000000000000000032371522221414100143210ustar00rootroot00000000000000# AGENTS.md Guidance for agents and contributors working on pokrok. ## Project pokrok is a small pure-Python library: a single API over several progress-bar backends (tqdm, progressbar2, halo, and a stdlib-logging fallback), selected at runtime via entry points. - Trunk branch: `main` - Packaging: `pyproject.toml` only (setuptools build backend, setuptools-scm for dynamic versioning). There is no `setup.py`/`setup.cfg`/`versioneer.py`. - Python: `>=3.10`. - Tooling: managed with [uv](https://docs.astral.sh/uv/). ## Build / test / lint ```bash uv sync # create the venv, install dev deps uv run pytest --cov --cov-report term-missing # run tests with coverage uv run ruff check pokrok tests # lint uv run ruff format pokrok tests # format uv build # build sdist + wheel into dist/ ``` The test suite exercises the real backend libraries (tqdm, progressbar2, halo, stdlib logging), which are installed via the `dev` dependency group, so tests need no mocking of the underlying progress-bar packages. ## Versioning / release The version is derived from git tags by setuptools-scm; do not hardcode a version in `pyproject.toml`. To cut a release, tag the commit (e.g. `0.3.0`) and build: ```bash git tag 0.3.0 uv build ``` Publishing to PyPI is a human step (e.g. `uv publish`); agents must not push tags or publish. ## Conventions - Keep `pyproject.toml` the single source of build configuration. - Use `importlib.metadata` / `importlib.resources` (not `pkg_resources`). - Each backend lives in `pokrok/plugins/.py` and is wired in via the `[project.entry-points.pokrok]` table. pokrok-0.2.1/CHANGES000066400000000000000000000003711522221414100140050ustar00rootroot00000000000000v0.1.0 ------ * Widgets can now be configured with arbitrary attributes. * Added a TextWidget for progress bars that support intermixing arbitrary text with other widgets. * Added a plugin that logs progress using the buildin python logging module. pokrok-0.2.1/LICENSE000066400000000000000000000020621522221414100140160ustar00rootroot00000000000000Copyright (c) 2017 John Didion Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.pokrok-0.2.1/Makefile000066400000000000000000000006251522221414100144540ustar00rootroot00000000000000module = pokrok .PHONY: all install test lint format build clean all: build install: uv sync test: uv run pytest --cov --cov-report term-missing lint: uv run ruff check $(module) tests format: uv run ruff format $(module) tests build: uv build clean: rm -rf build dist *.egg-info rm -rf .pytest_cache .ruff_cache .coverage htmlcov find . -type d -name __pycache__ -prune -exec rm -rf {} + pokrok-0.2.1/README.md000066400000000000000000000174131522221414100142760ustar00rootroot00000000000000PyPI offers at least 30 different progress meter packages. Which one should you use? Why choose? With pokrok, you can access a number of popular packages using a single consistent interface. With this approach, you can: 1. Allow your users to configure their favorite package. 2. Switch from one progress meter implementation to another without having to rewrite any code. 3. Use any supported package optimally, for the most common use cases, without having to read the manual. # Types of progress meters Generally, a progress meter is a visual representation of the progress of a task. The two most common types of progress meters are: * Progress bar - shows progress of a task of known size, advancing steadily from 0 to 100% complete. * Spinner - shows that there is progress, but with no indication of how much of the task is remaining (because the size of the task is unknown). In addition to the progress indicator, each type of progress meter may be accompanied by other information such as elapsed time and estimated time remaining. We call the different aspects of the progress bar display 'widgets.' Pokrok provides a simple API for displaying progress bars and spinners. The actual work is done by any one of many available progress bar packges in python. For each package, a sensible default display is provided. In addition, high-level configuration options are provided for limited customization of the bar/spinner display without having to know the details of the underlying progress bar/spinner implementation. Finally, access to the fine-grained configuration of the underlying package is provided for those that want it. # Installation Pokrok requires python 3.4+. ```bash pip install pokrok ``` This will install pokrok, but for pokrok to be useful it also needs at least one supported progress bar package to be installed. Some examples are: * tqdm * halo # Examples ```python import pokrok as pk import random # Configure pokrok with a list of your preferred progress bars, in order. Set # exclusive=True to prevent pokrok from using any other packages but the ones # you specify. pk.set_plugins(['tqdm', 'halo'], exclusive=True) # Show progress while iterating 100 times numbers = [random.random() for i in pk.progress_range(100)] # Show progress while iterating over lines in a file for line in pk.progress_file('foo.txt', 'rt'): print(line) # Show progress while iterating over an arbitrary iterable. Since the # `progress_iter` function doesn't know the number of items over which # it is iterating, it must be told explicitly using the 'size' argument. def generate_random(n): for i in range(n): yield random.random() for num in pk.progress_iter(generate_random(100), size=100): print(num) # Get a progress context manager bar you can control manually. with pk.progress_meter(size=100) as bar: for i in range(1000): # Only update progress every 10 cycles if i % 10 == 0: bar.increment() print(i) # Or take full control. bar = pk.progress_meter(size=100) bar.start() for i in range(1000): # Only update progress every 10 cycles if i % 10 == 0: bar.increment() print(i) bar.finish() ``` # Configuration If you'd just like to use the default implementations provided by whatever plugin is selected, you don't need to do anything. However, if you want to take some control over the progress bar/spinner display, you have two options. ## High-level configuration Pokrok defines a set of widgets that are commonly provided by progress meter packages. Widgets are combined into "styles." A style is simply a listing of the desired widgets (left-to-right). Separate widget lists can be provided for sized (i.e. progress bar) versus unsized (i.e. spinner) progress meters. There is a global default style, and any number of named styles can be created. When a style is used to create a progress meter, the underlying progress meter library will do its best to provide the desired display; any widgets it cannot provide are ignored silently. ```python # Global configuration from pokrok import set_styles, progress_meter, Style, Widget as w set_styles( default=Style(sized=[w.BAR, w.ETA], unsized=[w.SPINNER, w.ELAPSED]), kitchen_sink=Style([w.SPINNER, w.BAR, w.ELAPSED, w.ETA]) ) # Use a pre-defined configuration bar = progress_meter(size=100, style='kitchen_sink') # Per-progress meter configuration bar = progress_meter(size=100, style=Style([w.ETA, w.ELAPSED, w.BAR])) ``` ## Fine-grained configuration **NOTE: the fine-grained configuration support described below is still being implemented.** You can also specify configuration options for each of the progress meter packages you want to support. All configuration options can be set via a JSON configuration file, '.pokrok'. Pokrok looks for this file by default in the following places (in order): 1. ~/pokrok.json 2. ./pokrok.json By default, the first file discovered is loaded and used for configuration. You can override or supplement this behavior using the `configure()` function. Configuration options for each package can be specified in a dict via a keyword argument to `configure()`. If a file and keyword arguments are given together, the options in the file override the keyword arguments (i.e. the keyword arguments are treated as defaults for options that are not specified in the file). To override this behavior, call the `configure()` function twice - first with the file, then with the keyword arguments. ```python import pokrok as pk # Execute the default configuration behavior (this happens by default when the # first progress meter is requested, so while you can call the configure() # function explicitly, you don't have to unless you intend to override any options. pk.configure() # Load the configuration from an alternate file path, and override options for the # tqdm and halo libraries. pk.configure(filename='~/pokrok.json') pk.configure( tqdm=dict(ncols=100, unit='sec'), halo=dict(spinner='dots', color='blue') ) ``` # Plugins Plugins are created by implementing the pokrok API. The easiest way to do this is to extend the base classes in pokrok.plugins. For example, here is the implementation of the built-in `tqdm` module: ```python from pokrok.plugins import ( DefaultProgressMeterFactory, BaseProgressMeter, Status) from pokrok.styles import Style, Widget class TqdmProgressMeterFactory(DefaultProgressMeterFactory): def __init__(self): style_superset = Style([ Widget.BAR, Widget.ETA, Widget.ELAPSED, Widget.SPINNER ]) super().__init__('tqdm', TqdmProgressMeter, style_superset) def iterate( self, iterable, size=None, widgets=None, desc=None, start=None, **kwargs): if self._load_module(): return self._module.tqdm( iterable, total=size, desc=desc, initial=start or 0, **kwargs) else: return iterable class TqdmProgressMeter(BaseProgressMeter): def __init__(self, mod, size, style, desc, start, **kwargs): super().__init__(size) self.tqdm = mod.tqdm( total=size, desc=desc, initial=start or 0, **kwargs) def finish(self): super().finish() self.tqdm.close() def increment(self, n=1): self._check_status(Status.STARTED) self.tqdm.update(n) ``` To make the plugin visible to pokrok, add an entry point in your setup.py. For example, here is how the built-in TQDM plugin is configured: ```python entry_points=""" [pokrok] tqdm=pokrok.plugins.tqdm:TqdmProgressMeterFactory """ ``` # TODO * Add manual and automatic error handling (with callback to the plugin). * Implement file-based configuration. * Add support for additional packages: * progressbar2 * Add multi-progress bar support (e.g. for multithreading/multiprocessing) * https://pypi.python.org/pypi/progrock/0.3.1 pokrok-0.2.1/pokrok/000077500000000000000000000000001522221414100143165ustar00rootroot00000000000000pokrok-0.2.1/pokrok/__init__.py000066400000000000000000000167131522221414100164370ustar00rootroot00000000000000"""Main pokrok API. All functions support the following basic keyword arguments. Additional keyword arguments can be passed to whichever plugin is selected, and that plugin can choose whether or not to use those arguments. * start: The starting value of the progress counter. Only value if `size` is also specified. * size: The total size (i.e. max counter value) of the progress bar. * style: The desired style; a string name of a pre-defined style, or a Style object. * desc: A string description to display next to the progress bar. * plugin_name: Name of a specific plugin to use. """ import importlib.resources as importlib_resources import json import math import os from collections.abc import Sized from importlib.metadata import PackageNotFoundError, version import pokrok.plugins import pokrok.styles from pokrok.styles import Style, Widget try: __version__ = version("pokrok") except PackageNotFoundError: __version__ = "Unknown" __all__ = [ "Style", "Widget", "ProgressFactory", "FACTORY", "set_plugins", "set_styles", "configure", "progress_range", "progress_file", "progress_iter", "progress_meter", ] class ProgressFactory: def __init__(self): self.plugins = pokrok.plugins.PluginManager() self.styles = pokrok.styles.StyleManager() self.configured = False @property def default_paths(self): return [ os.path.join(path, "pokrok.json") for path in (os.getcwd(), os.path.abspath(os.path.expanduser("~"))) ] def configure( self, filename=None, plugin_names=None, exclusive=False, styles=None, **kwargs ): if not (filename or self.configured): for fname in self.default_paths: if os.path.exists(fname): filename = fname break if filename: config = None if os.path.exists(filename): with open(filename) as inp: config = json.load(inp) else: try: package, path = filename.split(":") resource = importlib_resources.files(package).joinpath(path) if resource.is_file(): config = json.loads(resource.read_text()) except Exception: pass if config is None: raise ValueError(f"File not found: {filename}") self.plugins.set_plugin_options(config) self.styles.set_style_options(config) self.configured = True if plugin_names: self.plugins.load_plugins(plugin_names, exclusive) if kwargs: self.plugins.set_plugin_options(**kwargs) if styles: self.styles.update(**styles) def create( self, iterable=None, size=None, style="default", plugin_name=None, **kwargs ): """Create a progress meter. All parameters are optional. The default behavior (i.e. when just calling `create()`) is to return an unsized ProgressMeter with default style. Args: iterable: An iterable to wrap. size: The size of the progress meter, or None for an unsized progress meter. style: The desired style. plugin_name: The name of a specific plugin to use. kwargs: Additional keyword arguments to pass to the plugin creation method. Returns: A ProgressMeter if `iterable` is None, otherwise an iterable, or None if a ProgressMeter could not be created. Raises: ProgressMeterError if a specific plugin is requested and is not available or does not support the requested configuration. """ if not self.configured: self.configure() if isinstance(style, str): style = self.styles[style] if style else None sized = size is not None widgets = style.get_widgets(sized) if plugin_name: if not self.plugins.has_plugin(plugin_name): raise ValueError(f"Plugin {plugin_name} is not supported") plugin = self.plugins.get_plugin(plugin_name) if not plugin.provides(sized, widgets, force=True): raise ValueError( f"Plugin {plugin_name} does not support the requested " f"configuration (sized={sized}, style={widgets})" ) else: plugin = self.plugins.get_first_plugin(sized, widgets) if plugin and iterable: return plugin.iterate(iterable, size=size, widgets=widgets, **kwargs) elif plugin: return plugin.create(size=size, widgets=widgets, **kwargs) elif iterable: return iterable else: return None # Singleton factory class FACTORY = ProgressFactory() def set_plugins(names, exclusive=False): """High-level configuration of progress meter packages. Args: names: Names of plugins to prefer, in order. exclusive: Whether the listed packages should be the only ones allowed. """ FACTORY.configure(plugin_names=names, exclusive=exclusive) def set_styles(**styles): """Convenience method for configuring progress meter styles. Args: styles: keyword arguments, where the name is the style name and the value is a pokrok.styles.Style object. """ FACTORY.configure(styles=styles) def configure(**kwargs): """Low-level configuration. This is just a pass-through to FACTORY.configure(). Args: kwargs: Keyword arguments to pass to FACTORY.configure(). """ FACTORY.configure(**kwargs) def progress_range(start, stop=None, step=1, **kwargs): """Iterate over a range while showing a progress bar. Args: start: Range start. stop: Range stop. step: Range step. kwargs: Additional arguments - see package documentation. Returns: An iterable. """ if stop is not None: r = range(start, stop, step) size = math.ceil((stop - start) / step) else: r = range(start) size = start return progress_iter(r, size=size, **kwargs) def progress_file(filename, mode, **kwargs): """Iterate over a file while showing a progress bar. Args: filename: The name of the file to open and iterate over. mode: The file mode (must be readable). kwargs: Additional arguments - see package documentation. Yields: Lines from the file. """ with open(filename, mode) as f: yield from progress_iter(f, **kwargs) def progress_iter(iterable, size=None, **kwargs): """Wrap an iterable in a progress bar. Args: iterable: The iterable to wrap. size: The number of items that will be iterated over by the iterable. If None and this iterable happens to be Sized, the size will be determined using `len`. kwargs: Additional arguments - see package documentation. Returns: An iterable. """ if iterable is None: raise ValueError("Invalid iterable") if size is None and isinstance(iterable, Sized): size = len(iterable) return FACTORY.create(iterable=iterable, size=size, **kwargs) def progress_meter(**kwargs): """ Create a progress meter. Args: kwargs: see package documentation. Returns: A ProgressMeter. """ return FACTORY.create(**kwargs) pokrok-0.2.1/pokrok/plugins/000077500000000000000000000000001522221414100157775ustar00rootroot00000000000000pokrok-0.2.1/pokrok/plugins/__init__.py000066400000000000000000000320121522221414100201060ustar00rootroot00000000000000import enum import importlib from abc import ABCMeta, abstractmethod from collections import OrderedDict from collections.abc import Iterable from importlib.metadata import entry_points # ProgressMeter statuses class Status(enum.Enum): UNSTARTED = 0 STARTED = 1 FINISHED = 2 class ProgressMeterError(Exception): pass class PluginManager: def __init__(self): self.plugins = None def set_plugin_options(self, config=None, **kwargs): pass def load_plugins(self, names=None, exclusive=False): # Load the factory classes from the discovered entry points plugin_types = dict( (entry_point.name, entry_point.load()) for entry_point in entry_points(group="pokrok") ) # Filter and order if names are provided if names: ordered_plugin_types = OrderedDict() for name in names: if name in plugin_types: ordered_plugin_types[name] = plugin_types[name] if not exclusive: for name in set(plugin_types.keys()) - set(ordered_plugin_types.keys()): ordered_plugin_types[name] = plugin_types[name] plugin_types = ordered_plugin_types # Finally, only keep the plugins for which the underlying libraries # are installed self.plugins = {} for name, plugin_type in plugin_types.items(): plugin = plugin_type() if plugin.installed: self.plugins[name] = plugin def has_plugin(self, name): if not self.plugins: self.load_plugins() return name in self.plugins def get_plugin(self, name): if not self.has_plugin(name): raise ValueError(f"No such plugin: {name}") return self.plugins[name] def get_first_plugin(self, sized=None, widgets=None): """ Returns the first plugin that can provide a progress meter of the specified configuration. Args: sized: Whether a sized ProgressMeter is required. widgets: The desired style. Returns: A ProgressMeter instance if one is found that can provide the specified configuration, else None. """ if self.plugins is None: self.load_plugins() if len(self.plugins) == 0: return None plugin_names = tuple(self.plugins.keys()) if sized is None and widgets is None: return self.get_plugin(plugin_names[0]) def _find_first_plugin(force=False): for plugin_name in plugin_names: _plugin = self.get_plugin(plugin_name) if _plugin.provides(sized, widgets, force=force): return _plugin plugin = _find_first_plugin(force=False) # If no plugin supports the requested configuration, try again # with force = True if plugin is None: plugin = _find_first_plugin(force=True) return plugin class ProgressMeterFactory(metaclass=ABCMeta): """ Plugin base class. A plugin's entry point must provide an instance of a ProgressMeterFactory. """ @property @abstractmethod def name(self): """ The plugin's name. """ @property @abstractmethod def installed(self): """ Imports the package(s) depended upon by this module. Returns: True if all required packages are successfully imported. """ @abstractmethod def provides(self, sized, widgets=None): """ Whether the plugin can provide a progress meter that conforms to the specified style. The plugin is free to interpret this request as it sees fit, including ignoring or substituting widgets. Args: sized: Boolean, whether the user is requesting a sized progress meter. widgets: The Style requested, or None to use the plugin's default style. Returns: True if the plugin can provide a conforming progress meter. """ @abstractmethod def create( self, size=None, widgets=None, desc=None, start=None, unit=None, multiplier=None, **kwargs, ): """ Creates a progress meter that conforms to the requested style. Args: size: Size of the progress meter. This is unaffected by the multiplier. widgets: The desired style, or None to use the plugin's default style. desc: String description to print next to the progress bar. start: Counter start value. unit: The unit of the counter. multiplier: Multiplier for each counter increment. kwargs: Additional keyword arguments to pass to the underlying progress meter package. The plugin is free to interpret these arguments as it wishes, including ignoring them. Returns: A ProgressMeter instance. """ @abstractmethod def iterate( self, iterable: Iterable, size: int | None = None, widgets=None, desc: str | None = None, start: int | None = None, unit: str | None = None, multiplier: int | None = None, **kwargs, ): """ Wraps an iterable with a progress meter. Args: iterable: The iterable to wrap. size: Size of the progress meter. This is unaffected by the multiplier. widgets: The desired style, or None to use the plugin's default style. desc: String description to print next to the progress bar. start: Counter start value. unit: The unit of the counter. multiplier: Multiplier for each counter increment. kwargs: Additional keyword arguments to pass to the underlying progress meter package. The plugin is free to interpret these arguments as it wishes, including ignoring them. Returns: An iterable. Returns the original iterable if there were any problems creating the wrapper. """ class BaseProgressMeterFactory(ProgressMeterFactory, metaclass=ABCMeta): @property @abstractmethod def style_superset(self): pass def provides(self, sized: bool, widgets=None, force=False): """ Returns True if this plugin can provide a progress bar for the requested parameter. Args: sized: Whether a sized meter bar is requested. widgets: Requested set of widgets. force: Whether to try to force the plugin to provide a progress meter with the specified parameters, even if it typically would not. """ if not (widgets and self.style_superset): return True provided_widgets = self.style_superset.get_widgets(sized) if not provided_widgets: return False # If trying to force the plugin to provide a progress meter, return True # if the plugin provides *any* of the requested widgets, otherwise require # that it provides *all* of the requested widgets. if force: return len(set(widgets) & set(provided_widgets)) > 0 else: return len(set(widgets) - set(provided_widgets)) == 0 def iterate( self, iterable: Iterable, size: int | None = None, widgets=None, desc=None, start=None, unit=None, multiplier=None, **kwargs, ): pbar = self.create(size, widgets, desc, start, unit, multiplier, **kwargs) if pbar: with pbar: for item in iterable: yield item pbar.increment() else: # No meter could be created: pass the items through unchanged. # This method is a generator (it `yield`s above), so a bare # `return iterable` would silently yield nothing; yield from the # iterable instead so the documented pass-through behavior holds. yield from iterable class DefaultProgressMeterFactory(BaseProgressMeterFactory): """ Default implementation of ProgressMeterFactory. Assumes that a) the underlying package is in a module that can be imported using importlib (if it is installed), b) the package supports any desired progress meter configuration (override `provides` if this isn't true), and c) the ProgressMeter subclass has a constructor with signature (module, size, widgets, desc, start, **kwargs). Args: name: The plugin name. Also used as the module name if `module_name` is None. progress_meter_class: The ProgressMeter subclass to instantiate. style_superset: A Style object that lists all of the widgets supported by the plugin. module_name: The name of the module, if different from `name`. """ def __init__(self, name, progress_meter_class, style_superset, module_name=None): super().__init__() self._name = name self._package = module_name or name self._module = None self._progress_meter_class = progress_meter_class self._style_superset = style_superset @property def name(self): return self._name @property def installed(self): return self._load_module() @property def style_superset(self): return self._style_superset def create( self, size=None, widgets=None, desc=None, start=None, unit=None, multiplier=None, **kwargs, ): if self._load_module(): return self._progress_meter_class( mod=self._module, size=size, widgets=widgets, desc=desc, start=start, unit=unit, multiplier=multiplier, **kwargs, ) def _load_module(self): if self._module is None: try: self._module = importlib.import_module(self._package) except ImportError: self._module = False return self._module not in (None, False) class ProgressMeter(metaclass=ABCMeta): """ProgressMeter interface to be implemented by the plugin.""" def __enter__(self): self.start() return self def __exit__(self, exc_type, exc_val, exc_tb): self.finish() @property @abstractmethod def is_sized(self): pass @property @abstractmethod def status(self): """The current status of the ProgressMeter. Must be one of the enumerated values. """ pass def start(self): # noqa: B027 - optional override, default is a no-op """Initialize and show the progress meter.""" pass def finish(self): # noqa: B027 - optional override, default is a no-op """Signal that the task is complete. The progress meter should update its display accordingly (if applicable). """ pass @abstractmethod def increment(self, n=1): """Increment the progress meter by a fixed amount (default=1). The increment will be multiplied by the multiplier if specified. Args: n: The amount to increment. Returns: The current value after incrementing. """ pass def _check_status(self, status=Status.STARTED, error=True): """Checks that the ProgressMeter's status matches `stats`. Args: status: The status to check. error: If True, an error is raised if the ProgressMeter does not have the specified status. Returns: True if the ProgressMeter is has the specified status. False if the ProgressMeter does not have the specified status and `error` is False. Raises: ProgressMeterError if the ProgressMeter does not have the specified status and `error` is True. """ if self.status != status: if error: raise ProgressMeterError( f"The ProgressMeter status {self.status} differs from " f"expected status {status}" ) else: return False return True class BaseProgressMeter(ProgressMeter, metaclass=ABCMeta): """Default implementation of ProgressMeter. Subclasses only need to implement `increment()`. """ def __init__(self, size=None): self._status = Status.UNSTARTED self.size = size @property def is_sized(self): return self.size is not None @property def status(self): return self._status def start(self): self._check_status(Status.UNSTARTED) self._status = Status.STARTED def finish(self): self._check_status(Status.STARTED) self._status = Status.FINISHED pokrok-0.2.1/pokrok/plugins/halo.py000066400000000000000000000026141522221414100172770ustar00rootroot00000000000000from pokrok.plugins import BaseProgressMeter, DefaultProgressMeterFactory from pokrok.styles import Style, Widget class HaloProgressMeterFactory(DefaultProgressMeterFactory): def __init__(self): style_superset = Style(unsized=[Widget.SPINNER]) super().__init__("halo", HaloProgressMeter, style_superset) def iterate( self, iterable, size=None, widgets=None, desc=None, start=None, unit=None, multiplier=None, **kwargs, ): if self._load_module(): with self._module.Halo(text=desc or ""): yield from iterable else: yield from iterable def provides(self, sized, widgets=None, force=False): if sized and not force: return False if widgets: if force: return Widget.SPINNER in widgets else: return len(widgets) == 1 and widgets[0] == Widget.SPINNER return True class HaloProgressMeter(BaseProgressMeter): def __init__(self, mod, size, widgets, desc, start, unit, multiplier, **kwargs): super().__init__(size) self.spinner = mod.Halo(text=desc or "") def start(self): super().start() self.spinner.start() def finish(self): super().finish() self.spinner.succeed() def increment(self, n=1): pass pokrok-0.2.1/pokrok/plugins/logging.py000066400000000000000000000112411522221414100177760ustar00rootroot00000000000000import sys from datetime import datetime from pokrok.plugins import BaseProgressMeter, DefaultProgressMeterFactory from pokrok.styles import Style, Widget STYLE_SUPERSET = Style( sized=[Widget.BAR, Widget.COUNTER, Widget.PERCENT, Widget.ELAPSED], unsized=[Widget.COUNTER, Widget.ELAPSED], ) class LoggingProgressMeterFactory(DefaultProgressMeterFactory): def __init__(self): # The plugin's public name is "Logging" but the backing module is the # stdlib ``logging`` package; pass module_name explicitly so the # DefaultProgressMeterFactory imports the right (lowercase) module. super().__init__( "Logging", LoggingProgressMeter, STYLE_SUPERSET, module_name="logging" ) class LoggingProgressMeter(BaseProgressMeter): """ Progress meter that logs messages at a specified interval. Args: interval: The reporting interval. logger_name: """ def __init__( self, mod, size, widgets, desc="", start=0, unit=None, multiplier=None, interval: int = 1000, logger_name: str = "progress", logger_level: str = "INFO", **_, ): super().__init__(size) self._logger = mod.getLogger(logger_name) self._logger.setLevel(logger_level) if not self._logger.hasHandlers(): self._logger.addHandler(mod.StreamHandler(sys.stderr)) # Logger.log() requires a numeric level; logger_level may be a level # name (e.g. "INFO") or an int. Normalize to an int. if isinstance(logger_level, str): self._level = mod.getLevelName(logger_level) else: self._level = logger_level self.count = start or 0 self.size = size self.interval = interval self.multiplier = multiplier self.start_time = None self._scale = 1 self._bar_size = 10 self._bar_char = "*" default_widgets = STYLE_SUPERSET.get_widgets(size is not None) if widgets is None: widgets = default_widgets else: allowed = set(default_widgets) widgets = [w for w in widgets if w in allowed] self.key_fns = {} message = [] if desc: message.append(desc) for w in widgets: if w == Widget.COUNTER: if size: suffix = "" for suffix in ["", "k", "M", "G", "T", "P", "E", "Z"]: # noqa: B007 - suffix is read after the loop if size < 1000: break size /= 1000 self._scale *= 1000 if suffix: message.append("{count:.2f}/" + f"{size:.2f}" + suffix) else: message.append("{count}/" + str(size)) else: message.append("{count}") if unit: message.append(unit) self.key_fns["count"] = lambda: self.count / self._scale elif w == Widget.ELAPSED: message.append("{elapsed:.1f} seconds") self.key_fns["elapsed"] = lambda: ( datetime.now().timestamp() - self.start_time.timestamp() ) elif w == Widget.BAR: message.append("{bar}") bar_fmt = f"[{{: <{self._bar_size}}}]" # bind bar_fmt as a default arg so the lambda captures the # current value rather than the loop variable. self.key_fns["bar"] = lambda bar_fmt=bar_fmt: bar_fmt.format( self._bar_char * round((self.count / self.size) * self._bar_size) ) elif w == Widget.PERCENT and size is not None: if Widget.COUNTER in widgets: message.append("({percent:.0%})") else: message.append("{percent:.0%}") self.key_fns["percent"] = lambda: self.count / self.size self.message = " ".join(message) def start(self): super().start() self.start_time = datetime.now() def finish(self): super().finish() self._logger.log(self._level, f"Read a total of {self.count} records") def increment(self, n=1): cur_mod = self.count % self.interval if self.multiplier: n *= self.multiplier self.count += n if (self.count % self.interval) <= cur_mod: format_kwargs = dict((key, fn()) for key, fn in self.key_fns.items()) self._logger.log(self._level, self.message.format(**format_kwargs)) pokrok-0.2.1/pokrok/plugins/progressbar2.py000066400000000000000000000051601522221414100207660ustar00rootroot00000000000000from pokrok.plugins import BaseProgressMeter, DefaultProgressMeterFactory, Status from pokrok.styles import Style, Widget class Progressbar2ProgressMeterFactory(DefaultProgressMeterFactory): def __init__(self): style_superset = Style( [ Widget.BAR, Widget.ETA, Widget.ELAPSED, Widget.SPINNER, Widget.COUNTER, Widget.PERCENT, ] ) super().__init__("progressbar", Progressbar2ProgressMeter, style_superset) def iterate( self, iterable, size=None, widgets=None, desc=None, start=None, unit=None, multiplier=None, **kwargs, ): if multiplier is None and self._load_module(): pb = self._module.ProgressBar( widgets=create_widgets(self._module, widgets, desc, unit), initial_value=start or 0, max_value=size or self._module.UnknownLength, ) return pb(iterable) else: return super().iterate( iterable, size, widgets, desc, start, unit, multiplier, **kwargs ) class Progressbar2ProgressMeter(BaseProgressMeter): def __init__(self, mod, size, widgets, desc, start, unit, multiplier, **kwargs): super().__init__(size) self.pb = mod.ProgressBar( widgets=create_widgets(mod, widgets, desc, unit), initial_value=start or 0, max_value=size or mod.UnknownLength, ) self.multiplier = multiplier def start(self): super().start() self.pb.start() def finish(self): super().finish() self.pb.finish() def increment(self, n=1): self._check_status(Status.STARTED) if self.multiplier: n *= self.multiplier self.pb.update(self.pb.value + n) PB_WIDGETS = { Widget.ETA: "AdaptiveETA", Widget.SPINNER: "AnimatedMarker", Widget.BAR: "Bar", Widget.COUNTER: "Counter", Widget.PERCENT: "Percentage", Widget.ELAPSED: "Timer", } """Mapping of pokrok widget types onto progressbar2 widget classes.""" def create_widgets(mod, widgets, desc=None, unit=None): pb_widgets = [] if desc: pb_widgets.append(desc) for widget in widgets or (): if pb_widgets: pb_widgets.append(" ") widget_class = getattr(mod, PB_WIDGETS[widget]) pb_widgets.append(widget_class()) if unit and widget == Widget.COUNTER: pb_widgets.append(" ") pb_widgets.append(unit) return pb_widgets pokrok-0.2.1/pokrok/plugins/tqdm.py000066400000000000000000000032651522221414100173240ustar00rootroot00000000000000from pokrok.plugins import BaseProgressMeter, DefaultProgressMeterFactory, Status from pokrok.styles import Style, Widget class TqdmProgressMeterFactory(DefaultProgressMeterFactory): def __init__(self): style_superset = Style([Widget.BAR, Widget.ETA, Widget.ELAPSED, Widget.SPINNER]) super().__init__("tqdm", TqdmProgressMeter, style_superset) def iterate( self, iterable, size=None, widgets=None, desc=None, start=None, unit=None, multiplier=None, **kwargs, ): if multiplier is None and self._load_module(): return self._module.tqdm( iterable, total=size, desc=desc, initial=start or 0, unit_scale=True, unit=unit or "it", ) else: return super().iterate( iterable, size, widgets, desc, start, unit, multiplier ) class TqdmProgressMeter(BaseProgressMeter): def __init__( self, mod, size, widgets, desc, start, unit, multiplier, unit_scale=True, **kwargs, ): super().__init__(size) self.tqdm = mod.tqdm( total=size, desc=desc, initial=start or 0, unit_scale=unit_scale, unit=unit or "it", ) self.multiplier = multiplier def finish(self): super().finish() self.tqdm.close() def increment(self, n=1): self._check_status(Status.STARTED) if self.multiplier: n *= self.multiplier self.tqdm.update(n) pokrok-0.2.1/pokrok/styles.py000066400000000000000000000032201522221414100162100ustar00rootroot00000000000000import enum class Widget(enum.Enum): """Enumeration of widget types that are commonly supported across progress bar libraries. """ BAR = "BAR" ETA = "ETA" ELAPSED = "ELAPSED" SPINNER = "SPINNER" COUNTER = "COUNTER" PERCENT = "PERCENT" class StyleManager(dict): def __init__(self, default_style=None): super().__init__() self["default"] = default_style or Style() def set_style_options(self, config, **kwargs): if "styles" in config: for name, style_config in config["styles"].items(): self[name] = Style(**style_config) if "default_style" in config: if isinstance(config["default_style"], str): self["default"] = self[config["default_style"]] else: self["default"] = Style(**config["default_style"]) class Style: """ Args: widgets: Widgets to use for `sized` and `unsized` unless they are not None. sized: Widgets for sized progress meters. unsized: Widgets specifically for unsized progress meters.s """ def __init__(self, widgets=None, sized=None, unsized=None): if widgets: self.sized = _resolve_widgets(sized or widgets) self.unsized = _resolve_widgets(unsized or widgets) else: self.sized = _resolve_widgets(sized) self.unsized = _resolve_widgets(unsized) def get_widgets(self, sized): return self.sized if sized else self.unsized def _resolve_widgets(widgets): if widgets is None: return None return [Widget[w] if isinstance(w, str) else w for w in widgets] pokrok-0.2.1/pyproject.toml000066400000000000000000000036271522221414100157350ustar00rootroot00000000000000[build-system] requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] name = "pokrok" description = "Simple API for progress bars using any of several supported libraries" readme = "README.md" license = "MIT" requires-python = ">=3.10" authors = [ { name = "John Didion", email = "johndidion@gmail.com" }, ] classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "Topic :: Software Development :: User Interfaces", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] dynamic = ["version"] [project.urls] Homepage = "https://github.com/jdidion/pokrok" [project.entry-points.pokrok] tqdm = "pokrok.plugins.tqdm:TqdmProgressMeterFactory" progressbar2 = "pokrok.plugins.progressbar2:Progressbar2ProgressMeterFactory" halo = "pokrok.plugins.halo:HaloProgressMeterFactory" logging = "pokrok.plugins.logging:LoggingProgressMeterFactory" [dependency-groups] dev = [ "pytest>=8", "pytest-cov>=5", "ruff>=0.6", "tqdm>=4", "progressbar2>=4", "halo>=0.0.31", ] [tool.setuptools.packages.find] include = ["pokrok*"] [tool.setuptools_scm] [tool.ruff] line-length = 88 target-version = "py310" [tool.ruff.lint] select = ["E", "F", "W", "I", "UP", "B"] # E722 (bare except) is intentional in __init__.configure's resource fallback; # keep it scoped rather than silencing project-wide. [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-ra" [tool.coverage.run] branch = true source = ["pokrok"] [tool.coverage.report] show_missing = true exclude_lines = [ "pragma: no cover", "raise NotImplementedError", "if __name__ == .__main__.:", "@(abc\\.)?abstractmethod", ] pokrok-0.2.1/tests/000077500000000000000000000000001522221414100141535ustar00rootroot00000000000000pokrok-0.2.1/tests/__init__.py000066400000000000000000000000001522221414100162520ustar00rootroot00000000000000pokrok-0.2.1/tests/conftest.py000066400000000000000000000021771522221414100163610ustar00rootroot00000000000000"""Shared fixtures for the pokrok test suite. The tests exercise the real backend libraries (tqdm, progressbar2, halo, logging), so we keep their visible output off the terminal by redirecting the relevant streams to dummies where it matters. tqdm/progressbar2/halo all write to stderr by default; pytest captures that automatically, so no special handling is required beyond not asserting on rendered output. """ import pytest import pokrok @pytest.fixture def fresh_factory(): """A ProgressFactory that has not yet been configured. Using a fresh instance per test avoids leaking plugin/style state between tests via the module-level singleton. """ return pokrok.ProgressFactory() @pytest.fixture(autouse=True) def reset_singleton(): """Reset the module-level FACTORY singleton around each test. Several public functions (set_plugins, set_styles, configure, progress_*) delegate to the singleton, which caches configuration. Reset it so tests that touch the singleton do not depend on ordering. """ pokrok.FACTORY = pokrok.ProgressFactory() yield pokrok.FACTORY = pokrok.ProgressFactory() pokrok-0.2.1/tests/resource_config.json000066400000000000000000000000731522221414100202220ustar00rootroot00000000000000{"styles": {"from_resource": {"widgets": ["BAR", "ETA"]}}} pokrok-0.2.1/tests/test_api.py000066400000000000000000000225421522221414100163420ustar00rootroot00000000000000"""Tests for the top-level pokrok API in pokrok/__init__.py. These exercise ProgressFactory configuration and creation logic plus the public convenience functions, using the real installed backends. """ import json import pytest import pokrok from pokrok.styles import Style, Widget class TestVersion: def test_version_is_a_string(self): assert isinstance(pokrok.__version__, str) assert pokrok.__version__ class TestDefaultPaths: def test_default_paths_includes_cwd_and_home(self, fresh_factory, monkeypatch): monkeypatch.chdir("/tmp") paths = fresh_factory.default_paths assert len(paths) == 2 assert all(p.endswith("pokrok.json") for p in paths) class TestConfigureFromFile: def test_configure_reads_json_file(self, fresh_factory, tmp_path): cfg = tmp_path / "pokrok.json" cfg.write_text(json.dumps({"styles": {"x": {"widgets": ["BAR"]}}})) fresh_factory.configure(filename=str(cfg)) assert fresh_factory.configured is True assert "x" in fresh_factory.styles def test_configure_missing_file_raises(self, fresh_factory, tmp_path): missing = tmp_path / "nope.json" with pytest.raises(ValueError, match="File not found"): fresh_factory.configure(filename=str(missing)) def test_configure_autodiscovers_default_path( self, fresh_factory, monkeypatch, tmp_path ): monkeypatch.chdir(tmp_path) # write a pokrok.json in cwd so default_paths discovers it (tmp_path / "pokrok.json").write_text( json.dumps({"styles": {"disco": {"widgets": ["SPINNER"]}}}) ) # patch home so the second default path does not interfere monkeypatch.setenv("HOME", str(tmp_path)) fresh_factory.configure() assert fresh_factory.configured is True assert "disco" in fresh_factory.styles def test_configure_no_file_found_stays_unconfigured_styles( self, fresh_factory, monkeypatch, tmp_path ): monkeypatch.chdir(tmp_path) monkeypatch.setenv("HOME", str(tmp_path)) # no pokrok.json anywhere -> configure() runs but finds no file fresh_factory.configure() # default style still present assert "default" in fresh_factory.styles def test_configure_with_plugin_names(self, fresh_factory): fresh_factory.configure(plugin_names=["tqdm"], exclusive=True) assert list(fresh_factory.plugins.plugins) == ["tqdm"] def test_configure_with_styles_kwarg(self, fresh_factory): fresh_factory.configure(styles={"mine": Style(widgets=["BAR"])}) assert "mine" in fresh_factory.styles def test_configure_from_package_resource(self, fresh_factory): # filename of the form "package:resource" loads config via # importlib.resources rather than the filesystem. fresh_factory.configure(filename="tests:resource_config.json") assert fresh_factory.configured is True assert "from_resource" in fresh_factory.styles def test_configure_bad_resource_spec_raises(self, fresh_factory): # a non-existent path with no ':' fails the split, the bare-except # swallows it, config stays None, and a ValueError is raised. with pytest.raises(ValueError, match="File not found"): fresh_factory.configure(filename="this-path-does-not-exist") class TestCreate: def test_create_unsized_returns_meter(self, fresh_factory): meter = fresh_factory.create() assert meter is not None assert meter.is_sized is False def test_create_sized_returns_meter(self, fresh_factory): meter = fresh_factory.create(size=100) assert meter is not None assert meter.is_sized is True def test_create_with_iterable_returns_iterable(self, fresh_factory): result = fresh_factory.create(iterable=range(3), size=3) assert list(result) == [0, 1, 2] def test_create_specific_plugin(self, fresh_factory): meter = fresh_factory.create(size=10, plugin_name="tqdm") assert type(meter).__name__ == "TqdmProgressMeter" def test_create_unknown_plugin_raises(self, fresh_factory): with pytest.raises(ValueError, match="is not supported"): fresh_factory.create(size=10, plugin_name="nope") def test_create_plugin_unsupported_config_raises(self, fresh_factory): # halo cannot provide a sized BAR meter -> requesting it by name with a # style it cannot satisfy raises. style = Style(sized=[Widget.BAR]) with pytest.raises(ValueError, match="does not support"): fresh_factory.create(size=10, style=style, plugin_name="halo") def test_create_with_style_object(self, fresh_factory): style = Style(sized=[Widget.BAR, Widget.ETA]) meter = fresh_factory.create(size=10, style=style) assert meter is not None def test_create_empty_style_string_resolves_to_none(self, fresh_factory): # style="" is falsy, so the style resolves to None... which makes the # default-style lookup unnecessary. This is a documented edge: an empty # style name yields None and create() then fails on get_widgets. We # assert the AttributeError to pin the current behavior. fresh_factory.configure() with pytest.raises(AttributeError): fresh_factory.create(size=10, style="") def test_create_skips_configure_when_already_configured( self, fresh_factory, tmp_path ): cfg = tmp_path / "pokrok.json" cfg.write_text("{}") fresh_factory.configure(filename=str(cfg)) assert fresh_factory.configured is True # second create() should not re-run configure (covers the already- # configured branch); a meter is still produced. assert fresh_factory.create(size=3) is not None def test_create_iterable_only_no_plugin_returns_iterable( self, fresh_factory, monkeypatch ): # Force no plugin available -> create() should return the raw iterable. fresh_factory.configure() monkeypatch.setattr( fresh_factory.plugins, "get_first_plugin", lambda *a, **k: None ) result = fresh_factory.create(iterable=range(3)) assert list(result) == [0, 1, 2] def test_create_no_plugin_no_iterable_returns_none( self, fresh_factory, monkeypatch ): fresh_factory.configure() monkeypatch.setattr( fresh_factory.plugins, "get_first_plugin", lambda *a, **k: None ) assert fresh_factory.create() is None def test_create_triggers_configure_when_unconfigured(self, fresh_factory): # create() calls configure() implicitly; with no config file present # `configured` stays False (it only flips True when a file is loaded), # but plugins are loaded lazily and a meter is still produced. assert fresh_factory.configured is False assert fresh_factory.plugins.plugins is None meter = fresh_factory.create(size=5) assert meter is not None assert fresh_factory.plugins.plugins is not None class TestPublicFunctions: def test_set_plugins_configures_singleton(self): pokrok.set_plugins(["tqdm"], exclusive=True) assert list(pokrok.FACTORY.plugins.plugins) == ["tqdm"] def test_set_styles_configures_singleton(self): pokrok.set_styles(custom=Style(widgets=["BAR"])) assert "custom" in pokrok.FACTORY.styles def test_configure_passthrough(self): pokrok.configure(plugin_names=["halo"], exclusive=True) assert list(pokrok.FACTORY.plugins.plugins) == ["halo"] def test_configure_with_plugin_option_kwargs(self): # arbitrary keyword args are forwarded to PluginManager.set_plugin_options # (a documented no-op placeholder); this should not raise. pokrok.configure(tqdm={"ncols": 80}) def test_progress_meter_returns_meter(self): meter = pokrok.progress_meter(size=10) assert meter is not None assert meter.is_sized is True class TestProgressRange: def test_range_with_stop(self): result = list(pokrok.progress_range(0, 10, 2)) assert result == [0, 2, 4, 6, 8] def test_range_stop_none_uses_start_as_size(self): result = list(pokrok.progress_range(5)) assert result == [0, 1, 2, 3, 4] def test_range_size_computed_with_step(self): # 0..10 step 3 -> ceil(10/3) == 4 items result = list(pokrok.progress_range(0, 10, 3)) assert result == [0, 3, 6, 9] class TestProgressIter: def test_none_iterable_raises(self): with pytest.raises(ValueError, match="Invalid iterable"): pokrok.progress_iter(None) def test_sized_iterable_infers_size(self): result = list(pokrok.progress_iter([1, 2, 3])) assert result == [1, 2, 3] def test_unsized_iterable_with_explicit_size(self): def gen(): yield from range(3) result = list(pokrok.progress_iter(gen(), size=3)) assert result == [0, 1, 2] def test_unsized_iterable_without_size(self): def gen(): yield from range(3) result = list(pokrok.progress_iter(gen())) assert result == [0, 1, 2] class TestProgressFile: def test_iterates_file_lines(self, tmp_path): f = tmp_path / "data.txt" f.write_text("a\nb\nc\n") lines = list(pokrok.progress_file(str(f), "rt")) assert lines == ["a\n", "b\n", "c\n"] pokrok-0.2.1/tests/test_plugin_halo.py000066400000000000000000000045341522221414100200730ustar00rootroot00000000000000"""Tests for the halo plugin against the real halo backend. Halo writes spinner frames to a stream; pytest captures stdout/stderr so the frames do not clutter test output. We assert on the plugin's provides() logic and meter lifecycle rather than rendered frames. """ import pokrok from pokrok.plugins import Status from pokrok.plugins.halo import HaloProgressMeter, HaloProgressMeterFactory from pokrok.styles import Widget def test_factory_name_and_installed(): f = HaloProgressMeterFactory() assert f.name == "halo" assert f.installed is True class TestProvides: def setup_method(self): self.f = HaloProgressMeterFactory() def test_sized_not_forced_is_false(self): assert self.f.provides(sized=True) is False def test_sized_forced_still_checks_widgets(self): # forced + spinner present -> True assert self.f.provides(sized=True, widgets=[Widget.SPINNER], force=True) is True def test_unsized_no_widgets_is_true(self): assert self.f.provides(sized=False) is True def test_unsized_only_spinner_strict_true(self): assert self.f.provides(sized=False, widgets=[Widget.SPINNER]) is True def test_unsized_spinner_plus_other_strict_false(self): assert ( self.f.provides(sized=False, widgets=[Widget.SPINNER, Widget.BAR]) is False ) def test_force_with_spinner_in_set_true(self): widgets = [Widget.SPINNER, Widget.BAR] assert self.f.provides(sized=False, widgets=widgets, force=True) is True def test_force_without_spinner_false(self): assert self.f.provides(sized=False, widgets=[Widget.BAR], force=True) is False def test_meter_lifecycle(): f = HaloProgressMeterFactory() meter = f.create(size=None, desc="loading") assert isinstance(meter, HaloProgressMeter) assert meter.status == Status.UNSTARTED meter.start() assert meter.status == Status.STARTED # increment is a documented no-op for a spinner meter.increment() meter.finish() assert meter.status == Status.FINISHED def test_iterate_yields_all_items(): f = HaloProgressMeterFactory() result = list(f.iterate(range(5), desc="spin")) assert result == [0, 1, 2, 3, 4] def test_end_to_end_via_api(): pokrok.set_plugins(["halo"], exclusive=True) result = list(pokrok.progress_iter(["x", "y"])) assert result == ["x", "y"] pokrok-0.2.1/tests/test_plugin_logging.py000066400000000000000000000161671522221414100206030ustar00rootroot00000000000000"""Tests for the logging plugin against the real stdlib logging backend. The logging plugin builds a format-string message from the requested widgets and emits it at a configurable interval. We attach a capturing handler to the configured logger and assert on the emitted records, which exercises the counter-scaling, bar, percent and interval branches. """ import logging import pytest import pokrok from pokrok.plugins import Status from pokrok.plugins.logging import ( STYLE_SUPERSET, LoggingProgressMeter, LoggingProgressMeterFactory, ) from pokrok.styles import Widget @pytest.fixture def capture(): """Attach a list-capturing handler to a uniquely named logger. Returns a (logger_name, records) tuple. The records list collects every LogRecord emitted to that logger. """ records = [] class _ListHandler(logging.Handler): def emit(self, record): records.append(record) name = "pokrok-test-logger" logger = logging.getLogger(name) handler = _ListHandler() logger.addHandler(handler) logger.setLevel(logging.DEBUG) yield name, records logger.removeHandler(handler) def _make(capture, **kwargs): name, records = capture f = LoggingProgressMeterFactory() meter = f.create(logger_name=name, **kwargs) return meter, records def test_factory_name_and_module(): f = LoggingProgressMeterFactory() # the plugin name is "Logging" and the backing module is the stdlib logging assert f.name == "Logging" assert f.installed is True def test_superset_has_sized_and_unsized(): assert Widget.BAR in STYLE_SUPERSET.get_widgets(True) assert Widget.COUNTER in STYLE_SUPERSET.get_widgets(False) assert Widget.BAR not in STYLE_SUPERSET.get_widgets(False) def test_lifecycle_and_finish_logs_total(capture): meter, records = _make(capture, size=10, interval=1) assert isinstance(meter, LoggingProgressMeter) meter.start() assert meter.status == Status.STARTED meter.increment() meter.finish() assert meter.status == Status.FINISHED # the finish() call logs a "Read a total of N records" summary assert any("Read a total of" in r.getMessage() for r in records) def test_interval_gating_only_logs_on_crossing(capture): # interval 5: increments at 1..4 do not cross a multiple of 5; the 5th does. meter, records = _make(capture, size=100, interval=5, widgets=[Widget.COUNTER]) meter.start() start_count = len(records) for _ in range(4): meter.increment() assert len(records) == start_count # no crossing yet meter.increment() # count now 5 -> crosses interval boundary assert len(records) == start_count + 1 meter.finish() def test_counter_unsized_format(capture): meter, records = _make(capture, size=None, interval=1, widgets=[Widget.COUNTER]) meter.start() meter.increment() msg = records[-1].getMessage() # the counter value is scaled via float division (count / scale), so the # bare "{count}" format renders the float, e.g. "1.0". assert msg == "1.0" meter.finish() def test_counter_sized_small_format(capture): meter, records = _make(capture, size=10, interval=1, widgets=[Widget.COUNTER]) meter.start() meter.increment() # small size (<1000): "count/size" with no scale suffix; count is a float. assert records[-1].getMessage() == "1.0/10" meter.finish() def test_counter_sized_large_scales_with_suffix(capture): # size >= 1000 triggers the k/M/... scaling branch. meter, records = _make(capture, size=2000, interval=1, widgets=[Widget.COUNTER]) meter.start() meter.increment() msg = records[-1].getMessage() # scaled message uses 2 decimals and a 'k' suffix, e.g. "0.00/2.00k" assert "k" in msg assert "/2.00" in msg meter.finish() def test_counter_with_unit(capture): meter, records = _make( capture, size=10, interval=1, widgets=[Widget.COUNTER], unit="recs" ) meter.start() meter.increment() assert "recs" in records[-1].getMessage() meter.finish() def test_bar_widget_renders_fill(capture): meter, records = _make(capture, size=10, interval=1, widgets=[Widget.BAR]) meter.start() meter.increment(5) # 50% -> 5 of 10 bar chars msg = records[-1].getMessage() assert msg.startswith("[") assert "*" in msg meter.finish() def test_percent_widget_with_counter(capture): meter, records = _make( capture, size=10, interval=1, widgets=[Widget.COUNTER, Widget.PERCENT] ) meter.start() meter.increment(5) msg = records[-1].getMessage() # percent is parenthesized when a counter is also present assert "(50%)" in msg meter.finish() def test_percent_widget_without_counter(capture): meter, records = _make(capture, size=10, interval=1, widgets=[Widget.PERCENT]) meter.start() meter.increment(3) msg = records[-1].getMessage() # bare percent (no parens) when no counter present assert "30%" in msg assert "(" not in msg meter.finish() def test_elapsed_widget(capture): meter, records = _make(capture, size=10, interval=1, widgets=[Widget.ELAPSED]) meter.start() meter.increment() assert "seconds" in records[-1].getMessage() meter.finish() def test_unsupported_widget_filtered_out(capture): # SPINNER is not in the logging superset; it should be dropped silently, # leaving only the supported COUNTER in the message. meter, records = _make( capture, size=10, interval=1, widgets=[Widget.SPINNER, Widget.COUNTER] ) meter.start() meter.increment() assert records[-1].getMessage() == "1.0/10" meter.finish() def test_desc_prepended(capture): meter, records = _make( capture, size=10, interval=1, desc="loading", widgets=[Widget.COUNTER] ) meter.start() meter.increment() assert records[-1].getMessage().startswith("loading ") meter.finish() def test_increment_multiplier(capture): meter, records = _make( capture, size=100, interval=1, multiplier=10, widgets=[Widget.COUNTER] ) meter.start() meter.increment(2) assert meter.count == 20 meter.finish() def test_numeric_logger_level_accepted(): # logger_level may be passed as an int; it should be stored as-is and used # directly by Logger.log (covers the non-string level branch). f = LoggingProgressMeterFactory() meter = f.create( size=10, interval=1, logger_level=logging.DEBUG, widgets=[Widget.COUNTER] ) assert meter._level == logging.DEBUG def test_fresh_logger_gets_default_handler(): # A logger with no pre-existing handlers gets a StreamHandler attached # (covers the hasHandlers() False branch). unique = "pokrok-fresh-logger-xyz" logging.getLogger(unique).handlers.clear() f = LoggingProgressMeterFactory() meter = f.create(size=10, logger_name=unique, widgets=[Widget.COUNTER]) assert meter._logger.hasHandlers() # clean up to avoid leaking the handler to other tests meter._logger.handlers.clear() def test_end_to_end_via_api(): pokrok.set_plugins(["logging"], exclusive=True) result = list(pokrok.progress_iter([1, 2, 3, 4, 5])) assert result == [1, 2, 3, 4, 5] pokrok-0.2.1/tests/test_plugin_progressbar2.py000066400000000000000000000054171522221414100215640ustar00rootroot00000000000000"""Tests for the progressbar2 plugin against the real progressbar backend.""" import progressbar import pokrok from pokrok.plugins import Status from pokrok.plugins.progressbar2 import ( PB_WIDGETS, Progressbar2ProgressMeter, Progressbar2ProgressMeterFactory, create_widgets, ) from pokrok.styles import Widget def test_factory_name_and_superset(): f = Progressbar2ProgressMeterFactory() # backend module name is "progressbar" assert f._package == "progressbar" assert f.installed is True superset = f.style_superset.get_widgets(True) assert Widget.COUNTER in superset assert Widget.PERCENT in superset def test_create_widgets_maps_every_pokrok_widget(): widgets = list(PB_WIDGETS.keys()) pb_widgets = create_widgets(progressbar, widgets, desc="task", unit="recs") # description prepended assert pb_widgets[0] == "task" # all mapped widget classes are real progressbar widget instances classes = {type(w).__name__ for w in pb_widgets if not isinstance(w, str)} assert "Bar" in classes assert "Counter" in classes assert "Percentage" in classes def test_create_widgets_unit_only_after_counter(): pb_widgets = create_widgets(progressbar, [Widget.COUNTER], unit="bytes") # unit string follows the counter assert "bytes" in pb_widgets def test_create_widgets_no_desc_no_leading_space(): pb_widgets = create_widgets(progressbar, [Widget.BAR]) # first element should be the Bar widget, not a separator space assert not (isinstance(pb_widgets[0], str) and pb_widgets[0] == " ") def test_meter_lifecycle_sized(): f = Progressbar2ProgressMeterFactory() meter = f.create(size=10, widgets=[Widget.BAR, Widget.PERCENT]) assert isinstance(meter, Progressbar2ProgressMeter) meter.start() assert meter.status == Status.STARTED meter.increment(4) assert meter.pb.value == 4 meter.finish() assert meter.status == Status.FINISHED def test_increment_applies_multiplier(): f = Progressbar2ProgressMeterFactory() meter = f.create(size=100, widgets=[Widget.BAR], multiplier=5) meter.start() meter.increment(2) assert meter.pb.value == 10 meter.finish() def test_unsized_uses_unknown_length(): f = Progressbar2ProgressMeterFactory() meter = f.create(size=None, widgets=[Widget.SPINNER]) # max_value should be UnknownLength sentinel for unsized meters assert meter.pb.max_value is progressbar.UnknownLength def test_iterate_fast_path(): f = Progressbar2ProgressMeterFactory() wrapped = f.iterate(range(4), size=4, widgets=[Widget.BAR]) assert list(wrapped) == [0, 1, 2, 3] def test_end_to_end_via_api(): pokrok.set_plugins(["progressbar2"], exclusive=True) result = list(pokrok.progress_iter([1, 2, 3])) assert result == [1, 2, 3] pokrok-0.2.1/tests/test_plugin_tqdm.py000066400000000000000000000036561522221414100201210ustar00rootroot00000000000000"""Tests for the tqdm plugin against the real tqdm backend.""" import pokrok from pokrok.plugins import Status from pokrok.plugins.tqdm import TqdmProgressMeter, TqdmProgressMeterFactory from pokrok.styles import Widget def test_factory_name_and_superset(): f = TqdmProgressMeterFactory() assert f.name == "tqdm" assert f.installed is True superset = f.style_superset.get_widgets(True) assert Widget.BAR in superset def test_create_meter_and_lifecycle(): f = TqdmProgressMeterFactory() meter = f.create(size=10, desc="work") assert isinstance(meter, TqdmProgressMeter) assert meter.status == Status.UNSTARTED meter.start() assert meter.status == Status.STARTED meter.increment(3) assert meter.tqdm.n == 3 meter.finish() assert meter.status == Status.FINISHED def test_increment_applies_multiplier(): f = TqdmProgressMeterFactory() meter = f.create(size=100, multiplier=10) meter.start() meter.increment(2) assert meter.tqdm.n == 20 meter.finish() def test_iterate_fast_path_returns_tqdm_iterable(): f = TqdmProgressMeterFactory() # multiplier None -> the optimized tqdm path is used (returns a tqdm object) wrapped = f.iterate(range(4), size=4, desc="d") assert list(wrapped) == [0, 1, 2, 3] def test_iterate_with_multiplier_uses_base_path(): f = TqdmProgressMeterFactory() # multiplier set -> falls back to BaseProgressMeterFactory.iterate (generator) wrapped = f.iterate(range(3), size=3, multiplier=2) assert list(wrapped) == [0, 1, 2] def test_end_to_end_via_api(): pokrok.set_plugins(["tqdm"], exclusive=True) result = list(pokrok.progress_iter([10, 20, 30])) assert result == [10, 20, 30] def test_context_manager_via_api(): pokrok.set_plugins(["tqdm"], exclusive=True) with pokrok.progress_meter(size=5) as bar: for _ in range(5): bar.increment() assert bar.status == Status.FINISHED pokrok-0.2.1/tests/test_plugins_base.py000066400000000000000000000220331522221414100202370ustar00rootroot00000000000000"""Tests for pokrok.plugins base classes: PluginManager, provides() logic, the ProgressMeter status state machine, and the DefaultProgressMeterFactory module-loading behavior. These tests use real entry points (declared in pyproject.toml) and the real backend libraries, which are installed as dev dependencies. """ import pytest from pokrok.plugins import ( BaseProgressMeter, BaseProgressMeterFactory, DefaultProgressMeterFactory, PluginManager, ProgressMeterError, Status, ) from pokrok.styles import Style, Widget class TestPluginManagerLoad: def test_load_discovers_entry_points(self): pm = PluginManager() pm.load_plugins() # All four built-in plugins have their backends installed as dev deps. assert set(pm.plugins) == {"tqdm", "progressbar2", "halo", "logging"} def test_load_with_names_orders_listed_first(self): pm = PluginManager() pm.load_plugins(names=["halo", "tqdm"]) names = list(pm.plugins) assert names[:2] == ["halo", "tqdm"] # non-exclusive keeps the rest too assert set(names) == {"tqdm", "progressbar2", "halo", "logging"} def test_load_exclusive_drops_unlisted(self): pm = PluginManager() pm.load_plugins(names=["tqdm"], exclusive=True) assert list(pm.plugins) == ["tqdm"] def test_load_names_ignores_unknown_name(self): pm = PluginManager() pm.load_plugins(names=["does-not-exist", "tqdm"], exclusive=True) assert list(pm.plugins) == ["tqdm"] def test_has_plugin_lazy_loads(self): pm = PluginManager() assert pm.plugins is None assert pm.has_plugin("tqdm") is True assert pm.plugins is not None def test_has_plugin_false_for_unknown(self): pm = PluginManager() assert pm.has_plugin("nope") is False def test_get_plugin_returns_instance(self): pm = PluginManager() pm.load_plugins() plugin = pm.get_plugin("tqdm") assert plugin.name == "tqdm" def test_get_plugin_unknown_raises(self): pm = PluginManager() pm.load_plugins() with pytest.raises(ValueError, match="No such plugin"): pm.get_plugin("nope") def test_set_plugin_options_is_noop(self): # documented placeholder; should not raise pm = PluginManager() pm.set_plugin_options({"anything": 1}, foo="bar") class TestGetFirstPlugin: def test_empty_plugins_returns_none(self): pm = PluginManager() pm.plugins = {} # simulate "loaded, none installed" assert pm.get_first_plugin(sized=True, widgets=[Widget.BAR]) is None def test_no_constraints_returns_first(self): pm = PluginManager() pm.load_plugins(names=["tqdm"], exclusive=True) assert pm.get_first_plugin().name == "tqdm" def test_lazy_loads_when_none(self): pm = PluginManager() assert pm.plugins is None plugin = pm.get_first_plugin() assert plugin is not None def test_strict_match_selects_capable_plugin(self): pm = PluginManager() # halo only provides SPINNER and only for unsized; tqdm provides BAR. pm.load_plugins(names=["halo", "tqdm"]) plugin = pm.get_first_plugin(sized=True, widgets=[Widget.BAR]) # halo cannot provide a sized BAR, so tqdm should be chosen. assert plugin.name == "tqdm" def test_force_fallback_when_no_strict_match(self): pm = PluginManager() # Request a widget that no plugin provides strictly in its superset for # this configuration, forcing the force=True second pass. pm.load_plugins(names=["halo"], exclusive=True) # halo strictly only supports unsized SPINNER. Ask for unsized SPINNER # plus a counter -> not a strict subset, should fall through to force. plugin = pm.get_first_plugin( sized=False, widgets=[Widget.SPINNER, Widget.COUNTER] ) assert plugin is not None assert plugin.name == "halo" class _SupersetFactory(BaseProgressMeterFactory): """Minimal concrete factory to exercise BaseProgressMeterFactory.provides.""" def __init__(self, superset): self._superset = superset @property def name(self): return "test" @property def installed(self): return True @property def style_superset(self): return self._superset def create(self, *args, **kwargs): # pragma: no cover - not exercised here return None class TestProvides: def test_no_widgets_requested_returns_true(self): f = _SupersetFactory(Style(sized=[Widget.BAR])) assert f.provides(sized=True, widgets=None) is True def test_no_superset_returns_true(self): f = _SupersetFactory(None) assert f.provides(sized=True, widgets=[Widget.BAR]) is True def test_superset_with_no_widgets_for_dimension_returns_false(self): # superset has sized widgets but none for unsized f = _SupersetFactory(Style(sized=[Widget.BAR])) assert f.provides(sized=False, widgets=[Widget.SPINNER]) is False def test_strict_requires_all_widgets_in_superset(self): f = _SupersetFactory(Style(sized=[Widget.BAR, Widget.ETA])) assert f.provides(sized=True, widgets=[Widget.BAR]) is True assert f.provides(sized=True, widgets=[Widget.BAR, Widget.SPINNER]) is False def test_force_requires_any_overlap(self): f = _SupersetFactory(Style(sized=[Widget.BAR])) # BAR overlaps -> force True assert f.provides(sized=True, widgets=[Widget.BAR, Widget.SPINNER], force=True) # no overlap -> force still False assert not f.provides(sized=True, widgets=[Widget.SPINNER], force=True) class TestDefaultFactoryModuleLoading: def test_installed_true_for_real_module(self): f = DefaultProgressMeterFactory("tqdm", object, None) assert f.installed is True def test_installed_false_for_missing_module(self): f = DefaultProgressMeterFactory( "no-such-pkg", object, None, module_name="definitely_not_installed_xyz" ) assert f.installed is False def test_create_returns_none_when_module_missing(self): f = DefaultProgressMeterFactory( "no-such-pkg", object, None, module_name="definitely_not_installed_xyz" ) assert f.create(size=10) is None def test_name_property(self): f = DefaultProgressMeterFactory("foo", object, None) assert f.name == "foo" class _Meter(BaseProgressMeter): """Concrete BaseProgressMeter for exercising the status state machine.""" def increment(self, n=1): self._check_status(Status.STARTED) return n class TestStatusStateMachine: def test_initial_status_unstarted(self): m = _Meter(size=10) assert m.status == Status.UNSTARTED assert m.is_sized is True def test_unsized_when_no_size(self): assert _Meter().is_sized is False def test_start_transitions_to_started(self): m = _Meter() m.start() assert m.status == Status.STARTED def test_finish_transitions_to_finished(self): m = _Meter() m.start() m.finish() assert m.status == Status.FINISHED def test_start_twice_raises(self): m = _Meter() m.start() with pytest.raises(ProgressMeterError): m.start() def test_finish_before_start_raises(self): m = _Meter() with pytest.raises(ProgressMeterError): m.finish() def test_increment_before_start_raises(self): m = _Meter() with pytest.raises(ProgressMeterError): m.increment() def test_check_status_error_false_returns_bool(self): m = _Meter() assert m._check_status(Status.STARTED, error=False) is False m.start() assert m._check_status(Status.STARTED, error=False) is True def test_context_manager_drives_status(self): m = _Meter() with m as entered: assert entered is m assert m.status == Status.STARTED assert m.status == Status.FINISHED class TestBaseFactoryIterate: def test_iterate_yields_items_and_increments(self): """BaseProgressMeterFactory.iterate wraps create() in a context manager. We drive it through the logging plugin's factory (a BaseProgressMeter backend) and confirm every item is yielded. """ from pokrok.plugins.logging import LoggingProgressMeterFactory f = LoggingProgressMeterFactory() items = list(f.iterate(range(5), size=5)) assert items == [0, 1, 2, 3, 4] def test_iterate_returns_raw_iterable_when_create_returns_none(self, monkeypatch): """When create() yields no meter, iterate falls back to the raw iterable.""" from pokrok.plugins.logging import LoggingProgressMeterFactory f = LoggingProgressMeterFactory() monkeypatch.setattr(f, "create", lambda *a, **k: None) # iterate() is a generator; with no meter it yields the source items # through unchanged. assert list(f.iterate([1, 2, 3], size=3)) == [1, 2, 3] pokrok-0.2.1/tests/test_styles.py000066400000000000000000000070461522221414100171160ustar00rootroot00000000000000"""Tests for pokrok.styles: Widget enum, Style, StyleManager, _resolve_widgets.""" import pytest from pokrok.styles import Style, StyleManager, Widget, _resolve_widgets class TestResolveWidgets: def test_none_returns_none(self): assert _resolve_widgets(None) is None def test_empty_list_returns_empty_list(self): assert _resolve_widgets([]) == [] def test_string_names_resolve_to_enum(self): assert _resolve_widgets(["BAR", "ETA"]) == [Widget.BAR, Widget.ETA] def test_enum_values_passed_through(self): assert _resolve_widgets([Widget.SPINNER]) == [Widget.SPINNER] def test_mixed_string_and_enum(self): assert _resolve_widgets(["BAR", Widget.SPINNER]) == [ Widget.BAR, Widget.SPINNER, ] def test_unknown_string_raises_keyerror(self): with pytest.raises(KeyError): _resolve_widgets(["NOT_A_WIDGET"]) class TestStyle: def test_default_style_has_no_widgets(self): style = Style() assert style.sized is None assert style.unsized is None def test_widgets_apply_to_both_sized_and_unsized(self): style = Style(widgets=["BAR", "ETA"]) assert style.sized == [Widget.BAR, Widget.ETA] assert style.unsized == [Widget.BAR, Widget.ETA] def test_sized_overrides_widgets_for_sized(self): style = Style(widgets=["BAR"], sized=["COUNTER"]) assert style.sized == [Widget.COUNTER] # unsized falls back to widgets since unsized not provided assert style.unsized == [Widget.BAR] def test_unsized_overrides_widgets_for_unsized(self): style = Style(widgets=["BAR"], unsized=["SPINNER"]) assert style.sized == [Widget.BAR] assert style.unsized == [Widget.SPINNER] def test_no_widgets_uses_sized_and_unsized_directly(self): style = Style(sized=["BAR"], unsized=["SPINNER"]) assert style.sized == [Widget.BAR] assert style.unsized == [Widget.SPINNER] def test_get_widgets_selects_by_sized_flag(self): style = Style(sized=["BAR"], unsized=["SPINNER"]) assert style.get_widgets(True) == [Widget.BAR] assert style.get_widgets(False) == [Widget.SPINNER] class TestStyleManager: def test_default_style_present(self): sm = StyleManager() assert "default" in sm assert isinstance(sm["default"], Style) def test_custom_default_style(self): custom = Style(widgets=["BAR"]) sm = StyleManager(default_style=custom) assert sm["default"] is custom def test_set_style_options_adds_named_styles(self): sm = StyleManager() sm.set_style_options({"styles": {"fancy": {"widgets": ["BAR", "ETA"]}}}) assert "fancy" in sm assert sm["fancy"].sized == [Widget.BAR, Widget.ETA] def test_set_style_options_default_as_string_reference(self): sm = StyleManager() sm.set_style_options( { "styles": {"fancy": {"widgets": ["SPINNER"]}}, "default_style": "fancy", } ) # the default now points at the previously-defined fancy style assert sm["default"] is sm["fancy"] def test_set_style_options_default_as_inline_config(self): sm = StyleManager() sm.set_style_options({"default_style": {"widgets": ["COUNTER"]}}) assert sm["default"].sized == [Widget.COUNTER] def test_set_style_options_no_keys_is_noop(self): sm = StyleManager() before = sm["default"] sm.set_style_options({}) assert sm["default"] is before pokrok-0.2.1/uv.lock000066400000000000000000002060441522221414100143230ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.14.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf", size = 219722, upload-time = "2026-05-26T20:38:14.002Z" }, { url = "https://files.pythonhosted.org/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf", size = 220240, upload-time = "2026-05-26T20:38:17.424Z" }, { url = "https://files.pythonhosted.org/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d", size = 246981, upload-time = "2026-05-26T20:38:19.266Z" }, { url = "https://files.pythonhosted.org/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2", size = 248812, upload-time = "2026-05-26T20:38:20.75Z" }, { url = "https://files.pythonhosted.org/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47", size = 250675, upload-time = "2026-05-26T20:38:22.159Z" }, { url = "https://files.pythonhosted.org/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550", size = 252590, upload-time = "2026-05-26T20:38:23.63Z" }, { url = "https://files.pythonhosted.org/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e", size = 247691, upload-time = "2026-05-26T20:38:25.057Z" }, { url = "https://files.pythonhosted.org/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f", size = 248716, upload-time = "2026-05-26T20:38:26.406Z" }, { url = "https://files.pythonhosted.org/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1", size = 246721, upload-time = "2026-05-26T20:38:28.282Z" }, { url = "https://files.pythonhosted.org/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5", size = 250533, upload-time = "2026-05-26T20:38:29.753Z" }, { url = "https://files.pythonhosted.org/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b", size = 246990, upload-time = "2026-05-26T20:38:31.516Z" }, { url = "https://files.pythonhosted.org/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332", size = 247593, upload-time = "2026-05-26T20:38:33.065Z" }, { url = "https://files.pythonhosted.org/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59", size = 222330, upload-time = "2026-05-26T20:38:35.36Z" }, { url = "https://files.pythonhosted.org/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253", size = 223261, upload-time = "2026-05-26T20:38:37.196Z" }, { url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" }, { url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" }, { url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" }, { url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" }, { url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" }, { url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" }, { url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" }, { url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" }, { url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" }, { url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" }, { url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" }, { url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" }, { url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" }, { url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" }, { url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" }, { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "halo" version = "0.0.31" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, { name = "log-symbols" }, { name = "six" }, { name = "spinners" }, { name = "termcolor" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/48/d53580d30b1fabf25d0d1fcc3f5b26d08d2ac75a1890ff6d262f9f027436/halo-0.0.31.tar.gz", hash = "sha256:7b67a3521ee91d53b7152d4ee3452811e1d2a6321975137762eb3d70063cc9d6", size = 11666, upload-time = "2020-11-10T02:36:48.335Z" } [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "log-symbols" version = "0.0.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/87/e86645d758a4401c8c81914b6a88470634d1785c9ad09823fa4a1bd89250/log_symbols-0.0.14.tar.gz", hash = "sha256:cf0bbc6fe1a8e53f0d174a716bc625c4f87043cc21eb55dd8a740cfe22680556", size = 3211, upload-time = "2019-08-08T06:32:22.538Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/5d/d710c38be68b0fb54e645048fe359c3904cc3cb64b2de9d40e1712bf110c/log_symbols-0.0.14-py3-none-any.whl", hash = "sha256:4952106ff8b605ab7d5081dd2c7e6ca7374584eff7086f499c06edd1ce56dcca", size = 3081, upload-time = "2019-08-08T06:32:20.604Z" }, ] [[package]] name = "packaging" version = "26.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pokrok" source = { editable = "." } [package.dev-dependencies] dev = [ { name = "halo" }, { name = "progressbar2" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, { name = "tqdm" }, ] [package.metadata] [package.metadata.requires-dev] dev = [ { name = "halo", specifier = ">=0.0.31" }, { name = "progressbar2", specifier = ">=4" }, { name = "pytest", specifier = ">=8" }, { name = "pytest-cov", specifier = ">=5" }, { name = "ruff", specifier = ">=0.6" }, { name = "tqdm", specifier = ">=4" }, ] [[package]] name = "progressbar2" version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-utils" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/24/3587e795fc590611434e4bcb9fbe0c3dddb5754ce1a20edfd86c587c0004/progressbar2-4.5.0.tar.gz", hash = "sha256:6662cb624886ed31eb94daf61e27583b5144ebc7383a17bae076f8f4f59088fb", size = 101449, upload-time = "2024-08-28T22:50:12.391Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ee/94/448f037fb0ffd0e8a63b625cf9f5b13494b88d15573a987be8aaa735579d/progressbar2-4.5.0-py3-none-any.whl", hash = "sha256:625c94a54e63915b3959355e6d4aacd63a00219e5f3e2b12181b76867bf6f628", size = 57132, upload-time = "2024-08-28T22:50:10.264Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pytest" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, ] [[package]] name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] name = "python-utils" version = "3.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/4c/ef8b7b1046d65c1f18ca31e5235c7d6627ca2b3f389ab1d44a74d22f5cc9/python_utils-3.9.1.tar.gz", hash = "sha256:eb574b4292415eb230f094cbf50ab5ef36e3579b8f09e9f2ba74af70891449a0", size = 35403, upload-time = "2024-11-26T00:38:58.736Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl", hash = "sha256:0273d7363c7ad4b70999b2791d5ba6b55333d6f7a4e4c8b6b39fb82b5fab4613", size = 32078, upload-time = "2024-11-26T00:38:57.488Z" }, ] [[package]] name = "ruff" version = "0.15.17" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" }, { url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" }, { url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" }, { url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" }, { url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" }, { url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" }, { url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" }, { url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" }, { url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" }, { url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" }, { url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" }, { url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" }, { url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" }, { url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" }, { url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" }, { url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" }, { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "spinners" version = "0.0.24" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d3/91/bb331f0a43e04d950a710f402a0986a54147a35818df0e1658551c8d12e1/spinners-0.0.24.tar.gz", hash = "sha256:1eb6aeb4781d72ab42ed8a01dcf20f3002bf50740d7154d12fb8c9769bf9e27f", size = 5308, upload-time = "2020-02-19T21:42:32.326Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/8e/3310207a68118000ca27ac878b8386123628b335ecb3d4bec4743357f0d1/spinners-0.0.24-py3-none-any.whl", hash = "sha256:2fa30d0b72c9650ad12bbe031c9943b8d441e41b4f5602b0ec977a19f3290e98", size = 5499, upload-time = "2020-02-19T21:42:30.876Z" }, ] [[package]] name = "termcolor" version = "3.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "tqdm" version = "4.68.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/05/0d5260f1f1ca784f4a4a0def9cbe6affe587f5b4025328d446c3d67765f4/tqdm-4.68.2.tar.gz", hash = "sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add", size = 171923, upload-time = "2026-06-09T13:26:42.539Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/75/1a0392bcc21c44dcdf87b3cf2d137e7829be2c083a1e38d44efca3d57a16/tqdm-4.68.2-py3-none-any.whl", hash = "sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede", size = 78578, upload-time = "2026-06-09T13:26:40.731Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ]