diff --git a/.github/workflows/publish_testpypi.yml b/.github/workflows/publish_testpypi.yml index be56130a..73de2001 100644 --- a/.github/workflows/publish_testpypi.yml +++ b/.github/workflows/publish_testpypi.yml @@ -12,7 +12,7 @@ jobs: max-parallel: 2 matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python_v: ['3.8', '3.9', '3.10', ''] + python_v: ['3.9', '3.10', '3.11', ''] # chrome_v: ['-1'] defaults: run: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1aba10fe..35e27aaa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} + env: + PYTHONTRACEMALLOC: "1" + PYTHONWARNINGS: "error::ResourceWarning" defaults: run: working-directory: ./src/py/ diff --git a/.gitignore b/.gitignore index 068efd7f..31e47007 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ src/py/integration_tests/report* node_modules/ src/py/site/* +.hypothesis/ diff --git a/src/py/.pre-commit-config.yaml b/src/py/.pre-commit-config.yaml index 5f875d93..41499ae6 100644 --- a/src/py/.pre-commit-config.yaml +++ b/src/py/.pre-commit-config.yaml @@ -14,7 +14,7 @@ default_install_hook_types: [pre-commit, commit-msg] default_stages: [pre-commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,12 +25,12 @@ repos: - id: check-toml - id: debug-statements - repo: https://github.com/asottile/add-trailing-comma - rev: v3.1.0 + rev: v3.2.0 hooks: - id: add-trailing-comma - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.10 + rev: v0.13.3 hooks: # Run the linter. - id: ruff @@ -40,7 +40,7 @@ repos: types_or: [python, pyi] # options: ignore one line things [E701] - repo: https://github.com/adrienverge/yamllint - rev: v1.35.1 + rev: v1.37.1 hooks: - id: yamllint name: yamllint @@ -55,7 +55,7 @@ repos: }\ }"] - repo: https://github.com/rhysd/actionlint - rev: v1.7.4 + rev: v1.7.7 hooks: - id: actionlint name: Lint GitHub Actions workflow files @@ -76,18 +76,9 @@ repos: args: [--staged, -c, "general.ignore=B6,T3", --msg-filename] stages: [commit-msg] - repo: https://github.com/crate-ci/typos - rev: v1.28.2 + rev: v1 hooks: - id: typos - - repo: https://github.com/markdownlint/markdownlint - rev: v0.13.0 - hooks: - - id: markdownlint - name: Markdownlint - description: Run markdownlint on your Markdown files - entry: mdl --rules ~MD026 --style .markdown.rb - language: ruby - files: \.(md|mdown|markdown)$ - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: @@ -96,3 +87,16 @@ repos: language: python entry: detect-secrets-hook args: [''] + - repo: https://github.com/rvben/rumdl-pre-commit + rev: v0.0.153 # Use the latest release tag + hooks: + - id: rumdl + # To only check (default): + # args: [] + # To automatically fix issues: + # args: [--fix] + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.406 # pin a tag; latest as of 2025-10-01 + hooks: + - id: pyright + args: ["--project=src/py"] diff --git a/src/py/CHANGELOG.txt b/src/py/CHANGELOG.txt index 57b4de61..a9a25322 100644 --- a/src/py/CHANGELOG.txt +++ b/src/py/CHANGELOG.txt @@ -1,3 +1,11 @@ +v1.3.0 +- Significant refactor, better organization +- `write_fig` and `_from_object` now take an additional argument: + `fail_on_error: bool`. If False, default True, returns a list of errors. +- Unused `path` argument for `calc_fig` was removed. +- Fixed race condition where two render tasks would choose the same filename + + v1.2.0 - Try to use plotly JSON encoder instead of default diff --git a/src/py/kaleido/__init__.py b/src/py/kaleido/__init__.py index 71c18427..900e40b9 100644 --- a/src/py/kaleido/__init__.py +++ b/src/py/kaleido/__init__.py @@ -19,11 +19,13 @@ from pathlib import Path from typing import Any, TypeVar, Union - from ._fig_tools import Figurish, LayoutOpts + from ._utils.fig_tools import Figurish, LayoutOpts T = TypeVar("T") AnyIterable = Union[AsyncIterable[T], Iterable[T]] + from .kaleido import FigureDict + __all__ = [ "Kaleido", "PageGenerator", @@ -50,7 +52,7 @@ def start_sync_server(*args: Any, silence_warnings: bool = False, **kwargs: Any) function will warn you if the server is already running. This wrapper function takes the exact same arguments as kaleido.Kaleido(), - except one extra, `silence_warnings`. + except one extra: `silence_warnings`. Args: *args: all arguments `Kaleido()` would take. @@ -64,11 +66,13 @@ def start_sync_server(*args: Any, silence_warnings: bool = False, **kwargs: Any) def stop_sync_server(*, silence_warnings: bool = False): """ - Stop the kaleido server. It can be restarted. Warns if not started. + Stop the kaleido server. It can be restarted. + + This function will warn you if the server is already stopped. Args: silence_warnings: (bool, default False): If True, don't emit warning if - stopping a server that's not running. + stopping an already stopped server. """ _global_server.close(silence_warnings=silence_warnings) @@ -76,7 +80,6 @@ def stop_sync_server(*, silence_warnings: bool = False): async def calc_fig( fig: Figurish, - path: str | None | Path = None, opts: LayoutOpts | None = None, *, topojson: str | None = None, @@ -86,14 +89,13 @@ async def calc_fig( Return binary for plotly figure. A convenience wrapper for `Kaleido.calc_fig()` which starts a `Kaleido` and - executes the `calc_fig()`. + executes `calc_fig()`. It takes an additional argument, `kopts`, a dictionary of arguments to pass to the kaleido process. See the `kaleido.Kaleido` docs. However, `calc_fig()` will never use more than one processor, so any `n` value will be overridden. - - See documentation for `Kaleido.calc_fig()`. + See also the documentation for `Kaleido.calc_fig()`. """ kopts = kopts or {} @@ -101,7 +103,6 @@ async def calc_fig( async with Kaleido(**kopts) as k: return await k.calc_fig( fig, - path=path, opts=opts, topojson=topojson, ) @@ -122,14 +123,13 @@ async def write_fig( A convenience wrapper for `Kaleido.write_fig()` which starts a `Kaleido` and executes the `write_fig()`. It takes an additional argument, `kopts`, a dictionary of arguments to pass - to the kaleido process. See the `kaleido.Kaleido` docs. - + to the `Kaleido` constructor. See the `kaleido.Kaleido` docs. - See documentation for `Kaleido.write_fig()` for the other arguments. + See also the documentation for `Kaleido.write_fig()`. """ async with Kaleido(**(kopts or {})) as k: - await k.write_fig( + return await k.write_fig( fig, path=path, opts=opts, @@ -139,26 +139,25 @@ async def write_fig( async def write_fig_from_object( - generator: AnyIterable, # this could be more specific with [] + fig_dicts: FigureDict | AnyIterable[FigureDict], *, kopts: dict[str, Any] | None = None, **kwargs, ): """ - Write a plotly figure(s) to a file. + Write a plotly figure(s) to a file specified by a dictionary or iterable of. A convenience wrapper for `Kaleido.write_fig_from_object()` which starts a `Kaleido` and executes the `write_fig_from_object()` It takes an additional argument, `kopts`, a dictionary of arguments to pass - to the kaleido process. See the `kaleido.Kaleido` docs. + to the `Kaleido` constructor. See the `kaleido.Kaleido` docs. - See documentation for `Kaleido.write_fig_from_object()` for the other - arguments. + See also the documentation for `Kaleido.write_fig_from_object()`. """ async with Kaleido(**(kopts or {})) as k: - await k.write_fig_from_object( - generator, + return await k.write_fig_from_object( + fig_dicts, **kwargs, ) @@ -174,14 +173,18 @@ def calc_fig_sync(*args: Any, **kwargs: Any): def write_fig_sync(*args: Any, **kwargs: Any): """Call `write_fig` but blocking.""" if _global_server.is_running(): - _global_server.call_function("write_fig", *args, **kwargs) + return _global_server.call_function("write_fig", *args, **kwargs) else: - _sync_server.oneshot_async_run(write_fig, args=args, kwargs=kwargs) + return _sync_server.oneshot_async_run(write_fig, args=args, kwargs=kwargs) def write_fig_from_object_sync(*args: Any, **kwargs: Any): """Call `write_fig_from_object` but blocking.""" if _global_server.is_running(): - _global_server.call_function("write_fig_from_object", *args, **kwargs) + return _global_server.call_function("write_fig_from_object", *args, **kwargs) else: - _sync_server.oneshot_async_run(write_fig_from_object, args=args, kwargs=kwargs) + return _sync_server.oneshot_async_run( + write_fig_from_object, + args=args, + kwargs=kwargs, + ) diff --git a/src/py/kaleido/_fig_tools.py b/src/py/kaleido/_fig_tools.py deleted file mode 100644 index 30701808..00000000 --- a/src/py/kaleido/_fig_tools.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -Adapted from old code, it 1. validates, 2. write defaults, 3. packages object. - -Its a bit complicated and mixed in order. -""" - -from __future__ import annotations - -import glob -import re -from pathlib import Path -from typing import TYPE_CHECKING, Literal, TypedDict - -import logistro - -if TYPE_CHECKING: - from typing import Any - - from typing_extensions import TypeGuard - - Figurish = Any # Be nice to make it more specific, dictionary or something - FormatString = Literal["png", "jpg", "jpeg", "webp", "svg", "json", "pdf"] - -_logger = logistro.getLogger(__name__) - -# constants -DEFAULT_EXT = "png" -DEFAULT_SCALE = 1 -DEFAULT_WIDTH = 700 -DEFAULT_HEIGHT = 500 -SUPPORTED_FORMATS: tuple[FormatString, ...] = ( - "png", - "jpg", - "jpeg", - "webp", - "svg", - "json", - "pdf", -) - - -def _assert_format(ext: str) -> TypeGuard[FormatString]: - if ext not in SUPPORTED_FORMATS: - raise ValueError( - f"Invalid format '{ext}'.\n Supported formats: {SUPPORTED_FORMATS!s}", - ) - return True - - -def _is_figurish(o: Any) -> TypeGuard[Figurish]: - valid = hasattr(o, "to_dict") or (isinstance(o, dict) and "data" in o) - if not valid: - _logger.debug( - f"Figure has to_dict? {hasattr(o, 'to_dict')} " - f"is dict? {isinstance(o, dict)} " - f"Keys: {o.keys() if hasattr(o, 'keys') else None!s}", - ) - return valid - - -def _get_figure_dimensions( - layout: dict, - width: float | None, - height: float | None, -) -> tuple[float, float]: - # Compute image width / height with fallbacks - width = ( - width - or layout.get("width") - or layout.get("template", {}).get("layout", {}).get("width") - or DEFAULT_WIDTH - ) - height = ( - height - or layout.get("height") - or layout.get("template", {}).get("layout", {}).get("height") - or DEFAULT_HEIGHT - ) - return width, height - - -def _get_format(extension: str) -> FormatString: - formatted_extension = extension.lower() - if formatted_extension == "jpg": - return "jpeg" - if not _assert_format(formatted_extension): - raise ValueError # this line will never be reached its for typer - return formatted_extension - - -# Input of to_spec (user gives us this) -class LayoutOpts(TypedDict, total=False): - format: FormatString | None - scale: int | float - height: int | float - width: int | float - - -# Output of to_spec (we give kaleido_scopes.js this) -# refactor note: this could easily be right before send -class Spec(TypedDict): - format: FormatString - width: int | float - height: int | float - scale: int | float - data: Figurish - - -# validate configuration options for kaleido.js and package like its wants -def to_spec(figure: Figurish, layout_opts: LayoutOpts) -> Spec: - # Get figure layout - layout = figure.get("layout", {}) - - for k, v in layout_opts.items(): - if k == "format": - if v is not None and not isinstance(v, (str)): - raise TypeError( - f"{k} must be one of {SUPPORTED_FORMATS!s} or None, not {v}.", - ) - elif k in ("scale", "height", "width"): - if v is not None and not isinstance(v, (float, int)): - raise TypeError(f"{k} must be numeric or None, not {v}.") - else: - raise AttributeError(f"Unknown key in layout options, {k}") - - # Extract info - extension = _get_format(layout_opts.get("format") or DEFAULT_EXT) - - width, height = _get_figure_dimensions( - layout, - layout_opts.get("width"), - layout_opts.get("height"), - ) - scale = layout_opts.get("scale", DEFAULT_SCALE) - - return { - "format": extension, - "width": width, - "height": height, - "scale": scale, - "data": figure, - } - - -# if we need to suffix the filename automatically: -def _next_filename(path: Path | str, prefix: str, ext: str) -> str: - path = path if isinstance(path, Path) else Path(path) - default = 1 if (path / f"{prefix}.{ext}").exists() else 0 - re_number = re.compile( - r"^" + re.escape(prefix) + r"\-(\d+)\." + re.escape(ext) + r"$", - ) - escaped_prefix = glob.escape(prefix) - escaped_ext = glob.escape(ext) - numbers = [ - int(match.group(1)) - for name in path.glob(f"{escaped_prefix}-*.{escaped_ext}") - if (match := re_number.match(Path(name).name)) - ] - n = max(numbers, default=default) + 1 - return f"{prefix}.{ext}" if n == 1 else f"{prefix}-{n}.{ext}" - - -# validate and build full route if needed: -def _build_full_path( - path: Path | None, - fig: Figurish, - ext: FormatString, -) -> Path: - full_path: Path | None = None - - directory: Path - - if not path: - directory = Path() # use current Path - elif path and (not path.suffix or path.is_dir()): - if not path.is_dir(): - raise ValueError(f"Directory {path} not found. Please create it.") - directory = path - else: - full_path = path - if not full_path.parent.is_dir(): - raise RuntimeError( - f"Cannot reach path {path.parent}. Are all directories created?", - ) - - if not full_path: - _logger.debug("Looking for title") - prefix = fig.get("layout", {}).get("title", {}).get("text", "fig") - prefix = re.sub(r"[ \-]", "_", prefix) - prefix = re.sub(r"[^a-zA-Z0-9_]", "", prefix) - prefix = prefix or "fig" - _logger.debug(f"Found: {prefix}") - name = _next_filename(directory, prefix, ext) - full_path = directory / name - return full_path - - -# call all validators/automatic config fill-in/packaging in expected format -def build_fig_spec( - fig: Figurish, - path: Path | str | None, - opts: LayoutOpts | None, -) -> tuple[Spec, Path]: - if not opts: - opts = {} - - if not _is_figurish(fig): - raise TypeError("Figure supplied doesn't seem to be a valid plotly figure.") - - if hasattr(fig, "to_dict"): - fig = fig.to_dict() - - if isinstance(path, str): - path = Path(path) - elif path and not isinstance(path, Path): - raise TypeError("Path should be a string or `pathlib.Path` object (or None)") - - if not opts.get("format") and path and path.suffix: - ext = path.suffix.lstrip(".") - if _assert_format(ext): # not strict necessary if but helps typeguard - opts["format"] = ext - - spec = to_spec(fig, opts) - - full_path = _build_full_path(path, fig, spec["format"]) - - return spec, full_path diff --git a/src/py/kaleido/_kaleido_tab.py b/src/py/kaleido/_kaleido_tab.py deleted file mode 100644 index 6288fa06..00000000 --- a/src/py/kaleido/_kaleido_tab.py +++ /dev/null @@ -1,380 +0,0 @@ -from __future__ import annotations - -import base64 -import json -import time -from typing import TYPE_CHECKING - -import logistro -from choreographer.errors import DevtoolsProtocolError - -from ._utils import ErrorEntry, to_thread - -if TYPE_CHECKING: - from pathlib import Path - from typing import Any - - import choreographer as choreo - -_logger = logistro.getLogger(__name__) - -_TEXT_FORMATS = ("svg", "json") # eps - - -class JavascriptError(RuntimeError): # TODO(A): process better # noqa: TD003, FIX002 - """Used to report errors from javascript.""" - - -### Error definitions ### -class KaleidoError(Exception): - """An error to interpret errors from Kaleido's JS side.""" - - def __init__(self, code, message): - """ - Construct an error object. - - Args: - code: the number code of the error. - message: the message of the error. - - """ - super().__init__(message) - self._code = code - self._message = message - - def __str__(self): - """Display the KaleidoError nicely.""" - return f"Error {self._code}: {self._message}" - - -def _check_error(result): - e = _check_error_ret(result) - if e: - raise e - - -def _check_error_ret(result): # Utility - """Check browser response for errors. Helper function.""" - if "error" in result: - return DevtoolsProtocolError(result) - if result.get("result", {}).get("result", {}).get("subtype", None) == "error": - return JavascriptError(str(result.get("result"))) - return None - - -def _make_console_logger(name, log): - """Create printer specifically for console events. Helper function.""" - - async def console_printer(event): - _logger.debug2(f"{name}:{event}") # TODO(A): parse # noqa: TD003, FIX002 - log.append(str(event)) - - return console_printer - - -class _KaleidoTab: - """ - A Kaleido tab is a wrapped choreographer tab providing the functions we need. - - The choreographer tab can be access through the `self.tab` attribute. - """ - - tab: choreo.Tab - """The underlying choreographer tab.""" - - javascript_log: list[Any] - """A list of console outputs from the tab.""" - - def __init__(self, tab, *, _stepper=False): - """ - Create a new _KaleidoTab. - - Args: - tab: the choreographer tab to wrap. - - """ - self.tab = tab - self.javascript_log = [] - self._stepper = _stepper - - def _regenerate_javascript_console(self): - tab = self.tab - self.javascript_log = [] - _logger.debug2("Subscribing to all console prints for tab {tab}.") - tab.unsubscribe("Runtime.consoleAPICalled") - tab.subscribe( - "Runtime.consoleAPICalled", - _make_console_logger("tab js console", self.javascript_log), - ) - - async def navigate(self, url: str | Path = ""): - """ - Navigate to the kaleidofier script. This is effectively the real initialization. - - Args: - url: Override the location of the kaleidofier script if necessary. - - """ - tab = self.tab - javascript_ready = tab.subscribe_once("Runtime.executionContextCreated") - while javascript_ready.done(): - _logger.debug2("Clearing an old Runtime.executionContextCreated") - javascript_ready = tab.subscribe_once("Runtime.executionContextCreated") - page_ready = tab.subscribe_once("Page.loadEventFired") - while page_ready.done(): - _logger.debug2("Clearing a old Page.loadEventFired") - page_ready = tab.subscribe_once("Page.loadEventFired") - - _logger.debug2(f"Calling Page.navigate on {tab}") - _check_error(await tab.send_command("Page.navigate", params={"url": url})) - # Must enable after navigating. - _logger.debug2(f"Calling Page.enable on {tab}") - _check_error(await tab.send_command("Page.enable")) - _logger.debug2(f"Calling Runtime.enable on {tab}") - _check_error(await tab.send_command("Runtime.enable")) - - await javascript_ready - self._current_js_id = ( - javascript_ready.result() - .get("params", {}) - .get("context", {}) - .get("id", None) - ) - if not self._current_js_id: - raise RuntimeError( - "Refresh sequence didn't work for reload_tab_with_javascript." - "Result {javascript_ready.result()}.", - ) - await page_ready - self._regenerate_javascript_console() - - async def reload(self): - """Reload the tab, and set the javascript runtime id.""" - tab = self.tab - _logger.debug(f"Reloading tab {tab} with javascript.") - javascript_ready = tab.subscribe_once("Runtime.executionContextCreated") - while javascript_ready.done(): - _logger.debug2("Clearing an old Runtime.executionContextCreated") - javascript_ready = tab.subscribe_once("Runtime.executionContextCreated") - is_loaded = tab.subscribe_once("Page.loadEventFired") - while is_loaded.done(): - _logger.debug2("Clearing an old Page.loadEventFired") - is_loaded = tab.subscribe_once("Page.loadEventFired") - _logger.debug2(f"Calling Page.reload on {tab}") - _check_error(await tab.send_command("Page.reload")) - await javascript_ready - self._current_js_id = ( - javascript_ready.result() - .get("params", {}) - .get("context", {}) - .get("id", None) - ) - if not self._current_js_id: - raise RuntimeError( - "Refresh sequence didn't work for reload_tab_with_javascript." - "Result {javascript_ready.result()}.", - ) - await is_loaded - self._regenerate_javascript_console() - - async def console_print(self, message: str) -> None: - """ - Print something to the javascript console. - - Args: - message: The thing to print. - - """ - jsfn = r"function()" r"{" f"console.log('{message}')" r"}" - params = { - "functionDeclaration": jsfn, - "returnByValue": False, - "userGesture": True, - "awaitPromise": True, - "executionContextId": self._current_js_id, - } - - # send request to run script in chromium - _logger.debug("Calling js function") - result = await self.tab.send_command("Runtime.callFunctionOn", params=params) - _logger.debug(f"Sent javascript got result: {result}") - _check_error(result) - - def _finish_profile(self, profile, state, error=None): - _logger.debug("Finishing profile") - profile["duration"] = float(f"{time.perf_counter() - profile['start']:.6f}") - del profile["start"] - profile["state"] = state - if self.javascript_log: - profile["js_console"] = self.javascript_log - if error: - profile["error"] = error - - async def _write_fig( - self, - spec, - full_path, - *, - topojson=None, - error_log=None, - profiler=None, - ): - """Calculate and write figure to file. Wraps _calc_fig, and writes a file.""" - img, profile = await self._calc_fig( - spec, - full_path, - topojson=topojson, - error_log=error_log, - profiler=profiler, - ) - - def write_image(binary): - with full_path.open("wb") as file: - file.write(binary) - - _logger.info(f"Starting write of {full_path.name}") - await to_thread(write_image, img) - _logger.info(f"Wrote {full_path.name}") - - if profile is not None: - profile["megabytes"] = full_path.stat().st_size / 1000000 - profile["state"] = "WROTE" - - async def _calc_fig( # noqa: C901, PLR0912, complexity, branches - self, - spec, - full_path, - *, - topojson=None, - error_log=None, - profiler=None, - ): - """ - Call the plotly renderer via javascript. - - Args: - spec: the processed plotly figure - full_path: the path to write the image too. if its a directory, we will try - to generate a name. If the path contains an extension, - "path/to/my_image.png", that extension will be the format used if not - overridden in `opts`. - opts: dictionary describing format, width, height, and scale of image - topojson: topojsons are used to customize choropleths - error_log: A supplied list, will be populated with `ErrorEntry`s - which can be converted to strings. Note, this is for - collections errors that have to do with plotly. They will - not be thrown. Lower level errors (kaleido, choreographer) - will still be thrown. If not passed, all errors raise. - profiler: a supplied dictionary to collect stats about the operation - - """ - tab = self.tab - execution_context_id = self._current_js_id - if profiler is not None: - profile = { - "name": full_path.name, - "start": time.perf_counter(), - "state": "INIT", - } - else: - profile = None - - _logger.debug(f"In tab {tab.target_id[:4]} calc_fig for {full_path.name}.") - - _logger.info(f"Processing {full_path.name}") - # js script - kaleido_jsfn = ( - r"function(spec, ...args)" - r"{" - r"return kaleido_scopes.plotly(spec, ...args).then(JSON.stringify);" - r"}" - ) - - # params - arguments = [{"value": spec}] - arguments.append({"value": topojson if topojson else None}) - arguments.append({"value": self._stepper}) - params = { - "functionDeclaration": kaleido_jsfn, - "arguments": arguments, - "returnByValue": False, - "userGesture": True, - "awaitPromise": True, - "executionContextId": execution_context_id, - } - - _logger.info(f"Sending big command for {full_path.name}.") - if profile: - profile["state"] = "SENDING" - result = await tab.send_command("Runtime.callFunctionOn", params=params) - if profile: - profile["state"] = "SENT" - _logger.info(f"Sent big command for {full_path.name}.") - e = _check_error_ret(result) - if e: - if profiler is not None: - self._finish_profile(profile, "ERROR", e) - profiler[tab.target_id].append(profile) - if error_log is not None: - error_log.append(ErrorEntry(full_path.name, e, self.javascript_log)) - _logger.error(f"Failed {full_path.name}", exc_info=e) - else: - _logger.error(f"Raising error on {full_path.name}") - raise e - _logger.debug2(f"Result of function call: {result}") - if self._stepper: - print(f"Image {full_path.name} was sent to browser") # noqa: T201 - input("Press Enter to continue...") - if e: - return None, None - - img = await self._img_from_response(result) - if isinstance(img, BaseException): - if profiler is not None: - self._finish_profile(profile, "ERROR", img) - profiler[tab.target_id].append(profile) - if error_log is not None: - error_log.append( - ErrorEntry(full_path.name, img, self.javascript_log), - ) - _logger.info(f"Failed {full_path.name}") - return None, None - else: - raise img - if profile: - self._finish_profile(profile, "CALCULATED", None) - profiler[tab.target_id].append(profile) - return img, profile - - async def _img_from_response(self, response): - js_response = json.loads(response.get("result").get("result").get("value")) - - if js_response["code"] != 0: - return KaleidoError(js_response["code"], js_response["message"]) - - response_format = js_response.get("format") - img = js_response.get("result") - if response_format == "pdf": - pdf_params = { - "printBackground": True, - "marginTop": 0.1, - "marginBottom": 0.1, - "marginLeft": 0.1, - "marginRight": 0.1, - "preferCSSPageSize": True, - "pageRanges": "1", - } - pdf_response = await self.tab.send_command( - "Page.printToPDF", - params=pdf_params, - ) - e = _check_error_ret(pdf_response) - if e: - return e - img = pdf_response.get("result").get("data") - # Base64 decode binary types - if response_format not in _TEXT_FORMATS: - img = base64.b64decode(img) - else: - img = str.encode(img) - return img diff --git a/src/py/kaleido/_kaleido_tab/__init__.py b/src/py/kaleido/_kaleido_tab/__init__.py new file mode 100644 index 00000000..1828d1be --- /dev/null +++ b/src/py/kaleido/_kaleido_tab/__init__.py @@ -0,0 +1,8 @@ +from ._errors import JavascriptError, KaleidoError +from ._tab import _KaleidoTab + +__all__ = [ + "JavascriptError", + "KaleidoError", + "_KaleidoTab", +] diff --git a/src/py/kaleido/_kaleido_tab/_devtools_utils.py b/src/py/kaleido/_kaleido_tab/_devtools_utils.py new file mode 100644 index 00000000..813c98f3 --- /dev/null +++ b/src/py/kaleido/_kaleido_tab/_devtools_utils.py @@ -0,0 +1,125 @@ +"""Contains both DevTools protocol and kaleido_scopes.js helper fns.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +import logistro + +from ._errors import KaleidoError, _raise_error + +if TYPE_CHECKING: + from typing import Any + + import choreographer + +_logger = logistro.getLogger(__name__) + + +def get_js_id(result) -> int: + """Grab javascript engine ID from a chrome executionContextStarted event.""" + if _id := result.get("params", {}).get("context", {}).get("id", None): + return _id + raise RuntimeError( + "Refresh sequence didn't work for reload_tab_with_javascript." + "Result {javascript_ready.result()}.", + ) + + +async def console_print(tab: choreographer.Tab, js_id: int, message: str) -> None: + """ + Print something to the javascript console. + + Note: tab and js_id need to specified separately + + Args: + tab: the choreographer tab to print on + js_id: the javascript engine id to print on + message: The thing to print. + + """ + fn = r"function()" r"{" f"console.log('{message}')" r"}" + params = { + "functionDeclaration": fn, + "returnByValue": False, + "userGesture": True, + "awaitPromise": True, + "executionContextId": js_id, + } + + # send request to run script in chromium + _logger.debug("Calling js function") + result = await tab.send_command("Runtime.callFunctionOn", params=params) + _logger.debug(f"Sent javascript got result: {result}") + _raise_error(result) + + +async def exec_js_fn( + tab: choreographer.Tab, + js_id: int, + fn: str, + *args: Any, +): + args_structured = [{"value": arg} for arg in args] + params = { + "functionDeclaration": fn, + "arguments": args_structured, + "returnByValue": False, + "userGesture": True, + "awaitPromise": True, + "executionContextId": js_id, + } + return await tab.send_command("Runtime.callFunctionOn", params=params) + + +def check_kaleido_js_response( + response, +) -> dict: + _raise_error(response) + js_response = json.loads( + response.get( + "result", + {}, + ) + .get( + "result", + {}, + ) + .get( + "value", + ), + ) + if not js_response: + raise RuntimeError( + f"Javascript response not understood: {response}", + ) + # NOTE: Why above "JavascriptError" + # This is an error in extracting a response when we expected javascript. + # If its an actual javascript error, the response will be coherent, and + # _raise_error above will find it. + + if js_response["code"] != 0: + raise KaleidoError(js_response["code"], js_response["message"]) + + return js_response + + +async def print_pdf( + tab: choreographer.Tab, +) -> str: + pdf_params = { + "printBackground": True, + "marginTop": 0.1, + "marginBottom": 0.1, + "marginLeft": 0.1, + "marginRight": 0.1, + "preferCSSPageSize": True, + "pageRanges": "1", + } + pdf_response = await tab.send_command( + "Page.printToPDF", + params=pdf_params, + ) + _raise_error(pdf_response) + return pdf_response.get("result", {}).get("data") # Check for None? diff --git a/src/py/kaleido/_kaleido_tab/_errors.py b/src/py/kaleido/_kaleido_tab/_errors.py new file mode 100644 index 00000000..f9a088b1 --- /dev/null +++ b/src/py/kaleido/_kaleido_tab/_errors.py @@ -0,0 +1,46 @@ +from choreographer.errors import DevtoolsProtocolError + + +class JavascriptError(RuntimeError): + """Used to report errors from javascript.""" + + +### Error definitions ### +class KaleidoError(Exception): + """ + An error to interpret errors from Kaleido's JS side. + + This is not for all js errors, just kaleido_scopes.js errors. + """ + + def __init__(self, code, message): + """ + Construct an error object. + + Args: + code: the number code of the error. + message: the message of the error. + + """ + super().__init__(message) + self._code = code + self._message = message + + def __str__(self): + """Display the KaleidoError nicely.""" + return f"Error {self._code}: {self._message}" + + +def _get_error(result): + """Check browser response for errors. Helper function.""" + if "error" in result: + return DevtoolsProtocolError(result) + if result.get("result", {}).get("result", {}).get("subtype", None) == "error": + return JavascriptError(str(result.get("result"))) + return None + + +def _raise_error(result): + e = _get_error(result) + if e: + raise e diff --git a/src/py/kaleido/_kaleido_tab/_js_logger.py b/src/py/kaleido/_kaleido_tab/_js_logger.py new file mode 100644 index 00000000..bad0516e --- /dev/null +++ b/src/py/kaleido/_kaleido_tab/_js_logger.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import logistro + +_logger = logistro.getLogger(__name__) + +if TYPE_CHECKING: + from typing import Any + + import choreographer + + +def _make_console_logger(name, log): + """Create printer specifically for console events. Helper function.""" + + async def console_printer(event): + _logger.debug2(f"{name}:{event}") + log.append(str(event)) + + return console_printer + + +class JavascriptLogger: + log: list[Any] + """A list of console outputs from the tab.""" + + def __init__(self, tab: choreographer.Tab) -> None: + self.log = [] + self.tab = tab + + def activate(self): + self.tab.unsubscribe("Runtime.consoleAPICalled") + self.tab.subscribe( + "Runtime.consoleAPICalled", + _make_console_logger("tab js console", self.log), + ) + + def reset(self): + self.tab.unsubscribe("Runtime.consoleAPICalled") + self.log = [] + self.tab.subscribe( + "Runtime.consoleAPICalled", + _make_console_logger("tab js console", self.log), + ) diff --git a/src/py/kaleido/_kaleido_tab/_tab.py b/src/py/kaleido/_kaleido_tab/_tab.py new file mode 100644 index 00000000..2c1da8df --- /dev/null +++ b/src/py/kaleido/_kaleido_tab/_tab.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import base64 +from typing import TYPE_CHECKING + +import logistro + +from . import _devtools_utils as _dtools +from . import _js_logger +from ._errors import _raise_error + +if TYPE_CHECKING: + import asyncio + from pathlib import Path + + import choreographer as choreo + + from kaleido._utils import fig_tools + + +_TEXT_FORMATS = ("svg", "json") # eps + +_logger = logistro.getLogger(__name__) + + +def _subscribe_new(tab: choreo.Tab, event: str) -> asyncio.Future: + """Create subscription to tab clearing old ones first: helper function.""" + new_future = tab.subscribe_once(event) + while new_future.done(): + _logger.debug2(f"Clearing an old {event}") + new_future = tab.subscribe_once(event) + return new_future + + +class _KaleidoTab: + """ + A Kaleido tab is a wrapped choreographer tab providing the functions we need. + + The choreographer tab can be accessed through the `self.tab` attribute. + """ + + tab: choreo.Tab + """The underlying choreographer tab.""" + js_logger: _js_logger.JavascriptLogger + """A log for recording javascript.""" + + def __init__(self, tab): + """ + Create a new _KaleidoTab. + + Args: + tab: the choreographer tab to wrap. + + """ + self.tab = tab + self.js_logger = _js_logger.JavascriptLogger(self.tab) + + async def navigate(self, url: str | Path = ""): + """ + Navigate to the kaleidofier script. This is effectively the real initialization. + + Args: + url: Override the location of the kaleidofier script if necessary. + + """ + # Subscribe to event which will contain javascript engine ID (need it + # for calling javascript functions) + javascript_ready = _subscribe_new(self.tab, "Runtime.executionContextCreated") + + # Subscribe to event indicating page ready. + page_ready = _subscribe_new(self.tab, "Page.loadEventFired") + + # Navigating page. This will trigger the above events. + _logger.debug2(f"Calling Page.navigate on {self.tab}") + _raise_error(await self.tab.send_command("Page.navigate", params={"url": url})) + + # Enabling page events (for page_ready- like all events, if already + # ready, the latest will fire immediately) + _logger.debug2(f"Calling Page.enable on {self.tab}") + _raise_error(await self.tab.send_command("Page.enable")) + + # Enabling javascript events (for javascript_ready) + _logger.debug2(f"Calling Runtime.enable on {self.tab}") + _raise_error(await self.tab.send_command("Runtime.enable")) + + self._current_js_id = _dtools.get_js_id(await javascript_ready) + + await page_ready # don't care result, ready is ready + + # this runs *after* page load because running it first thing + # requires a couple extra lines + self.js_logger.reset() + + # reload is truly so close to navigate + async def reload(self): + """Reload the tab, and set the javascript runtime id.""" + _logger.debug(f"Reloading tab {self.tab} with javascript.") + + javascript_ready = _subscribe_new(self.tab, "Runtime.executionContextCreated") + + page_ready = _subscribe_new(self.tab, "Page.loadEventFired") + + _logger.debug2(f"Calling Page.reload on {self.tab}") + _raise_error(await self.tab.send_command("Page.reload")) + + self._current_js_id = _dtools.get_js_id(await javascript_ready) + + await page_ready + + self.js_logger.reset() + + async def _calc_fig( + self, + spec: fig_tools.Spec, + *, + topojson: str | None, + render_prof, + stepper, + ) -> bytes: + # js script + kaleido_js_fn = ( + r"function(spec, ...args)" + r"{" + r"return kaleido_scopes.plotly(spec, ...args).then(JSON.stringify);" + r"}" + ) + render_prof.profile_log.tick("sending javascript") + result = await _dtools.exec_js_fn( + self.tab, + self._current_js_id, + kaleido_js_fn, + spec, + topojson, + stepper, + ) + _raise_error(result) + render_prof.profile_log.tick("javascript sent") + + _logger.debug2(f"Result of function call: {result}") + js_response = _dtools.check_kaleido_js_response(result) + + if (response_format := js_response.get("format")) == "pdf": + render_prof.profile_log.tick("printing pdf") + img_raw = await _dtools.print_pdf(self.tab) + render_prof.profile_log.tick("pdf printed") + else: + img_raw = js_response["result"] + + if response_format not in _TEXT_FORMATS: + res = base64.b64decode(img_raw) + else: + res = str.encode(img_raw) + + render_prof.data_out_size = len(res) + render_prof.js_log = self.js_logger.log + return res diff --git a/src/py/kaleido/_mocker.py b/src/py/kaleido/_mocker.py deleted file mode 100644 index 6c9fe419..00000000 --- a/src/py/kaleido/_mocker.py +++ /dev/null @@ -1,301 +0,0 @@ -from __future__ import annotations - -import argparse -import asyncio -import multiprocessing -import sys -import time -import warnings -from pathlib import Path -from pprint import pp -from random import sample -from typing import TypedDict - -import logistro -import orjson - -import kaleido - -_logger = logistro.getLogger(__name__) - -cpus = multiprocessing.cpu_count() - -# Extract jsons of mocks -test_dir = Path(__file__).resolve().parent.parent / "integration_tests" -in_dir = test_dir / "mocks" -out_dir = test_dir / "renders" - - -def _get_jsons_in_paths(path: str | Path) -> list[Path]: - # Work with Paths and directories - path = Path(path) if isinstance(path, str) else path - - if path.is_dir(): - _logger.info(f"Input is path {path}") - return list(path.glob("*.json")) - elif path.is_file(): - _logger.info(f"Input is file {path}") - return [path] - else: - raise TypeError("--input must be file or directory") - - -class Param(TypedDict): - name: str - opts: dict[str, int | float] - - -def _load_figures_from_paths(paths: list[Path]): - # Set json - params: list[Param] - for path in paths: - if path.is_file(): - with path.open(encoding="utf-8") as file: - figure = orjson.loads(file.read()) - _logger.info(f"Yielding {path.stem}") - if args.parameterize_opts is False: - params = [ - { - "name": f"{path.stem}.{args.format or 'png'}", - "opts": { - "scale": args.scale, - "width": args.width, - "height": args.height, - }, - }, - ] - else: - widths = [args.width] if args.width else [200, 700, 1000] - heights = [args.height] if args.height else [200, 500, 1000] - scales = [args.scale] if args.scale else [0.5, 1, 2] - formats = ( - [args.format] - if args.format - else [ - "png", - "pdf", - "jpg", - "webp", - "svg", - "json", - ] - ) - params = [] - for w in widths: - for h in heights: - for s in scales: - for f in formats: - params.append( - { - "name": ( - f"{path.stem!s}-{w!s}" - f"x{h!s}X{s!s}.{f!s}" - ), - "opts": { - "scale": s, - "width": w, - "height": h, - }, - }, - ) - for p in params: - yield { - "fig": figure, - "path": str(Path(args.output) / p["name"]), - "opts": p["opts"], - } - else: - raise RuntimeError(f"Path {path} is not a file.") - - -# Set the arguments -description = """kaleido_mocker will load up json files of plotly figs and export them. - -If you set multiple process, -n, non-headless mode won't function well because -chrome will actually throttle tabs or windows/visibile- unless that tab/window -is headless. - -The export of the program is a json object containing information about the execution. -""" - -if "--headless" in sys.argv and "--no-headless" in sys.argv: - raise ValueError( - "Choose either '--headless' or '--no-headless'.", - ) - -parser = argparse.ArgumentParser( - add_help=True, - parents=[logistro.parser], - conflict_handler="resolve", - description=description, -) -parser.add_argument( - "--logistro-level", - default="INFO", - dest="log", - help="Set the logging level (default INFO)", -) -parser.add_argument( - "--n", - type=int, - default=cpus, - help="Number of tabs, defaults to # of cpus", -) -parser.add_argument( - "--input", - type=str, - default=in_dir, - help="Directory of mock file/s or single file (default tests/mocks)", -) -parser.add_argument( - "--output", - type=str, - default=out_dir, - help="DIRECTORY of mock file/s (default tests/renders)", -) -parser.add_argument( - "--format", - type=str, - default=None, - help="png (default), pdf, jpg, webp, svg, json", -) -parser.add_argument( - "--width", - type=str, - default=None, - help="width in pixels (default 700)", -) -parser.add_argument( - "--height", - type=str, - default=None, - help="height in pixels (default 500)", -) -parser.add_argument( - "--scale", - type=str, - default=None, - help="Scale ratio, acts as multiplier for height/width (default 1)", -) -parser.add_argument( - "--parameterize_opts", - action="store_true", - default=False, - help="Run mocks w/ different configurations.", -) -parser.add_argument( - "--timeout", - type=int, - default=90, - help="Set timeout in seconds for any 1 mock (default 60 seconds)", -) -parser.add_argument( - "--headless", - action="store_true", - default=True, - help="Set headless as True (default)", -) -parser.add_argument( - "--no-headless", - action="store_false", - dest="headless", - help="Set headless as False", -) -parser.add_argument( - "--stepper", - action="store_true", - default=False, - dest="stepper", - help="Stepper sets n to 1, headless to False, no timeout " - "and asks for confirmation before printing.", -) -parser.add_argument( - "--random", - type=int, - default=0, - help="Will select N random jsons- or if 0 (default), all.", -) -parser.add_argument( - "--fail-fast", - action="store_true", - default=False, - help="Throw first error encountered and stop execution.", -) - -args = parser.parse_args() -logistro.getLogger().setLevel(args.log) - -if not Path(args.output).is_dir(): - raise ValueError(f"Specified output must be existing directory. Is {args.output!s}") - - -# Function to process the images -async def _main(error_log=None, profiler=None): - paths = _get_jsons_in_paths(args.input) - if args.random: - if args.random > len(paths): - raise ValueError( - f"Input discover {len(paths)} paths, but a sampling of" - f"{args.random} was asked for.", - ) - paths = sample(paths, args.random) - if args.stepper: - _logger.info("Setting stepper.") - args.n = 1 - args.headless = False - args.timeout = 0 - if args.format == "svg": - warnings.warn( - "Stepper won't render svgs. It's feasible, " - "but the adaption is just slightly more involved.", - stacklevel=1, - ) - await asyncio.sleep(3) - # sets a global in kaleido, gross huh - - async with kaleido.Kaleido( - page_generator=kaleido.PageGenerator(force_cdn=True), - n=args.n, - headless=args.headless, - timeout=args.timeout, - stepper=args.stepper, - ) as k: - await k.write_fig_from_object( - _load_figures_from_paths(paths), - error_log=error_log, - profiler=profiler, - ) - - -def build_mocks(): - start = time.perf_counter() - try: - error_log = [] if not args.fail_fast else None - profiler = {} - asyncio.run(_main(error_log, profiler)) - finally: - # ruff: noqa: PLC0415 - from operator import itemgetter - - for tab, tab_profile in profiler.items(): - profiler[tab] = sorted( - tab_profile, - key=itemgetter("duration"), - reverse=True, - ) - - elapsed = time.perf_counter() - start - with_error_log = error_log is not None - results = { - "error_log": [str(log) for log in error_log] if with_error_log else None, - "profiles": profiler, - "total_time": f"Time taken: {elapsed:.6f} seconds", - "total_errors": len(error_log) if with_error_log else "untracked", - } - pp(results) - if error_log: - sys.exit(1) - - -if __name__ == "__main__": - build_mocks() diff --git a/src/py/kaleido/_page_generator.py b/src/py/kaleido/_page_generator.py index 34b4064e..f99ad5a4 100644 --- a/src/py/kaleido/_page_generator.py +++ b/src/py/kaleido/_page_generator.py @@ -1,11 +1,20 @@ from __future__ import annotations from pathlib import Path -from urllib.parse import urlparse -from urllib.request import url2pathname +from typing import TYPE_CHECKING import logistro +from ._utils import path_tools + +if TYPE_CHECKING: + from typing import Tuple, Union + + from typing_extensions import TypeAlias + + UrlAndCharset: TypeAlias = Tuple[Union[str, Path], str] + """A tuple to explicitly set charset= in the ' script_tag_charset = '\n ' @@ -146,8 +147,4 @@ def generate_index(self, path=None): page += script_tag_charset % script page += self.footer _logger.debug2(page) - if not path: - return page - with (path).open("w") as f: - f.write(page) - return path.as_uri() + return page diff --git a/src/py/kaleido/_profiler.py b/src/py/kaleido/_profiler.py new file mode 100644 index 00000000..6ae29231 --- /dev/null +++ b/src/py/kaleido/_profiler.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + from typing import Any + + from ._utils import fig_tools + + Event = str + + +class WriteCall: + name: str + renders: list[RenderTaskProfile] + + __slots__ = tuple(__annotations__) + + def __init__(self, name: str): + self.name = name + self.renders = [] + + +class RenderTaskProfile: + info: dict[str, Any] # literal? + error: None | BaseException + js_log: list[str] + profile_log: ProfileLog + data_in_size: int | None + data_out_size: int | None + + __slots__ = tuple(__annotations__) + + def __init__( + self, + spec: fig_tools.Spec, + full_path: Path | None, + tab_id: str, + ) -> None: + self.info = {} + self.error = None + self.js_log = [] + self.profile_log = ProfileLog() + self.data_in_size = None # need to get this from choreographer + self.data_out_size = None + + self.info.update( + {k: v for k, v in spec.items() if k != "data"}, + ) + self.info["path"] = full_path + self.info["tab"] = tab_id + + +class ProfileLog: + _logs: dict[Event, float] + + __slots__ = tuple(__annotations__) + + def __init__(self) -> None: + self._logs = {} + + def tick(self, name: str) -> None: + self._logs[name] = time.perf_counter() + + def get_logs(self) -> dict[Event, float]: + return self._logs diff --git a/src/py/kaleido/_sync_server.py b/src/py/kaleido/_sync_server.py index 714fcce2..740bd902 100644 --- a/src/py/kaleido/_sync_server.py +++ b/src/py/kaleido/_sync_server.py @@ -11,7 +11,7 @@ from .kaleido import Kaleido if TYPE_CHECKING: - from typing import Any + from typing import Any, Callable class Task(NamedTuple): @@ -95,7 +95,20 @@ def close(self, *, silence_warnings=False): del self._return_queue self._initialized = False - def call_function(self, cmd: str, *args, **kwargs): + def call_function(self, cmd: str, *args: Any, **kwargs: Any): + """ + Call any function on the singleton Kaleido object. + + Preferred functions would be: `calc_fig`, `write_fig`, and + `write_fig_from_object`. Methods that doesn't exist will raise a + BaseException. + + Args: + cmd (str): the name of the method to call + args (Any): the method's arguments + kwargs (Any): the method's keyword arguments + + """ if not self.is_running(): raise RuntimeError("Can't call function on stopped server.") if kwargs.pop("kopts", None): @@ -113,7 +126,24 @@ def call_function(self, cmd: str, *args, **kwargs): return res -def oneshot_async_run(func, args: tuple[Any, ...], kwargs: dict): +def oneshot_async_run( + func: Callable, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + """ + Run a thread to execute a single function. + + Used by _sync functions in + `__init__` to ensure their async loop is separate from the users main + one. + + Args: + func: the function to run + args: a tuple of arguments to pass + kwargs: a dictionary of keyword arguments to pass + + """ q: Queue[Any] = Queue(maxsize=1) def run(func, q, *args, **kwargs): diff --git a/src/py/kaleido/_utils.py b/src/py/kaleido/_utils/__init__.py similarity index 54% rename from src/py/kaleido/_utils.py rename to src/py/kaleido/_utils/__init__.py index 052f79b9..c03c45c6 100644 --- a/src/py/kaleido/_utils.py +++ b/src/py/kaleido/_utils/__init__.py @@ -1,16 +1,66 @@ +from __future__ import annotations + import asyncio -import traceback import warnings from functools import partial from importlib.metadata import PackageNotFoundError, version +from typing import TYPE_CHECKING import logistro from packaging.version import Version _logger = logistro.getLogger(__name__) +if TYPE_CHECKING: + from typing import Any, Callable, Coroutine + + +def event_printer(name: str) -> Callable[[Any], Coroutine[Any, Any, None]]: + """Return function that prints whatever argument received.""" + + async def print_all(response: Any) -> None: + _logger.debug2(f"{name!s}:{response!s}") + + return print_all + + +def _clean_error(t: asyncio.Task) -> None: + """Check a task to avoid "task never awaited" errors.""" + if t.cancelled(): + _logger.error(f"{t} cancelled.") + elif (exc := t.exception()) is not None: + _logger.error(f"{t} raised error.", exc_info=exc) + + +def create_task_log_error(coroutine) -> asyncio.Task: + """Create a task and assign a callback to log its errors.""" + t = asyncio.create_task(coroutine) + t.add_done_callback(_clean_error) + return t + + +def ensure_async_iter(obj): + """Convert any iterable to an async iterator.""" + if hasattr(obj, "__aiter__"): + return obj + + it = iter(obj) + + class _AIter: + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(it) + except StopIteration: + raise StopAsyncIteration # noqa: B904 + + return _AIter() + async def to_thread(func, *args, **kwargs): + """Polyfill `asyncio.to_thread()`.""" _loop = asyncio.get_running_loop() fn = partial(func, *args, **kwargs) await _loop.run_in_executor(None, fn) @@ -35,7 +85,10 @@ def warn_incompatible_plotly(): "This means that static image generation (e.g. `fig.write_image()`) " "will not work.\n\n" f"Please upgrade Plotly to version {min_compatible_plotly_version} " - "or greater, or downgrade Kaleido to version 0.2.1." + "or greater, or downgrade Kaleido to version 0.2.1.\n\n" + "You can however, use the Kaleido API directly which will work " + "with your plotly version. `kaleido.write_fig(...)`, for example. " + "Please see the kaleido documentation." "\n", UserWarning, stacklevel=3, @@ -49,30 +102,3 @@ def warn_incompatible_plotly(): # Since this compatibility check is just a convenience, # we don't want to block the whole library if there's an issue _logger.info("Error while checking Plotly version.", exc_info=e) - - -class ErrorEntry: - """A simple object to record errors and context.""" - - def __init__(self, name, error, javascript_log): - """ - Construct an error entry. - - Args: - name: the name of the image with the error - error: the error object (from class BaseException) - javascript_log: an array of entries from the javascript console - - """ - self.name = name - self.error = error - self.javascript_log = javascript_log - - def __str__(self): - """Display the error object in a concise way.""" - ret = f"{self.name}:\n" - e = self.error - ret += " ".join(traceback.format_exception(type(e), e, e.__traceback__)) - ret += " javascript Log:\n" - ret += "\n ".join(self.javascript_log) - return ret diff --git a/src/py/kaleido/_utils/fig_tools.py b/src/py/kaleido/_utils/fig_tools.py new file mode 100644 index 00000000..71541097 --- /dev/null +++ b/src/py/kaleido/_utils/fig_tools.py @@ -0,0 +1,141 @@ +""" +Tools to help prepare data for plotly.js from kaleido. + +It 1. validates, 2. write defaults, 3. packages object. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, TypedDict + +import logistro + +from . import path_tools + +if TYPE_CHECKING: + from pathlib import Path + from typing import Any + + from typing_extensions import TypeGuard + + Figurish = Any # Be nice to make it more specific, dictionary or something + FormatString = Literal["png", "jpg", "jpeg", "webp", "svg", "json", "pdf"] + + +# Input of to_spec (user gives us this) +class LayoutOpts(TypedDict, total=False): + format: FormatString | None + scale: int | float + height: int | float + width: int | float + + +# Output of to_spec (we give kaleido_scopes.js this) +# refactor note: this could easily be right before send +class Spec(TypedDict): + format: FormatString + width: int | float + height: int | float + scale: int | float + data: Figurish + + +_logger = logistro.getLogger(__name__) + +# constants +DEFAULT_EXT = "png" +DEFAULT_SCALE = 1 +DEFAULT_WIDTH = 700 +DEFAULT_HEIGHT = 500 +SUPPORTED_FORMATS: tuple[FormatString, ...] = ( + "png", + "jpg", + "jpeg", + "webp", + "svg", + "json", + "pdf", +) + + +# validation function +def is_figurish(o: Any) -> TypeGuard[Figurish]: + # so if data isn't in the dict things get weird. + valid = hasattr(o, "to_dict") or (isinstance(o, dict) and "data" in o) + if not valid: + _logger.debug( + f"Figure has to_dict? {hasattr(o, 'to_dict')} " + f"is dict? {isinstance(o, dict)} " + f"Keys: {o.keys() if hasattr(o, 'keys') else None!s}", + ) + return valid + + +def _coerce_format(extension: str) -> FormatString: + # wrap this condition as a typeguard for typechecker's sake + def is_fmt(s: str) -> TypeGuard[FormatString]: + return s in SUPPORTED_FORMATS + + formatted_extension = extension.lower() + if formatted_extension == "jpg": + return "jpeg" + elif not is_fmt(formatted_extension): + raise ValueError( + f"Invalid format '{formatted_extension}'.\n" + f"Supported formats: {SUPPORTED_FORMATS!s}", + ) + else: + return formatted_extension + + +def coerce_for_js( + fig: Figurish, + path: Path | str | None, + opts: LayoutOpts | None, +) -> Spec: + if not is_figurish(fig): # VALIDATE FIG + raise TypeError("Figure supplied doesn't seem to be a valid plotly figure.") + if hasattr(fig, "to_dict"): # COERCE FIG + fig = fig.to_dict() + + path = path_tools.get_path(path) if path else None + + opts = opts or {} + + if _rest := opts - LayoutOpts.__annotations__.keys(): + raise AttributeError(f"Unknown key(s) in layout options: {_rest}") + + # Extract info + file_format = _coerce_format( + opts.get("format") + or (path.suffix.lstrip(".") if path and path.suffix else DEFAULT_EXT), + ) + + layout = fig.get("layout", {}) + + width = ( + opts.get("width") + or layout.get("width") + or layout.get("template", {}).get("layout", {}).get("width") + or DEFAULT_WIDTH + ) + + height = ( + opts.get("height") + or layout.get("height") + or layout.get("template", {}).get("layout", {}).get("height") + or DEFAULT_HEIGHT + ) + + scale = opts.get("scale", DEFAULT_SCALE) + + # PACKAGING + spec: Spec = { + "format": file_format, + "width": width, + "height": height, + "scale": scale, + "data": fig, + } + + return spec diff --git a/src/py/kaleido/_utils/path_tools.py b/src/py/kaleido/_utils/path_tools.py new file mode 100644 index 00000000..fae5cf19 --- /dev/null +++ b/src/py/kaleido/_utils/path_tools.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import glob +import re +from pathlib import Path +from typing import TYPE_CHECKING +from urllib.parse import urlparse +from urllib.request import url2pathname + +import logistro + +if TYPE_CHECKING: + from . import fig_tools + +_logger = logistro.getLogger(__name__) + + +def _next_filename(path: Path | str, prefix: str, ext: str) -> str: + path = path if isinstance(path, Path) else Path(path) + default = 1 if (path / f"{prefix}.{ext}").exists() else 0 + re_number = re.compile( + r"^" + re.escape(prefix) + r"\-(\d+)\." + re.escape(ext) + r"$", + ) + escaped_prefix = glob.escape(prefix) + escaped_ext = glob.escape(ext) + numbers = [ + int(match.group(1)) + for name in path.glob(f"{escaped_prefix}-*.{escaped_ext}") + if (match := re_number.match(Path(name).name)) + ] + n = max(numbers, default=default) + 1 + return f"{prefix}.{ext}" if n == 1 else f"{prefix}-{n}.{ext}" + + +def determine_path( + path: Path | str | None, + fig: dict, + ext: fig_tools.FormatString, +) -> Path: + path = Path(path) if path else Path() + + if not path.suffix or path.is_dir(): # they gave us a directory + if not path.is_dir(): + raise ValueError(f"Directory {path} not found. Please create it.") + directory = path + _logger.debug("Looking for title") + prefix = fig.get("layout", {}).get("title", {}).get("text", "fig") + prefix = re.sub(r"[ \-]", "_", prefix) + prefix = re.sub(r"[^a-zA-Z0-9_]", "", prefix) + prefix = prefix or "fig" + _logger.debug(f"Found: {prefix}") + name = _next_filename(directory, prefix, ext) + full_path = directory / name + else: # we have full path, supposedly + full_path = path + if not full_path.parent.is_dir(): + raise RuntimeError( + f"Cannot reach path {path.parent}. Are all directories created?", + ) + return full_path + + +def get_path(p: str | Path) -> Path: + if isinstance(p, Path): + return p + elif not isinstance(p, str): + raise TypeError("Path should be a string or `pathlib.Path` object.") + + parsed = urlparse(str(p)) + + return Path( + url2pathname(parsed.path) if parsed.scheme.startswith("file") else p, + ) + + +def is_httpish(p: str) -> bool: + return urlparse(str(p)).scheme.startswith("http") diff --git a/src/py/kaleido/kaleido.py b/src/py/kaleido/kaleido.py index 6ee843a0..43c75f2a 100644 --- a/src/py/kaleido/kaleido.py +++ b/src/py/kaleido/kaleido.py @@ -3,33 +3,64 @@ from __future__ import annotations import asyncio -import warnings +from collections import deque from collections.abc import AsyncIterable, Iterable -from functools import partial from pathlib import Path -from typing import TYPE_CHECKING -from urllib.parse import unquote, urlparse +from typing import TYPE_CHECKING, TypedDict, cast, overload import choreographer as choreo import logistro from choreographer.errors import ChromeNotFoundError from choreographer.utils import TmpDirectory -from ._fig_tools import _is_figurish, build_fig_spec +from . import _profiler, _utils from ._kaleido_tab import _KaleidoTab from ._page_generator import PageGenerator -from ._utils import ErrorEntry, warn_incompatible_plotly +from ._utils import fig_tools, path_tools if TYPE_CHECKING: from types import TracebackType - from typing import Any, Callable, Coroutine + from typing import ( + Any, + AsyncGenerator, + List, + Literal, + Tuple, + TypeVar, + Union, + ValuesView, + ) + + from typing_extensions import NotRequired, Required, TypeAlias, TypeGuard + + T = TypeVar("T") + AnyIterable: TypeAlias = Union[Iterable[T], AsyncIterable[T]] # not runtime + + # union of sized iterables since 3.8 doesn't have & operator + # Iterable & Sized + Listish: TypeAlias = Union[Tuple[T], List[T], ValuesView[T]] + + class FigureDict(TypedDict): + """The type a fig_dicts returns for `write_fig_from_object`.""" + + fig: Required[fig_tools.Figurish] + path: NotRequired[None | str | Path] + opts: NotRequired[fig_tools.LayoutOpts | None] + topojson: NotRequired[None | str] + + +def _is_figuredict(obj: Any) -> TypeGuard[FigureDict]: + return isinstance(obj, dict) and "fig" in obj - from . import _fig_tools _logger = logistro.getLogger(__name__) +# Show a warning if the installed Plotly version +# is incompatible with this version of Kaleido +_utils.warn_incompatible_plotly() + try: - from plotly.utils import PlotlyJSONEncoder # noqa: I001 + from plotly.utils import PlotlyJSONEncoder # type: ignore[import-untyped] # noqa: I001 from choreographer import channels channels.register_custom_encoder(PlotlyJSONEncoder) @@ -37,239 +68,228 @@ except ImportError as e: _logger.debug(f'Couldn\'t import plotly due to "{e!s}" - skipping.') -# Show a warning if the installed Plotly version -# is incompatible with this version of Kaleido -warn_incompatible_plotly() +class Kaleido(choreo.Browser): + """ + The Kaleido object provides a browser to render and write plotly figures. -def _make_printer(name: str) -> Callable[[Any], Coroutine[Any, Any, None]]: - """Create event printer for generic events. Helper function.""" - - async def print_all(response: Any) -> None: - _logger.debug2(f"{name}:{response}") + It provides methods to render said figures, and manages any number of tabs + in a work queue. Start it one of a few equal ways: - return print_all + async with Kaleido() as k: + ... + # or -class Kaleido(choreo.Browser): - """ - Kaleido manages a set of image processors. + k = await Kaleido() + ... + await k.close() - It can be used as a context (`async with Kaleido(...)`), but can - also be used like: + # or - ``` - k = Kaleido(...) - k = await Kaleido.open() - ... # do stuff - k.close() - ``` + k = Kaleido() + await k.open() + ... + await.k.close() """ tabs_ready: asyncio.Queue[_KaleidoTab] - """A queue of ready tabs.""" - _background_render_tasks: set[asyncio.Task] - # not really render tasks - _main_tasks: set[asyncio.Task] + """A queue of tabs ready to process a kaleido figure.""" + _main_render_coroutines: set[asyncio.Task] + # technically Tasks, user sees coroutines + profiler: deque[_profiler.WriteCall] - async def close(self) -> None: - """Close the browser.""" - await super().close() - if self._tmp_dir: - self._tmp_dir.clean() - _logger.info("Cancelling tasks.") - for task in self._main_tasks: - if not task.done(): - task.cancel() - for task in self._background_render_tasks: - if not task.done(): - task.cancel() - _logger.info("Exiting Kaleido/Choreo") + _total_tabs: int + _html_tmp_dir: None | TmpDirectory - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Close the browser.""" - _logger.info("Waiting for all cleanups to finish.") - await asyncio.gather(*self._background_render_tasks, return_exceptions=True) - _logger.info("Exiting Kaleido") - return await super().__aexit__(exc_type, exc_value, exc_tb) + ### KALEIDO LIFECYCLE FUNCTIONS ### - def __init__( # noqa: D417, PLR0913 no args/kwargs in description + def __init__( self, - *args: Any, - page_generator: None | PageGenerator | str | Path = None, + # *args: Any, force named vars for all choreographer passthrough n: int = 1, timeout: int | None = 90, - width: int | None = None, # deprecate - height: int | None = None, # deprecate - stepper: bool = False, - plotlyjs: str | None = None, - mathjax: str | None = None, + page_generator: None | PageGenerator | str | Path = None, + plotlyjs: str | Path | None = None, + mathjax: str | Path | Literal[False] | None = None, **kwargs: Any, ) -> None: """ - Initialize Kaleido, a `choreo.Browser` wrapper adding kaleido functionality. - - It takes all `choreo.Browser` args, plus some extra. The extra - are listed, see choreographer for more documentation. - - Note: Chrome will throttle background tabs and windows, so non-headless - multi-process configurations don't work well. - - For argument `page`, if it is a string, it must be passed as a fully-qualified - URI, like `file://` or `https://`. - If it is a `Path`, `Path`'s `as_uri()` will be called. - If it is a string or path, its expected to be an HTML file, one will not - be generated. + Create a new Kaleido process for rendering plotly figures. Args: - n: the number of separate processes (windows, not seen) to use. - timeout: limit on any single render (default 90 seconds). - width: width of window (headless only) - height: height of window (headless only) - page: This can be a `kaleido.PageGenerator`, a `pathlib.Path`, or a string. + *args (Any): + Passed through to underlying choreographer.Browser() + n (int, optional): + Number of processors to use (parallelization). Defaults to 1. + + timeout (int | None, optional): + Number of seconds to wait to render any one image. None for no + timeout. Defaults to 90. + + page_generator (None | PageGenerator | str | Path, optional): + A PageGenerator object can be used for deep customization of the + plotly template page. This is for development use. You can also + pass a string or path directly to an index.html, or any object + with a `generate_index()->str function that prints an HTML + ppage. Defaults to None. + + plotlyjs (str | Path | None, optional): + A path or URL to a plotly.js file. Defaults to None- which means + to use the plotly.js included with your version of plotly.py or + if not installed, the latest version available via CDN. + + mathjax (str | Path | Literal[False] | None, optional): + A path or URL to a mathjax.js file. If Dalse, mathjax is + disabled. Defaults to None- which means to use version 2.35 via + CDN. + + **kwargs (Any): + Additional keyword arguments passed through to the underlying + Choreographer.browser constructor. Notable options include + `headless=False` (show window), `enable_sandbox=True` (turn on + sandboxing), and `enable_gpu=True` which will allow use of the + GPU. The defaults for these options are True, False, and False + respectively. """ - self._background_render_tasks = set() - self._main_tasks = set() + # State variables + self._main_render_coroutines = set() self.tabs_ready = asyncio.Queue(maxsize=0) - self._total_tabs = 0 - self._tmp_dir = None + self._total_tabs = 0 # tabs properly registered + self._html_tmp_dir = None + self.profiler: deque[_profiler.WriteCall] = deque(maxlen=5) + + # Kaleido Config + if page_generator and (plotlyjs is not None or mathjax is not None): + raise ValueError( + "page_generator cannot be set with mathjax or plotlyjs", + ) page = page_generator self._timeout = timeout self._n = n - self._height = height # deprecate - self._width = width # deprecate - self._stepper = stepper self._plotlyjs = plotlyjs self._mathjax = mathjax - if not kwargs.get("headless", True) and (self._height or self._width): - warnings.warn( - "Height and Width can only be used if headless=True, " - "ignoring both sizes.", - stacklevel=1, - ) - self._height = None - self._width = None + + # Diagnostic _logger.debug(f"Timeout: {self._timeout}") try: - super().__init__(*args, **kwargs) + super().__init__(**kwargs) except ChromeNotFoundError: raise ChromeNotFoundError( "Kaleido v1 and later requires Chrome to be installed. " "To install Chrome, use the CLI command `kaleido_get_chrome`, " - "or from Python, use either `kaleido.get_chrome()` " + "or from Python, use either `await kaleido.get_chrome()` " "or `kaleido.get_chrome_sync()`.", - ) from ChromeNotFoundError + ) from None # overwriting the error entirely. (diagnostics) - # do this during open because it requires close + # save this for open() because it requires close() self._saved_page_arg = page async def open(self): - """Build temporary file if we need one.""" + """Build page and temporary file if we need one, then opens browser.""" page = self._saved_page_arg del self._saved_page_arg - if isinstance(page, str): - if page.startswith(r"file://") and Path(unquote(urlparse(page).path)): - self._index = page - elif Path(page).is_file(): - self._index = Path(page).as_uri() - else: - raise FileNotFoundError(f"{page} does not exist.") - elif isinstance(page, Path): - if page.is_file(): - self._index = page.as_uri() + if isinstance(page, (Path, str)): + if (_p := _utils.get_path(page)).is_file(): + self._index = _p.as_uri() else: raise FileNotFoundError(f"{page!s} does not exist.") - else: - self._tmp_dir = TmpDirectory(sneak=self.is_isolated()) - index = self._tmp_dir.path / "index.html" + elif not page or hasattr(page, "generate_index"): + self._html_tmp_dir = TmpDirectory(sneak=self.is_isolated()) + index = self._html_tmp_dir.path / "index.html" self._index = index.as_uri() if not page: page = PageGenerator(plotly=self._plotlyjs, mathjax=self._mathjax) - page.generate_index(index) + with index.open("w") as f: # is blocking but ok + f.write(page.generate_index()) + else: + raise TypeError( + "page_generator must be one of: None, a" + " PageGenerator, or a file path to an index.html.", + ) await super().open() - async def _conform_tabs(self, tabs: list[choreo.Tab] | None = None) -> None: + async def _create_kaleido_tab(self) -> None: + tab = await super().create_tab( + url="", + window=True, + ) + await self._conform_tabs([tab]) + + async def _conform_tabs(self, tabs: Listish[choreo.Tab] | None = None) -> None: if not tabs: - tabs = list(self.tabs.values()) + tabs = self.tabs.values() _logger.info(f"Conforming {len(tabs)} to {self._index}") - for i, tab in enumerate(tabs): - n = f"tab-{i!s}" _logger.debug2(f"Subscribing * to tab: {tab}.") - tab.subscribe("*", _make_printer(n + " event")) - - _logger.debug("Navigating all tabs") + tab.subscribe("*", _utils.event_printer(f"tab-{i!s}: Event Dump:")) - kaleido_tabs = [_KaleidoTab(tab, _stepper=self._stepper) for tab in tabs] + kaleido_tabs = [_KaleidoTab(tab) for tab in tabs] - # A little hard to read because we don't have TaskGroup in this version - tasks = [asyncio.create_task(tab.navigate(self._index)) for tab in kaleido_tabs] - _logger.info("Waiting on all navigates") - await asyncio.gather(*tasks) - _logger.info("All navigates done, putting them all in queue.") + await asyncio.gather(*(tab.navigate(self._index) for tab in kaleido_tabs)) for ktab in kaleido_tabs: + self._total_tabs += 1 await self.tabs_ready.put(ktab) - self._total_tabs = len(kaleido_tabs) - _logger.debug("Tabs fully navigated/enabled/ready") async def populate_targets(self) -> None: """ Override the browser populate_targets to ensure the correct page. Is called automatically during initialization, and should only be called - once ever per object. + once. """ await super().populate_targets() await self._conform_tabs() needed_tabs = self._n - len(self.tabs) - if needed_tabs < 0: - raise RuntimeError("Did you set 0 or less tabs?") if not needed_tabs: return - tasks = [ - asyncio.create_task(self._create_kaleido_tab()) for _ in range(needed_tabs) - ] - await asyncio.gather(*tasks) - for tab in self.tabs.values(): - _logger.info(f"Tab ready: {tab.target_id}") + await asyncio.gather( + *(self._create_kaleido_tab() for _ in range(needed_tabs)), + ) - async def _create_kaleido_tab( + async def close(self) -> None: + """Close the browser.""" + if self._html_tmp_dir: + _logger.debug(f"Cleaning up {self._html_tmp_dir}") + self._html_tmp_dir.clean() + else: + _logger.debug("No kaleido._html_tmp_dir to clean up.") + + await super().close() + + # cancellation only happens if crash/early + _logger.info("Cancelling tasks.") + for task in self._main_render_coroutines: + if not task.done(): + task.cancel() + + _logger.info("Exiting Kaleido/Choreo.") + + async def __aexit__( self, - ) -> None: - """ - Create a tab with the kaleido script. + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Close the browser.""" + _logger.info("Waiting for all cleanups to finish.") - Returns: - The kaleido-tab created. + # render "tasks" are coroutines, so use is awaiting them - """ - tab = await super().create_tab( - url="", - width=self._width, - height=self._height, - window=True, - ) - await self._conform_tabs([tab]) + await asyncio.gather(*self._main_render_coroutines, return_exceptions=True) - async def _get_kaleido_tab(self) -> _KaleidoTab: - """ - Retrieve an available tab from queue. + _logger.info("Exiting Kaleido.") + return await super().__aexit__(exc_type, exc_value, exc_tb) - Returns: - A kaleido-tab from the queue. + ### TAB MANAGEMENT FUNCTIONS #### - """ + async def _get_kaleido_tab(self) -> _KaleidoTab: _logger.info(f"Getting tab from queue (has {self.tabs_ready.qsize()})") if not self._total_tabs: raise RuntimeError( @@ -280,13 +300,6 @@ async def _get_kaleido_tab(self) -> _KaleidoTab: return tab async def _return_kaleido_tab(self, tab: _KaleidoTab) -> None: - """ - Refresh tab and put it back into the available queue. - - Args: - tab: the kaleido tab to return. - - """ _logger.info(f"Reloading tab {tab.tab.target_id[:4]} before return.") await tab.reload() _logger.info( @@ -296,316 +309,216 @@ async def _return_kaleido_tab(self, tab: _KaleidoTab) -> None: await self.tabs_ready.put(tab) _logger.debug(f"{tab.tab.target_id[:4]} put back.") - def _clean_tab_return_task( - self, - main_task: asyncio.Task, - task: asyncio.Task, - ) -> None: - _logger.info("Cleaning out background tasks.") - self._background_render_tasks.remove(task) - e = task.exception() - if e: - _logger.error("Clean tab return task found exception", exc_info=e) - if not main_task.done(): - main_task.cancel() - raise e - - def _check_render_task( - self, - name: str, - tab: _KaleidoTab, - main_task: asyncio.Task, - error_log: None | list[ErrorEntry], - task: asyncio.Task, - ) -> None: - if task.cancelled(): - _logger.info(f"Something cancelled {name}.") - if error_log: - error_log.append( - ErrorEntry(name, asyncio.CancelledError, tab.javascript_log), - ) - elif e := task.exception(): - _logger.error(f"Render Task Error In {name}- ", exc_info=e) - if isinstance(e, (asyncio.TimeoutError, TimeoutError)) and error_log: - error_log.append( - ErrorEntry(name, e, tab.javascript_log), - ) - else: - _logger.error("Cancelling all.") - if not main_task.done(): - main_task.cancel() - raise e - _logger.info(f"Returning {name} tab after render.") - t = asyncio.create_task(self._return_kaleido_tab(tab)) - self._background_render_tasks.add(t) - t.add_done_callback(partial(self._clean_tab_return_task, main_task)) - + # _retuner_task MUST calculate full_path before it awaits async def _render_task( self, - tab: _KaleidoTab, - args: Any, - error_log: None | list[ErrorEntry] = None, - profiler: None | list = None, - ): - _logger.info(f"Posting a task for {args['full_path'].name}") - if self._timeout: - try: - await asyncio.wait_for( - tab._write_fig( # noqa: SLF001 I don't want it documented, too complex for user - **args, - error_log=error_log, - profiler=profiler, - ), - self._timeout, # timeout can be None, no need for branches - ) - except BaseException as e: - if error_log: - error_log.append( - ErrorEntry( - args["full_path"].name, - e, - tab.javascript_log - if hasattr( - tab, - "javascript_log", - ) - else [], - ), - ) - else: - raise - - else: - await tab._write_fig( # noqa: SLF001 I don't want it documented, too complex for user - **args, - error_log=error_log, - profiler=profiler, - ) - _logger.info(f"Posted task ending for {args['full_path'].name}") - - async def calc_fig( - self, - fig: _fig_tools.Figurish, - path: str | Path | None = None, - opts: None | _fig_tools.LayoutOpts = None, + fig_arg: FigureDict, *, - topojson: str | None = None, - ): - """ - Calculate the bytes for a figure. - - This function does not support parallelism or multi-image processing like - `write_fig` does, although its arguments are a subset of those of `write_fig`. - This function is currently just meant to bridge the old and new API. - """ - if not _is_figurish(fig) and isinstance(fig, Iterable): - raise TypeError("Calc fig can not process multiple images at a time.") - spec, full_path = build_fig_spec(fig, path, opts) - tab = await self._get_kaleido_tab() - args = { - "spec": spec, - "full_path": full_path, - "topojson": topojson, - } - data = None - timeout = self._timeout if self._timeout else None - data = await asyncio.wait_for( - tab._calc_fig( # noqa: SLF001 I don't want it documented, too complex for user - **args, - ), - timeout, + topojson: str | None, + _write: bool, + profiler: _profiler.WriteCall, + stepper: bool, + ) -> None | bytes: + spec = fig_tools.coerce_for_js( + fig_arg.get("fig"), + fig_arg.get("path", None), + fig_arg.get("opts", None), ) - await self._return_kaleido_tab(tab) - return data[0] - async def write_fig( # noqa: PLR0913, PLR0912, C901 (too many args, complexity) - self, - fig: _fig_tools.Figurish, - path: str | Path | None = None, - opts: _fig_tools.LayoutOpts | None = None, - *, - topojson: str | None = None, - error_log: None | list[ErrorEntry] = None, - profiler: None | list = None, - ): - """ - Call the plotly renderer via javascript on first available tab. - - Args: - fig: the plotly figure or an iterable of plotly figures - path: the path to write the images to. if its a directory, we will try to - generate a name. If the path contains an extension, - "path/to/my_image.png", that extension will be the format used if not - overridden in `opts`. If you pass a complete path (filename), for - multiple figures, you will overwrite every previous figure. - opts: dictionary describing format, width, height, and scale of image - topojson: a link ??? TODO - error_log: a supplied list, will be populated with `ErrorEntry`s - which can be converted to strings. Note, this is for - collections errors that have to do with plotly. They will - not be thrown. Lower level errors (kaleido, choreographer) - will still be thrown. If not passed, all errors raise. - profiler: a supplied dictionary to collect stats about the operation - about tabs, runtimes, etc. + if _write: + full_path = path_tools.determine_path( + fig_arg.get("path", None), + spec["data"], + spec["format"], # should just take spec + ) + full_path.touch() # claim our name - """ - if error_log is not None: - _logger.info("Using error log.") - if profiler is not None: - _logger.info("Using profiler.") + tab = await self._get_kaleido_tab() - if _is_figurish(fig) or not isinstance(fig, Iterable): - fig = [fig] - else: - _logger.debug(f"Is iterable {type(fig)}") + render_prof = _profiler.RenderTaskProfile( + spec, + full_path if _write else None, + tab.tab.target_id, + ) + render_prof.profile_log.tick("acquired tab") + profiler.renders.append(render_prof) - if main_task := asyncio.current_task(): - self._main_tasks.add(main_task) - tasks = set() - - async def _loop(f): - spec, full_path = build_fig_spec(f, path, opts) - tab = await self._get_kaleido_tab() - if profiler is not None and tab.tab.target_id not in profiler: - profiler[tab.tab.target_id] = [] - t = asyncio.create_task( - self._render_task( - tab, - args={ - "spec": spec, - "full_path": full_path, - "topojson": topojson, - }, - error_log=error_log, - profiler=profiler, - ), - ) - t.add_done_callback( - partial( - self._check_render_task, - full_path.name, - tab, - main_task, - error_log, + try: + img_bytes = await asyncio.wait_for( + tab._calc_fig( # noqa: SLF001 + spec, + topojson=topojson, + render_prof=render_prof, + stepper=stepper, ), + self._timeout, ) - tasks.add(t) - - try: - if hasattr(fig, "__aiter__"): # is async iterable - _logger.debug("Is async for") - async for f in fig: - await _loop(f) + if _write: + render_prof.profile_log.tick("starting file write") + await _utils.to_thread(full_path.write_bytes, img_bytes) + render_prof.profile_log.tick("file write done") + return None else: - _logger.debug("Is sync for") - for f in fig: - await _loop(f) - _logger.debug("awaiting tasks") - await asyncio.gather(*tasks, return_exceptions=True) - except: - _logger.exception("Cleaning tasks after error.") - for task in tasks: - if not task.done(): - task.cancel() + return img_bytes + except BaseException as e: + render_prof.profile_log.tick("errored out") + if _write: + full_path.unlink() # failure, no write + render_prof.error = e raise finally: - if main_task: - self._main_tasks.remove(main_task) + render_prof.profile_log.tick("returning tab") + await self._return_kaleido_tab(tab) + render_prof.profile_log.tick("tab returned") - async def write_fig_from_object( # noqa: C901 too complex + ### API ### + @overload + async def write_fig_from_object( self, - generator: Iterable | AsyncIterable, + fig_dicts: FigureDict, *, - error_log: None | list[ErrorEntry] = None, - profiler: None | list = None, - ): - """ - Equal to `write_fig` but allows the user to generate all arguments. + cancel_on_error: bool = False, + _write: Literal[False], + stepper: bool = False, + ) -> bytes: ... - Generator must yield dictionaries with keys: - - fig: the plotly figure - - path: (optional, string or pathlib.Path) the path - - opts: (optional) dictionary with: - - format (string) - - scale (number) - - height (number) - - and width (number) - - topojson: (optional) topojsons are used to customize choropleths + @overload + async def write_fig_from_object( + self, + fig_dicts: FigureDict | AnyIterable[FigureDict], + *, + cancel_on_error: Literal[True], + _write: Literal[True] = True, + stepper: bool = False, + ) -> None: ... - Generators are good because, if rendering many images, one doesn't need to - prerender them all. They can be rendered and yielded asynchronously. + @overload + async def write_fig_from_object( + self, + fig_dicts: FigureDict | AnyIterable[FigureDict], + *, + cancel_on_error: Literal[False] = False, + _write: Literal[True] = True, + stepper: bool = False, + ) -> tuple[Exception]: ... - While `write_fig` can also take generators, but only for the figure. - In this case, the generator will specify all render-related arguments. + @overload + async def write_fig_from_object( + self, + fig_dicts: FigureDict | AnyIterable[FigureDict], + *, + cancel_on_error: bool, + _write: Literal[True] = True, + stepper: bool = False, + ) -> tuple[Exception] | None: ... - Args: - generator: an iterable or generator which supplies a dictionary - of arguments to pass to tab.write_fig. - error_log: A supplied list, will be populated with `ErrorEntry`s - which can be converted to strings. Note, this is for - collections errors that have to do with plotly. They will - not be thrown. Lower level errors (kaleido, choreographer) - will still be thrown. - profiler: A supplied dictionary, will be populated with information - about tabs, runtimes, etc. + async def write_fig_from_object( + self, + fig_dicts: FigureDict | AnyIterable[FigureDict], + *, + cancel_on_error=False, + _write: bool = True, # backwards compatibility! + stepper: bool = False, + ) -> None | bytes | tuple[Exception]: + """Temp.""" + if not _write: + cancel_on_error = True - """ - if error_log is not None: - _logger.info("Using error log.") - if profiler is not None: - _logger.info("Using profiler.") + if _is_figuredict(fig_dicts): + fig_dicts = [fig_dicts] + name = "No Name" if main_task := asyncio.current_task(): - self._main_tasks.add(main_task) - tasks = set() - - async def _loop(args): - spec, full_path = build_fig_spec( - args.pop("fig"), - args.pop("path", None), - args.pop("opts", None), - ) - args["spec"] = spec - args["full_path"] = full_path - tab = await self._get_kaleido_tab() - if profiler is not None and tab.tab.target_id not in profiler: - profiler[tab.tab.target_id] = [] - t = asyncio.create_task( - self._render_task( - tab, - args=args, - error_log=error_log, - profiler=profiler, - ), - ) - t.add_done_callback( - partial( - self._check_render_task, - full_path.name, - tab, - main_task, - error_log, - ), - ) - tasks.add(t) + self._main_render_coroutines.add(main_task) + name = main_task.get_name() + + profiler = _profiler.WriteCall(name) + self.profiler.append(profiler) + + tasks: set[asyncio.Task] = set() try: - if hasattr(generator, "__aiter__"): # is async iterable - _logger.debug("Is async for") - async for args in generator: - await _loop(args) + async for fig_arg in _utils.ensure_async_iter(fig_dicts): + t: asyncio.Task = asyncio.create_task( + self._render_task( + fig_arg=fig_arg, + topojson=fig_arg.get("topojson"), + _write=_write, # backwards compatibility + profiler=profiler, + stepper=stepper, + ), + ) + tasks.add(t) + await asyncio.sleep(0) # this forces the added task to run + + res = await asyncio.gather(*tasks, return_exceptions=not cancel_on_error) + if not _write: + return cast("bytes", res[0]) + elif cancel_on_error: + return None else: - _logger.debug("Is sync for") - for args in generator: - await _loop(args) - _logger.debug("awaiting tasks") - await asyncio.gather(*tasks, return_exceptions=True) - except: - _logger.exception("Cleaning tasks after error.") + return cast("tuple[Exception]", tuple(r for r in res if r)) + + finally: for task in tasks: if not task.done(): task.cancel() - raise - finally: if main_task: - self._main_tasks.remove(main_task) + self._main_render_coroutines.remove(main_task) + + async def write_fig( # noqa: PLR0913 + self, + fig: fig_tools.Figurish, + path: None | Path | str = None, + opts: None | fig_tools.LayoutOpts = None, + *, + topojson: str | None = None, + cancel_on_error: bool = False, + stepper: bool = False, + ) -> tuple[Exception] | None: + """Temp.""" + if fig_tools.is_figurish(fig) or not isinstance( + fig, + (Iterable, AsyncIterable), + ): + fig = [fig] + + async def _temp_generator() -> AsyncGenerator[FigureDict, None]: + async for f in _utils.ensure_async_iter(fig): + yield { + "fig": f, + "path": path, + "opts": opts, + "topojson": topojson, + } + + generator = cast("AsyncIterable[FigureDict]", _temp_generator()) + return await self.write_fig_from_object( + fig_dicts=generator, + cancel_on_error=cancel_on_error, + stepper=stepper, + ) + + async def calc_fig( + self, + fig: fig_tools.Figurish, + opts: None | fig_tools.LayoutOpts = None, + *, + topojson: str | None = None, + stepper: bool = False, + ) -> bytes: + """Temp.""" + + async def _temp_generator(): + yield { + "fig": fig, + "opts": opts, + "topojson": topojson, + } + + return await self.write_fig_from_object( + fig_dicts=_temp_generator(), + cancel_on_error=True, + _write=False, + stepper=stepper, + ) diff --git a/src/py/kaleido/mocker/__init__.py b/src/py/kaleido/mocker/__init__.py new file mode 100644 index 00000000..63aaae39 --- /dev/null +++ b/src/py/kaleido/mocker/__init__.py @@ -0,0 +1,69 @@ +"""Mocker is an integration-test utility.""" + +from __future__ import annotations + +import asyncio +import sys +from random import sample +from typing import TYPE_CHECKING + +import logistro + +import kaleido + +from . import _utils +from ._args import args + +if TYPE_CHECKING: + from pathlib import Path + +_logger = logistro.getLogger(__name__) + +# ruff: noqa: T201 we print stuff. + + +def random_config(paths: list[Path]) -> list[Path]: + """Select a portion of possible paths.""" + if args.random > len(paths): + raise ValueError( + f"Input discover {len(paths)} paths, but a sampling of" + f"{args.random} was asked for.", + ) + return sample(paths, args.random) + + +# Function to process the images +async def _main(): + paths = _utils.get_jsons_in_paths(args.input) + if args.random: + paths = random_config(paths) + + async with kaleido.Kaleido( + page_generator=kaleido.PageGenerator(force_cdn=True), + n=args.n, + headless=args.headless, + timeout=args.timeout, + ) as k: + return await k.write_fig_from_object( + _utils.load_figures_from_paths(paths), + stepper=args.stepper, + cancel_on_error=args.fail_fast, + ), k.profiler + + +def main(): + """[project.scripts] expects to call a function, not a module.""" + errors, _profiler = asyncio.run(_main()) + # do profile here + if errors: + # better to get this from the profile + print(f"Number of errors: {len(errors)}") + for i, e in enumerate(errors): + print(str(e), file=sys.stderr) + if i > 10: # noqa: PLR2004 + print("More than 10 errors, use --profile.", file=sys.stderr) + break + + +if __name__ == "__main__": + main() diff --git a/src/py/kaleido/mocker/_args.py b/src/py/kaleido/mocker/_args.py new file mode 100644 index 00000000..ac44e0f9 --- /dev/null +++ b/src/py/kaleido/mocker/_args.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import logistro + +from . import _defaults + +_logger = logistro.getLogger(__name__) + +description = "\n".join( # noqa: FLY002 + [ + "kaleido_mocker loads & renders Plotly figures (from json or pickle).", + "", + "Furthermore, it outputs (to stdout) a JSON with performance information.", + "", + "", + ( + "Note: non-headless mode often interferes with multi-process mode as " + "non-visible windows are often throttled" + ), + ], +) + +if "--headless" in sys.argv and "--no-headless" in sys.argv: + raise ValueError( + "Choose either '--headless' or '--no-headless'.", + ) + +parser = argparse.ArgumentParser( + add_help=True, + parents=[logistro.parser], + conflict_handler="resolve", + description=description, +) + +# Overrides logstro default +parser.add_argument( + "--logistro-level", + default="INFO", + dest="log", + help="Set the logging level (default INFO)", +) + +basic_config = parser.add_argument_group("Basic Config Options") + +basic_config.add_argument( + "--n", + type=int, + default=_defaults.cpus, + help="Number of tabs, defaults to # of cpus", +) +basic_config.add_argument( + "--input", + type=str, + default=_defaults.in_dir, + help="Directory of mock file/s or single file (default tests/mocks)", +) +basic_config.add_argument( + "--output", + type=str, + default=_defaults.out_dir, + help="DIRECTORY of mock file/s (default tests/renders)", +) + +basic_config.add_argument( + "--timeout", + type=int, + default=90, + help="Set timeout in seconds for any 1 mock (default 60 seconds)", +) + +basic_config.add_argument( + "--fail-fast", + action="store_true", + default=False, + help="Throw first error encountered and stop execution.", +) + +basic_config.add_argument( + "--random", + type=int, + default=0, + help="Will select N random jsons- or if 0 (default), all.", +) + +# Image Setting Arguments + +image_parameters = parser.add_argument_group("Image Parameterize") + +image_parameters.add_argument( + "--parameterize", + action="store_true", + default=False, + help="Run mocks w/ different configurations.", +) + +image_parameters.add_argument( + "--format", + type=str, + default=argparse.SUPPRESS, + help="png (default), pdf, jpg, webp, svg, json", +) +image_parameters.add_argument( + "--width", + type=str, + default=argparse.SUPPRESS, + help="width in pixels (default 700)", +) +image_parameters.add_argument( + "--height", + type=str, + default=argparse.SUPPRESS, + help="height in pixels (default 500)", +) +image_parameters.add_argument( + "--scale", + type=str, + default=argparse.SUPPRESS, + help="Scale ratio, acts as multiplier for height/width (default 1)", +) + +# Diagnostic Arguments + +diagnostic_options = parser.add_argument_group("Diagnostic Options") + +diagnostic_options.add_argument( + "--headless", + action="store_true", + default=True, + help="Set headless as True (default)", +) +diagnostic_options.add_argument( + "--no-headless", + action="store_false", + dest="headless", + help="Set headless as False", +) + +diagnostic_options.add_argument( + "--stepper", + action="store_true", + default=False, + dest="stepper", + help="Stepper sets n to 1, headless to False, no timeout " + "and asks for confirmation before printing.", +) + + +args = parser.parse_args() + +logistro.getLogger().setLevel(args.log) + +if not Path(args.output).is_dir(): + raise ValueError(f"Specified output must be existing directory. Is {args.output!s}") + +args_d = vars(args) +if args_d["stepper"]: + args_d["n"] = 1 + args_d["headless"] = True + args_d["timeout"] = 0 + +_p = args_d["parameterize"] + +args_d.setdefault("width", _defaults.width if _p else _defaults.width[0]) +args_d.setdefault("height", _defaults.height if _p else _defaults.height[0]) +args_d.setdefault("scale", _defaults.scale if _p else _defaults.scale[0]) +args_d.setdefault("format", _defaults.extension if _p else _defaults.extension[0]) + +for key in ("width", "height", "scale", "format"): + if not isinstance(args_d[key], (list, tuple)): + args_d[key] = (args_d[key],) + +_logger.info(f"Mocker args calculated: {args_d}") diff --git a/src/py/kaleido/mocker/_defaults.py b/src/py/kaleido/mocker/_defaults.py new file mode 100644 index 00000000..1df94a75 --- /dev/null +++ b/src/py/kaleido/mocker/_defaults.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import multiprocessing +from pathlib import Path + +import logistro + +_logger = logistro.getLogger(__name__) + + +width = [700, 200, 1000] # first value in main default if not parameterized +height = [500, 200, 1000] +scale = [1, 0.5, 2] +extension = [ + "png", + "pdf", + "jpg", + "webp", + "svg", + "json", +] + +# use itertools.product + +# Number of CPUS +cpus = multiprocessing.cpu_count() + +# Default Directories +test_dir = Path(__file__).resolve().parent.parent.parent / "integration_tests" +in_dir = test_dir / "mocks" +out_dir = test_dir / "renders" diff --git a/src/py/kaleido/mocker/_utils.py b/src/py/kaleido/mocker/_utils.py new file mode 100644 index 00000000..125dbed0 --- /dev/null +++ b/src/py/kaleido/mocker/_utils.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import itertools +from pathlib import Path +from typing import TYPE_CHECKING, TypedDict + +import logistro +import orjson + +from ._args import args + +if TYPE_CHECKING: + ... + +_logger = logistro.getLogger(__name__) + + +def get_jsons_in_paths(path: str | Path) -> list[Path]: + # Work with Paths and directories + path = Path(path) if isinstance(path, str) else path + + if path.is_dir(): + _logger.info(f"Input is path {path}") + return list(path.glob("*.json")) + elif path.is_file(): + _logger.info(f"Input is file {path}") + return [path] + else: + raise TypeError("--input must be file or directory") + + +class Param(TypedDict): + name: str + opts: dict[str, int | float] + + +# maybe don't have this do params and figures +def load_figures_from_paths(paths: list[Path]): + # Set json + for path in paths: + if not path.is_file(): + raise RuntimeError(f"Path {path} is not a file.") + _logger.info(f"Found file: {path!s}") + with path.open(encoding="utf-8") as file: + figure = orjson.loads(file.read()) + for f, w, h, s in itertools.product( # all combos + args.format, + args.width, + args.height, + args.scale, + ): + name = ( + f"{path.stem}.{f!s}" + if not args.parameterize + else f"{path.stem!s}-{w!s}x{h!s}@{s!s}.{f!s}" + ) + opts = { + "scale": s, + "width": w, + "height": h, + } + _logger.info(f"Yielding spec: {name!s}") + yield { + "fig": figure, + "path": str(Path(args.output) / name), + "opts": opts, + } diff --git a/src/py/pyproject.toml b/src/py/pyproject.toml index c0436a8e..d44301ac 100644 --- a/src/py/pyproject.toml +++ b/src/py/pyproject.toml @@ -38,7 +38,7 @@ Homepage = "https://github.com/plotly/kaleido" Repository = "https://github.com/plotly/kaleido" [project.scripts] -kaleido_mocker = "kaleido._mocker:build_mocks" +kaleido_mocker = "kaleido.mocker:main" kaleido_get_chrome = "choreographer.cli._cli_utils:get_chrome_cli" [dependency-groups] @@ -54,7 +54,9 @@ dev = [ "pandas>=2.0.3", "typing-extensions>=4.12.2", "hypothesis>=6.113.0", + "pyright>=1.1.406", ] + pickles = [ "colorcet>=3.1.0", "datashader>=0.15.2", @@ -82,6 +84,7 @@ ignore = [ "SIM105", # Too opionated (try-except-pass) "PT003", # scope="function" implied but I like readability "G004", # fstrings in my logs + "TD003", # issue link in todos ] [tool.ruff.lint.per-file-ignores] @@ -102,7 +105,7 @@ log_cli = false # name = cmd [tool.poe.tasks.test] -cmd = "pytest --timeout=90 --log-level=1 -W error -n auto -v -rfE --capture=fd" +cmd = "pytest --timeout=50 --log-level=1 -W error -n auto -v -rfE --capture=fd" help = "Run all tests quickly" [tool.poe.tasks.debug-test] @@ -113,3 +116,8 @@ help = "Run test by test, slowly, quitting after first error" [tool.poe.tasks.filter-test] cmd = "pytest --log-level=1 -W error -vvvx -rA --capture=no --show-capture=no" help = "Run any/all tests one by one with basic settings: can include filename and -k filters" + +[tool.pyright] +venvPath = "." +venv = ".venv" +exclude= ["src/py/integration_tests/dates/*"] diff --git a/src/py/tests/README.md b/src/py/tests/README.md new file mode 100644 index 00000000..a332a06a --- /dev/null +++ b/src/py/tests/README.md @@ -0,0 +1,14 @@ + +To clarify three similar test files: + +- test_public_api.py does render tests of renderers in __init__.py + - it does not parametrize +- test_init.py tests that wrappers are proper and pass args/kwargs + - it does parameterize +- test_kaleido tests the substantial parts of kaleido and its returns + - it has incomplete parameterizing + + + +Parameterizing actual renders would be a huge burden, so integration tests +with mocks are currently the most complete tests. diff --git a/src/py/tests/test_calc_fig_one_off.py b/src/py/tests/test_calc_fig_one_off.py index 70ae170f..233260e1 100644 --- a/src/py/tests/test_calc_fig_one_off.py +++ b/src/py/tests/test_calc_fig_one_off.py @@ -13,7 +13,7 @@ async def test_calc_fig(): # ruff: noqa: PLC0415 - import plotly.express as px + import plotly.express as px # type: ignore[import-untyped] with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) diff --git a/src/py/tests/test_fig_tools.py b/src/py/tests/test_fig_tools.py index 4fc8be11..c170ff61 100644 --- a/src/py/tests/test_fig_tools.py +++ b/src/py/tests/test_fig_tools.py @@ -1,26 +1,30 @@ -from pathlib import Path - import pytest -from kaleido import _fig_tools +from kaleido._utils import fig_tools sources = ["argument", "layout", "template", "default"] values = [None, 150, 800, 1500] +values2 = [None, 300, 1000, 1300] @pytest.mark.parametrize("width_source", sources) @pytest.mark.parametrize("height_source", sources) @pytest.mark.parametrize("width_value", values) -@pytest.mark.parametrize("height_value", [x * 1.5 if x else x for x in values]) -def test_get_figure_dimensions(width_source, height_source, width_value, height_value): - """Test _get_figure_dimensions with all combinations of width/height sources.""" +@pytest.mark.parametrize("height_value", values2) +def test_coerce_for_js_dimensions( + width_source, + height_source, + width_value, + height_value, +): + """Test coerce_for_js with all combinations of width/height sources.""" layout = {} - width_arg = None + opts = {} expected_width = width_value if width_source == "argument": - width_arg = width_value + opts["width"] = width_value elif width_source == "layout": layout["width"] = width_value elif width_source == "template": @@ -32,14 +36,13 @@ def test_get_figure_dimensions(width_source, height_source, width_value, height_ # Set to default if None if expected_width is None: - expected_width = _fig_tools.DEFAULT_WIDTH + expected_width = fig_tools.DEFAULT_WIDTH # Do for height what I did for width - height_arg = None expected_height = height_value if height_source == "argument": - height_arg = height_value + opts["height"] = height_value elif height_source == "layout": layout["height"] = height_value elif height_source == "template": @@ -51,173 +54,20 @@ def test_get_figure_dimensions(width_source, height_source, width_value, height_ # Set to default if None if expected_height is None: - expected_height = _fig_tools.DEFAULT_HEIGHT + expected_height = fig_tools.DEFAULT_HEIGHT + + # Create a figure dict with the layout + fig = {"data": [], "layout": layout} # Call the function - r_width, r_height = _fig_tools._get_figure_dimensions( # noqa: SLF001 - layout, - width_arg, - height_arg, - ) + spec = fig_tools.coerce_for_js(fig, None, opts) # Assert results - assert r_width == expected_width, ( - f"Width mismatch: got {r_width}, expected {expected_width}, " + assert spec["width"] == expected_width, ( + f"Width mismatch: got {spec['width']}, expected {expected_width}, " f"source: {width_source}, value: {width_value}" ) - assert r_height == expected_height, ( - f"Height mismatch: got {r_height}, expected {expected_height}, " + assert spec["height"] == expected_height, ( + f"Height mismatch: got {spec['height']}, expected {expected_height}, " f"source: {height_source}, value: {height_value}" ) - - -def test_next_filename_no_existing_files(tmp_path): - """Test _next_filename when no files exist.""" - result = _fig_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 - assert result == "test.png" - - -def test_next_filename_base_file_exists(tmp_path): - """Test _next_filename when base file exists.""" - # Create the base file - (tmp_path / "test.png").touch() - - result = _fig_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 - assert result == "test-2.png" - - -def test_next_filename_numbered_files_exist(tmp_path): - """Test _next_filename when numbered files exist.""" - # Create various numbered files - (tmp_path / "test.png").touch() - (tmp_path / "test-2.png").touch() - (tmp_path / "test-3.png").touch() - (tmp_path / "test-5.png").touch() # Gap in numbering - - result = _fig_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 - assert result == "test-6.png" # Should be max + 1 - - -def test_next_filename_similar_names_ignored(tmp_path): - """Test _next_filename ignores files with similar but different names.""" - # Create files that shouldn't match the pattern - (tmp_path / "test.png").touch() - (tmp_path / "test-2.png").touch() - (tmp_path / "testing-3.png").touch() # Different prefix - (tmp_path / "test-2.jpg").touch() # Different extension - (tmp_path / "test-abc.png").touch() # Non-numeric suffix - - result = _fig_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 - assert result == "test-3.png" # Should only count test.png and test-2.png - - -def test_next_filename_special_characters(tmp_path): - """Test _next_filename with special characters in prefix and extension.""" - prefix = "test-file_name" - ext = "svg" # set up to be parameterized but not - - # Create some files - (tmp_path / f"{prefix}.{ext}").touch() - (tmp_path / f"{prefix}-2.{ext}").touch() - - result = _fig_tools._next_filename(tmp_path, prefix, ext) # noqa: SLF001 - assert result == f"{prefix}-3.{ext}" - - -def test_next_filename_only_numbered_files(tmp_path): - """Test _next_filename when only numbered files exist (no base file).""" - # Create only numbered files, no base file - (tmp_path / "test-2.png").touch() - (tmp_path / "test-3.png").touch() - (tmp_path / "test-10.png").touch() - - result = _fig_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 - assert result == "test-11.png" # Should be max + 1 - - -# Fixtures for _build_full_path tests - testing various title scenarios -@pytest.fixture( - params=[ - ( - { - "layout": { - "title": {"text": "My-Test!@#$%^&*()Chart_with[lots]of{symbols}"}, - }, - }, - "My_TestChart_withlotsofsymbols", - ), # Complex title - ( - {"layout": {"title": {"text": "Simple Title"}}}, - "Simple_Title", - ), # Simple title - ({"layout": {}}, "fig"), # No title - ], -) -def fig_fixture(request): - """Parameterized fixture for fig with various title scenarios.""" - return request.param - - -def test_build_full_path_no_path_input(fig_fixture): - """Test _build_full_path with no path input uses current path.""" - fig_dict, expected_prefix = fig_fixture - result = _fig_tools._build_full_path(None, fig_dict, "ext") # noqa: SLF001 - - # Should use current directory - assert result.parent.resolve() == Path().cwd().resolve() - assert result.parent.is_dir() - - assert result.name == f"{expected_prefix}.ext" - - -def test_build_full_path_no_suffix_directory(tmp_path, fig_fixture): - """Test _build_full_path with path having no suffix.""" - fig_dict, expected_prefix = fig_fixture - - # Test directory no suffix - test_dir = tmp_path - result = _fig_tools._build_full_path(test_dir, fig_dict, "ext") # noqa: SLF001 - - # Should use provided directory - assert result.parent == test_dir - assert result.name == f"{expected_prefix}.ext" - - # Test error - nonexistent_dir = Path("/nonexistent/directory") - with pytest.raises(ValueError, match=r"Directory .* not found. Please create it."): - _fig_tools._build_full_path(nonexistent_dir, fig_dict, "ext") # noqa: SLF001 - - -def test_build_full_path_directory_with_suffix(tmp_path, fig_fixture): - """Test _build_full_path with path that is directory even with suffix.""" - fig_dict, expected_prefix = fig_fixture - - # Create a directory with a suffix-like name - dir_with_suffix = tmp_path / "mydir.png" - dir_with_suffix.mkdir() - - result = _fig_tools._build_full_path(dir_with_suffix, fig_dict, "ext") # noqa: SLF001 - - # Should treat as directory - assert result.parent == dir_with_suffix - assert result.name == f"{expected_prefix}.ext" - - -def test_build_full_path_file_with_suffix(tmp_path, fig_fixture): - """Test _build_full_path with file path having suffix.""" - fig_dict, _expected_prefix = fig_fixture - - # Exists - file_path = tmp_path / "output.png" - result = _fig_tools._build_full_path(file_path, fig_dict, "ext") # noqa: SLF001 - - # Should return the exact path provided - assert result == file_path - - # Doesn't exist - file_path = Path("/nonexistent/directory/output.png") - with pytest.raises( - RuntimeError, - match=r"Cannot reach path .* Are all directories created?", - ): - _fig_tools._build_full_path(file_path, fig_dict, "ext") # noqa: SLF001 diff --git a/src/py/tests/test_init.py b/src/py/tests/test_init.py index 79fb5ba3..3574be76 100644 --- a/src/py/tests/test_init.py +++ b/src/py/tests/test_init.py @@ -1,15 +1,21 @@ """Tests for wrapper functions in __init__.py that test argument passing.""" +import subprocess +import sys +from pathlib import Path +from typing import TYPE_CHECKING from unittest.mock import AsyncMock, patch import pytest import kaleido -# Pretty complicated for basically testing a bunch of wrappers, but it works. -# Integration tests seem more important. -# I much prefer the public_api file, this set of tests can be considered -# for deletion. +if TYPE_CHECKING: + from kaleido._utils.fig_tools import Figurish + + +# Just tests wrapping, but in a way tests internals. +# These are better done as part of integration tests. @pytest.fixture @@ -24,6 +30,16 @@ def kwargs(): return {"width": 800} +# line serves to force static check of string in @patch +_ = kaleido._sync_server.GlobalKaleidoServer.open # noqa: SLF001 + + +def test_hangers(): + folder = Path(__file__).parent / "win_hang_scripts" + subprocess.run([sys.executable, str(folder / "open_close.py")], check=True) # noqa: S603 + subprocess.run([sys.executable, str(folder / "open.py")], check=True) # noqa: S603 + + @patch("kaleido._sync_server.GlobalKaleidoServer.open") def test_start_sync_server_passes_args(mock_open, args, kwargs): """Test that start_sync_server passes args and silence_warnings correctly.""" @@ -37,6 +53,10 @@ def test_start_sync_server_passes_args(mock_open, args, kwargs): mock_open.assert_called_with(*args, silence_warnings=True, **kwargs) +# line serves to force static check of string in @patch +_ = kaleido._sync_server.GlobalKaleidoServer.close # noqa: SLF001 + + @patch("kaleido._sync_server.GlobalKaleidoServer.close") def test_stop_sync_server_passes_args(mock_close): """Test that stop_sync_server passes silence_warnings correctly.""" @@ -50,6 +70,10 @@ def test_stop_sync_server_passes_args(mock_close): mock_close.assert_called_with(silence_warnings=True) +# line serves to force static check of string in @patch +_ = kaleido.Kaleido + + @patch("kaleido.Kaleido") async def test_async_wrapper_functions(mock_kaleido_class): """Test all async wrapper functions pass arguments correctly. @@ -71,13 +95,17 @@ async def test_async_wrapper_functions(mock_kaleido_class): topojson = "test_topojson" kopts = {"some_option": "value"} - result = await kaleido.calc_fig(fig, path, opts, topojson=topojson, kopts=kopts) + result = await kaleido.calc_fig( + fig, + opts, # type: ignore[reportArgumentType] + topojson=topojson, + kopts=kopts, + ) expected_kopts = {"some_option": "value", "n": 1} mock_kaleido_class.assert_called_with(**expected_kopts) mock_kaleido.calc_fig.assert_called_with( fig, - path=path, opts=opts, topojson=topojson, ) @@ -96,7 +124,13 @@ async def test_async_wrapper_functions(mock_kaleido_class): mock_kaleido.write_fig.reset_mock() # Test write_fig with full arguments - await kaleido.write_fig(fig, path, opts, topojson=topojson, kopts=kopts) + await kaleido.write_fig( + fig, + path, + opts, # type: ignore[reportArgumentType] + topojson=topojson, + kopts=kopts, + ) mock_kaleido_class.assert_called_with(**kopts) # write_fig doesn't force n=1 mock_kaleido.write_fig.assert_called_with( fig, @@ -118,12 +152,17 @@ async def test_async_wrapper_functions(mock_kaleido_class): mock_kaleido.write_fig_from_object.reset_mock() # Test write_fig_from_object - generator = [{"data": []}] + generator: list[Figurish] = [{"data": []}] await kaleido.write_fig_from_object(generator, kopts=kopts) mock_kaleido_class.assert_called_with(**kopts) mock_kaleido.write_fig_from_object.assert_called_with(generator) +# line serves to force static check of string in @patch +_ = kaleido._sync_server.GlobalKaleidoServer.is_running # noqa: SLF001 +_ = kaleido._sync_server.GlobalKaleidoServer.call_function # noqa: SLF001 + + @patch("kaleido._sync_server.GlobalKaleidoServer.is_running") @patch("kaleido._sync_server.GlobalKaleidoServer.call_function") def test_sync_wrapper_server(mock_call_function, mock_is_running, args, kwargs): @@ -147,6 +186,11 @@ def test_sync_wrapper_server(mock_call_function, mock_is_running, args, kwargs): mock_call_function.assert_called_with("write_fig_from_object", *args, **kwargs) +# line serves to force static check of string in @patch +_ = kaleido._sync_server.GlobalKaleidoServer.is_running # noqa: SLF001 +_ = kaleido._sync_server.oneshot_async_run # noqa: SLF001 + + @patch("kaleido._sync_server.GlobalKaleidoServer.is_running") @patch("kaleido._sync_server.oneshot_async_run") def test_sync_wrapper_oneshot(mock_oneshot_run, mock_is_running, args, kwargs): diff --git a/src/py/tests/test_kaleido.py b/src/py/tests/test_kaleido.py index 6b178dce..84424d2d 100644 --- a/src/py/tests/test_kaleido.py +++ b/src/py/tests/test_kaleido.py @@ -1,18 +1,21 @@ import asyncio import re -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest -from hypothesis import HealthCheck, Phase, given, settings +from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st from kaleido import Kaleido -@pytest.fixture +# can't do session scope because pytest complains that its used by +# function-scoped loops. tried to create a separate loop in here with +# session, lots of spooky errors, even asyncio.run() doesn't clean up right. +@pytest.fixture(scope="function") async def simple_figure_with_bytes(): """Create a simple figure with calculated bytes and PNG assertion.""" - import plotly.express as px # noqa: PLC0415 + import plotly.express as px # type: ignore[import-untyped] # noqa: PLC0415 fig = px.line(x=[1, 2, 3], y=[1, 2, 3]) @@ -117,52 +120,77 @@ async def test_write_fig_from_object_iterator(simple_figure_with_bytes, tmp_path ) +async def test_write_fig_from_object_return_modes(simple_figure_with_bytes, tmp_path): + """Test write_fig_from_object with different return schemes.""" + + fig_list = [] + file_paths = [] + for i in range(2): + path = tmp_path / "does_not_exist" / f"test_iter_{i}.png" + file_paths.append(path) + fig_list.append( + { + "fig": simple_figure_with_bytes["fig"], + "path": path, + "opts": simple_figure_with_bytes["opts"], + }, + ) + + # test collecting errors + async with Kaleido() as k: + res = await k.write_fig_from_object(fig_list, cancel_on_error=False) + for r in res: + assert isinstance(r, RuntimeError) + assert len(res) == len(fig_list) + + # test not collecting errors + with pytest.raises(RuntimeError): + async with Kaleido() as k: + res = await k.write_fig_from_object(fig_list, cancel_on_error=True) + + # test returning + async with Kaleido() as k: + res = await k.write_fig_from_object(fig_list[0], _write=False) + assert res == simple_figure_with_bytes["bytes"] + + # Assert that each created file matches the fixture bytes + for path in file_paths: + assert not path.exists() + + async def test_write_fig_from_object_bare_dictionary( simple_figure_with_bytes, tmp_path, ): - """Test write_fig_from_object with bare dictionary list.""" + """Test write_fig_from_object with bare dictionary.""" path1 = tmp_path / "test_dict_1.png" - path2 = tmp_path / "test_dict_2.png" - - fig_data = [ - { - "fig": simple_figure_with_bytes["fig"], - "path": path1, - "opts": simple_figure_with_bytes["opts"], - }, - { - "fig": simple_figure_with_bytes["fig"].to_dict(), - "path": path2, - "opts": simple_figure_with_bytes["opts"], - }, - ] + + fig_data = { + "fig": simple_figure_with_bytes["fig"], + "path": path1, + "opts": simple_figure_with_bytes["opts"], + } async with Kaleido() as k: await k.write_fig_from_object(fig_data) # Assert that each created file matches the fixture bytes - for path in [path1, path2]: - assert path.exists(), f"File {path} was not created" - created_bytes = path.read_bytes() - assert created_bytes == simple_figure_with_bytes["bytes"], ( - f"File {path} bytes don't match fixture bytes" - ) + assert path1.exists(), f"File {path1} was not created" + created_bytes = path1.read_bytes() + assert created_bytes == simple_figure_with_bytes["bytes"], ( + f"File {path1} bytes don't match fixture bytes" + ) -# In the refactor, all figure generation methods are really just wrappers -# for the most flexible, tested above, generate_fig_from_object. -# So we test that one, and then test to make sure its receiving arguments -# properly for the other tests. +@pytest.fixture(scope="function") +def test_kaleido(): # speed up hypothesis test using a function fixture + return Kaleido() -# Uncomment these settings after refactor. -# @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) @settings( - phases=[Phase.generate], - max_examples=1, suppress_health_check=[HealthCheck.function_scoped_fixture], + max_examples=50, ) @given( path=st.text( @@ -175,8 +203,17 @@ async def test_write_fig_from_object_bare_dictionary( format_type=st.sampled_from(["png", "svg", "pdf", "html"]), topojson=st.one_of(st.none(), st.text(min_size=1, max_size=20)), ) +@pytest.mark.parametrize( + "cancel_on_error", + [ + True, + False, + ], + ids=("cancel_on_error", "collect_errors"), +) async def test_write_fig_argument_passthrough( # noqa: PLR0913 - simple_figure_with_bytes, + test_kaleido, + cancel_on_error, tmp_path, path, width, @@ -184,32 +221,33 @@ async def test_write_fig_argument_passthrough( # noqa: PLR0913 format_type, topojson, ): - """Test that write_fig properly passes arguments to write_fig_from_object.""" - pytest.skip("Remove this failure line and the comment above after the refactor!") test_path = tmp_path / f"{path}.{format_type}" opts = {"format": format_type, "width": width, "height": height} - + fig = {"data": "test"} # Mock write_fig_from_object to capture arguments - with patch.object(Kaleido, "write_fig_from_object") as mock_write_fig_from_object: - async with Kaleido() as k: - await k.write_fig( - simple_figure_with_bytes["fig"], - path=test_path, - opts=opts, - topojson=topojson, - ) - + with patch.object( + Kaleido, + "write_fig_from_object", + new=AsyncMock(return_value=[]), + ) as mock_write_fig_from_object: + await test_kaleido.write_fig( + fig, + path=test_path, + opts=opts, + topojson=topojson, + cancel_on_error=cancel_on_error, + ) # Verify write_fig_from_object was called mock_write_fig_from_object.assert_called_once() # Extract the generator that was passed as first argument - args, _kwargs = mock_write_fig_from_object.call_args # not sure. - assert len(args) == 1, "Expected exactly one argument (the generator)" + _, kwargs = mock_write_fig_from_object.call_args # not sure. - generator = args[0] + generator = kwargs["fig_dicts"] + assert kwargs["cancel_on_error"] == cancel_on_error # Convert generator to list to inspect its contents - generated_args_list = list(generator) + generated_args_list = [v async for v in generator] assert len(generated_args_list) == 1, ( "Expected generator to yield exactly one item" ) @@ -223,14 +261,71 @@ async def test_write_fig_argument_passthrough( # noqa: PLR0913 assert "topojson" in generated_args, "Generated args should contain 'topojson'" # Check that the values match - assert generated_args["fig"] == simple_figure_with_bytes["fig"], ( - "Figure should match" - ) + assert generated_args["fig"] == fig, "Figure should match" assert str(generated_args["path"]) == str(test_path), "Path should match" assert generated_args["opts"] == opts, "Options should match" assert generated_args["topojson"] == topojson, "Topojson should match" +@settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + max_examples=50, +) +@given( + width=st.integers(min_value=100, max_value=2000), + height=st.integers(min_value=100, max_value=2000), + format_type=st.sampled_from(["png", "svg", "pdf", "html"]), + topojson=st.one_of(st.none(), st.text(min_size=1, max_size=20)), +) +async def test_calc_fig_argument_passthrough( + test_kaleido, + width, + height, + format_type, + topojson, +): + opts = {"format": format_type, "width": width, "height": height} + fig = {"data": "test"} + # Mock write_fig_from_object to capture arguments + with patch.object( + Kaleido, + "write_fig_from_object", + new=AsyncMock(return_value=[]), + ) as mock_write_fig_from_object: + await test_kaleido.calc_fig( + fig, + opts=opts, + topojson=topojson, + ) + # Verify write_fig_from_object was called + mock_write_fig_from_object.assert_called_once() + + # Extract the generator that was passed as first argument + _, kwargs = mock_write_fig_from_object.call_args # not sure. + + generator = kwargs["fig_dicts"] + assert kwargs["cancel_on_error"] is True + assert kwargs["_write"] is False + + # Convert generator to list to inspect its contents + generated_args_list = [v async for v in generator] + assert len(generated_args_list) == 1, ( + "Expected generator to yield exactly one item" + ) + + generated_args = generated_args_list[0] + + # Validate that the generated arguments match what we passed to write_fig + assert "fig" in generated_args, "Generated args should contain 'fig'" + assert "opts" in generated_args, "Generated args should contain 'opts'" + assert "topojson" in generated_args, "Generated args should contain 'topojson'" + + # Check that the values match + assert generated_args["fig"] == fig, "Figure should match" + assert generated_args["opts"] == opts, "Options should match" + assert generated_args["topojson"] == topojson, "Topojson should match" + + async def test_kaleido_instantiate_no_hang(): """Test that instantiating Kaleido doesn't hang.""" _ = Kaleido() @@ -340,7 +435,7 @@ async def test_unreasonable_timeout(simple_figure_with_bytes): opts = simple_figure_with_bytes["opts"] # Use an infinitely small timeout - async with Kaleido(timeout=0.000001) as k: + async with Kaleido(timeout=0.005) as k: with pytest.raises((asyncio.TimeoutError, TimeoutError)): await k.calc_fig(fig, opts=opts) diff --git a/src/py/tests/test_page_generator.py b/src/py/tests/test_page_generator.py index 3f7e4efc..f5b7907e 100644 --- a/src/py/tests/test_page_generator.py +++ b/src/py/tests/test_page_generator.py @@ -216,7 +216,7 @@ async def test_force_cdn(): """Test force_cdn=True forces use of CDN plotly even when plotly is available.""" # Verify plotly is available first if not find_spec("plotly"): - pytest.skip("Plotly not available - cannot test force_cdn override") + pytest.fail("Plotly not available - cannot test force_cdn override") forced_cdn = PageGenerator(force_cdn=True).generate_index() scripts, _encodings = get_scripts_from_html(forced_cdn) @@ -358,6 +358,10 @@ async def test_combined_overrides(tmp_path, data): assert len(scripts) == expected_count +# note: the logic below was extracted to utilities, +# so in a way its tested twice since tests were developed for that file + + # Test file path validation async def test_existing_file_path(temp_js_file): """Test that existing file paths work with and without file:/// protocol.""" @@ -383,6 +387,7 @@ async def test_existing_file_path(temp_js_file): async def test_nonexistent_file_path_raises_error( nonexistent_file_path, nonexistent_file_uri, + tmp_path, ): """Test that nonexistent file paths raise FileNotFoundError.""" # Test with regular path @@ -396,10 +401,15 @@ async def test_nonexistent_file_path_raises_error( with pytest.raises(FileNotFoundError): PageGenerator(plotly=nonexistent_file_uri) + # Test that existing directory raises error + with pytest.raises(FileNotFoundError): + PageGenerator(plotly=str(tmp_path)) + async def test_mathjax_nonexistent_file_raises_error( nonexistent_file_path, nonexistent_file_uri, + tmp_path, ): """Test that nonexistent mathjax file raises FileNotFoundError.""" # Test with regular path @@ -413,10 +423,15 @@ async def test_mathjax_nonexistent_file_raises_error( with pytest.raises(FileNotFoundError): PageGenerator(mathjax=nonexistent_file_uri) + # Test that existing directory raises error + with pytest.raises(FileNotFoundError): + PageGenerator(mathjax=str(tmp_path)) + async def test_others_nonexistent_file_raises_error( nonexistent_file_path, nonexistent_file_uri, + tmp_path, ): """Test that nonexistent file in others list raises FileNotFoundError.""" # Test with regular path @@ -430,6 +445,10 @@ async def test_others_nonexistent_file_raises_error( with pytest.raises(FileNotFoundError): PageGenerator(others=[nonexistent_file_uri]) + # Test that existing directory raises error + with pytest.raises(FileNotFoundError): + PageGenerator(others=[str(tmp_path)]) + # Test HTTP URLs (should not raise FileNotFoundError) async def test_http_urls_skip_file_validation(): diff --git a/src/py/tests/test_path_tools.py b/src/py/tests/test_path_tools.py new file mode 100644 index 00000000..e3c9ad1c --- /dev/null +++ b/src/py/tests/test_path_tools.py @@ -0,0 +1,157 @@ +from pathlib import Path + +import pytest + +from kaleido._utils import path_tools + + +def test_next_filename_no_existing_files(tmp_path): + """Test _next_filename when no files exist.""" + result = path_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 + assert result == "test.png" + + +def test_next_filename_base_file_exists(tmp_path): + """Test _next_filename when base file exists.""" + # Create the base file + (tmp_path / "test.png").touch() + + result = path_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 + assert result == "test-2.png" + + +def test_next_filename_numbered_files_exist(tmp_path): + """Test _next_filename when numbered files exist.""" + # Create various numbered files + (tmp_path / "test.png").touch() + (tmp_path / "test-2.png").touch() + (tmp_path / "test-3.png").touch() + (tmp_path / "test-5.png").touch() # Gap in numbering + + result = path_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 + assert result == "test-6.png" # Should be max + 1 + + +def test_next_filename_similar_names_ignored(tmp_path): + """Test _next_filename ignores files with similar but different names.""" + # Create files that shouldn't match the pattern + (tmp_path / "test.png").touch() + (tmp_path / "test-2.png").touch() + (tmp_path / "testing-3.png").touch() # Different prefix + (tmp_path / "test-2.jpg").touch() # Different extension + (tmp_path / "test-abc.png").touch() # Non-numeric suffix + + result = path_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 + assert result == "test-3.png" # Should only count test.png and test-2.png + + +def test_next_filename_special_characters(tmp_path): + """Test _next_filename with special characters in prefix and extension.""" + prefix = "test-f$ile_name" + ext = "s$v&g" # set up to be parameterized but not + + # Create some files + (tmp_path / f"{prefix}.{ext}").touch() + (tmp_path / f"{prefix}-2.{ext}").touch() + + result = path_tools._next_filename(tmp_path, prefix, ext) # noqa: SLF001 + assert result == f"{prefix}-3.{ext}" + + +def test_next_filename_only_numbered_files(tmp_path): + """Test _next_filename when only numbered files exist (no base file).""" + # Create only numbered files, no base file + (tmp_path / "test-2.png").touch() + (tmp_path / "test-3.png").touch() + (tmp_path / "test-10.png").touch() + + result = path_tools._next_filename(tmp_path, "test", "png") # noqa: SLF001 + assert result == "test-11.png" # Should be max + 1 + + +# Fixtures for determine_path tests - testing various title scenarios +@pytest.fixture( + params=[ + ( + { + "layout": { + "title": {"text": "My-Test!@#$%^&()Chart_with[lots]of{symbols}"}, + }, + }, + "My_TestChart_withlotsofsymbols", + ), # Complex title + ( + {"layout": {"title": {"text": "Simple Title"}}}, + "Simple_Title", + ), # Simple title + ({"layout": {}}, "fig"), # No title + ], +) +def fig_fixture(request): + """Parameterized fixture for fig with various title scenarios.""" + return request.param + + +def test_determine_path_no_path_input(fig_fixture): + """Test determine_path with no path input uses current path.""" + fig_dict, expected_prefix = fig_fixture + result = path_tools.determine_path(None, fig_dict, "ext") + + # Should use current directory + assert result.parent.resolve() == Path().cwd().resolve() + assert result.parent.is_dir() + + assert result.name == f"{expected_prefix}.ext" + + +def test_determine_path_no_suffix_directory(tmp_path, fig_fixture): + """Test determine_path with path to directory having no suffix.""" + fig_dict, expected_prefix = fig_fixture + + # Test directory no suffix + test_dir = tmp_path + result = path_tools.determine_path(test_dir, fig_dict, "ext") + + # Should use provided directory + assert result.parent == test_dir + assert result.name == f"{expected_prefix}.ext" + + # Test error + nonexistent_dir = Path("/nonexistent/directory") + with pytest.raises(ValueError, match=r"Directory .* not found. Please create it."): + path_tools.determine_path(nonexistent_dir, fig_dict, "ext") + + +def test_determine_path_directory_with_suffix(tmp_path, fig_fixture): + """Test determine_path with path that is directory even with suffix.""" + fig_dict, expected_prefix = fig_fixture + + # Create a directory with a suffix-like name + dir_with_suffix = tmp_path / "mydir.png" + dir_with_suffix.mkdir() + + result = path_tools.determine_path(dir_with_suffix, fig_dict, "ext") + + # Should treat as directory + assert result.parent == dir_with_suffix + assert result.name == f"{expected_prefix}.ext" + + +def test_determine_path_file_with_suffix(tmp_path, fig_fixture): + """Test determine_path with file path having suffix.""" + fig_dict, _expected_prefix = fig_fixture + + # Exists + file_path = tmp_path / "output.png" + result = path_tools.determine_path(file_path, fig_dict, "ext") + + # Should return the exact path provided + assert result == file_path + + # Doesn't exist + file_path = Path("/nonexistent/directory/output.png") + with pytest.raises( + RuntimeError, + match=r"Cannot reach path .* Are all directories created?", + ): + path_tools.determine_path(file_path, fig_dict, "ext") diff --git a/src/py/tests/test_public_api.py b/src/py/tests/test_public_api.py index 16fc4767..6e0745d2 100644 --- a/src/py/tests/test_public_api.py +++ b/src/py/tests/test_public_api.py @@ -12,7 +12,7 @@ def simple_figure(request): """Create a simple plotly figure for testing, either as figure or dict.""" # ruff: noqa: PLC0415 - import plotly.express as px + import plotly.express as px # type: ignore[import-untyped] fig = px.line(x=[1, 2, 3, 4], y=[1, 2, 3, 4]) @@ -30,7 +30,12 @@ async def test_async_api_functions(simple_figure, tmp_path): # Test write_fig and compare with calc_fig output write_fig_output = tmp_path / "test_write_fig.png" - await kaleido.write_fig(simple_figure, path=str(write_fig_output)) + res = await kaleido.write_fig( + simple_figure, + path=str(write_fig_output), + cancel_on_error=True, + ) + assert res is None with Path(write_fig_output).open("rb") as f: # noqa: ASYNC230 write_fig_bytes = f.read() @@ -40,7 +45,7 @@ async def test_async_api_functions(simple_figure, tmp_path): # Test write_fig_from_object and compare with calc_fig output write_fig_from_object_output = tmp_path / "test_write_fig_from_object.png" - await kaleido.write_fig_from_object( + res = await kaleido.write_fig_from_object( [ { "fig": simple_figure, @@ -48,6 +53,7 @@ async def test_async_api_functions(simple_figure, tmp_path): }, ], ) + assert res == () with Path(write_fig_from_object_output).open("rb") as f: # noqa: ASYNC230 write_fig_from_object_bytes = f.read() @@ -61,7 +67,7 @@ async def test_async_api_functions(simple_figure, tmp_path): assert write_fig_bytes == write_fig_from_object_bytes == calc_result -async def test_sync_api_functions(simple_figure, tmp_path): +async def test_sync_api_functions(simple_figure, tmp_path): # noqa: PLR0915 """Test sync wrappers with cross-validation.""" # Get expected bytes from calc_fig for comparison expected_bytes = await kaleido.calc_fig(simple_figure) @@ -88,7 +94,12 @@ async def test_sync_api_functions(simple_figure, tmp_path): assert calc_result_1 == expected_bytes # Test write_fig_sync - kaleido.write_fig_sync(simple_figure, path=str(write_fig_output_1)) + res = kaleido.write_fig_sync( + simple_figure, + path=str(write_fig_output_1), + cancel_on_error=True, + ) + assert res is None with Path(write_fig_output_1).open("rb") as f: # noqa: ASYNC230 write_fig_bytes_1 = f.read() @@ -96,7 +107,7 @@ async def test_sync_api_functions(simple_figure, tmp_path): assert write_fig_bytes_1.startswith(b"\x89PNG\r\n\x1a\n"), "Not a PNG file" # Test write_fig_from_object_sync - kaleido.write_fig_from_object_sync( + res = kaleido.write_fig_from_object_sync( [ { "fig": simple_figure, @@ -104,6 +115,7 @@ async def test_sync_api_functions(simple_figure, tmp_path): }, ], ) + assert res == () with Path(write_fig_from_object_output_1).open("rb") as f: # noqa: ASYNC230 from_object_bytes_1 = f.read() @@ -140,7 +152,8 @@ async def test_sync_api_functions(simple_figure, tmp_path): assert calc_result_2 == expected_bytes # Test write_fig_sync - kaleido.write_fig_sync(simple_figure, path=str(write_fig_output_2)) + res = kaleido.write_fig_sync(simple_figure, path=str(write_fig_output_2)) + assert res == () with Path(write_fig_output_2).open("rb") as f: # noqa: ASYNC230 write_fig_bytes_2 = f.read() @@ -148,14 +161,16 @@ async def test_sync_api_functions(simple_figure, tmp_path): assert write_fig_bytes_2.startswith(b"\x89PNG\r\n\x1a\n"), "Not a PNG file" # Test write_fig_from_object_sync - kaleido.write_fig_from_object_sync( + res = kaleido.write_fig_from_object_sync( [ { "fig": simple_figure, "path": write_fig_from_object_output_2, }, ], + cancel_on_error=True, ) + assert res is None with Path(write_fig_from_object_output_2).open("rb") as f: # noqa: ASYNC230 from_object_bytes_2 = f.read() diff --git a/src/py/tests/test_utils.py b/src/py/tests/test_utils.py new file mode 100644 index 00000000..0c2e5b07 --- /dev/null +++ b/src/py/tests/test_utils.py @@ -0,0 +1,57 @@ +from pathlib import Path + +import pytest + +from kaleido._utils.path_tools import get_path, is_httpish + +pytestmark = pytest.mark.asyncio(loop_scope="function") + +# ruff: noqa: S108 + + +# Test get_path utility function +async def test_get_path_with_file_uri(): + """Test get_path function with file:// URIs.""" + file_uri = "file:///tmp/test.js" + path_wrapped = Path(file_uri) + result = get_path(file_uri) + assert result == Path("/tmp/test.js") + assert get_path(path_wrapped) is path_wrapped + + +async def test_get_path_with_regular_path(): + """Test get_path function with regular file paths.""" + regular_path = "/tmp/test.js" + path_wrapped = Path(regular_path) + result = get_path(regular_path) + assert result == Path("/tmp/test.js") + assert get_path(path_wrapped) is path_wrapped + + +async def test_get_path_with_http_url(): + """Test get_path function with HTTP URLs.""" + http_url = "https://example.com/test.js" + path_wrapped = Path(http_url) + result = get_path(http_url) + assert result == Path("https://example.com/test.js") + assert get_path(path_wrapped) is path_wrapped + + +# Test is_httpish utility function +async def test_is_httpish_with_http(): + """Test is_httpish function with HTTP URLs.""" + assert is_httpish("http://example.com/test.js") is True + assert is_httpish("https://example.com/test.js") is True + + +async def test_is_httpish_with_file_paths(): + """Test is_httpish function with file paths.""" + assert is_httpish("/tmp/test.js") is False + assert is_httpish("test.js") is False + assert is_httpish("file:///tmp/test.js") is False + + +async def test_is_httpish_with_other_schemes(): + """Test is_httpish function with other URL schemes.""" + assert is_httpish("ftp://example.com/test.js") is False + assert is_httpish("mailto:test@example.com") is False diff --git a/src/py/tests/win_hang_scripts/open.py b/src/py/tests/win_hang_scripts/open.py new file mode 100644 index 00000000..3b2a5d72 --- /dev/null +++ b/src/py/tests/win_hang_scripts/open.py @@ -0,0 +1,5 @@ +import kaleido + + +def test_closing_no_close(): + kaleido.start_sync_server() diff --git a/src/py/tests/win_hang_scripts/open_close.py b/src/py/tests/win_hang_scripts/open_close.py new file mode 100644 index 00000000..ea9eb918 --- /dev/null +++ b/src/py/tests/win_hang_scripts/open_close.py @@ -0,0 +1,6 @@ +import kaleido + + +def test_open_close(): + kaleido.start_sync_server() + kaleido.stop_sync_server() diff --git a/src/py/uv.lock b/src/py/uv.lock index a5bf48ff..e528bd2e 100644 --- a/src/py/uv.lock +++ b/src/py/uv.lock @@ -294,7 +294,7 @@ dependencies = [ { name = "numpy", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pandas", version = "2.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "param", version = "2.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pillow", marker = "python_full_version < '3.9'" }, + { name = "pillow", version = "10.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pyct", marker = "python_full_version < '3.9'" }, { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "scipy", version = "1.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -346,7 +346,7 @@ dependencies = [ { name = "multipledispatch", marker = "python_full_version >= '3.10'" }, { name = "numba", version = "0.62.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging", marker = "python_full_version >= '3.10'" }, { name = "pandas", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "param", version = "2.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -380,7 +380,7 @@ version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -424,7 +424,7 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.138.3" +version = "6.139.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -437,9 +437,9 @@ dependencies = [ { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "sortedcontainers", marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/28/9aa38d1cf2b00d385926fd44318d2b49948c060969ab29e82e8bb654b16c/hypothesis-6.138.3.tar.gz", hash = "sha256:9bffd1382b99e67c46512dac45ec013bae4b39d3d0ef98f0d87535f06d8efc9e", size = 463165, upload-time = "2025-08-24T07:29:16.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/8e/f408b1a6d9745bf02c3d56e0788c930add554eee6b88a39bba141e897ac4/hypothesis-6.139.2.tar.gz", hash = "sha256:2dc2ff36ea977a9cb7fb68f24a5dbf5d673b88a2e502212676eafe09b699f511", size = 466099, upload-time = "2025-09-18T03:29:15.855Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/60/7db4d683d27b24a6dfd8f82ef28332d20b0d99a976ae696569622383c900/hypothesis-6.138.3-py3-none-any.whl", hash = "sha256:19291d3ba478527155c34704b038a21ba86b2f31d36673446f981a67f705b3f4", size = 530081, upload-time = "2025-08-24T07:29:12.862Z" }, + { url = "https://files.pythonhosted.org/packages/7c/81/4a85771072ae39064f114f23716e312771a42bfe3a089cba3da6697dd231/hypothesis-6.139.2-py3-none-any.whl", hash = "sha256:6f466780b7d1db074fb473af14e3111a5dd4fe36c47fcd776cd7c480ae0a02f2", size = 533752, upload-time = "2025-09-18T03:29:12.088Z" }, ] [[package]] @@ -479,7 +479,7 @@ dependencies = [ { name = "choreographer" }, { name = "logistro" }, { name = "orjson", version = "3.10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "orjson", version = "3.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "packaging" }, { name = "pytest-timeout" }, ] @@ -488,30 +488,32 @@ dependencies = [ dev = [ { name = "async-timeout" }, { name = "hypothesis", version = "6.113.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "hypothesis", version = "6.138.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "hypothesis", version = "6.139.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "mypy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mypy", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pandas", version = "2.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pandas", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "plotly", extra = ["express"] }, { name = "poethepoet", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "poethepoet", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyright" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest-asyncio", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-order" }, { name = "pytest-xdist", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest-xdist", version = "3.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] pickles = [ { name = "colorcet" }, { name = "datashader", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "datashader", version = "0.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "datashader", version = "0.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pillow" }, + { name = "pillow", version = "10.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "plotly", extra = ["express"] }, { name = "zstandard", version = "0.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "zstandard", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, @@ -534,6 +536,7 @@ dev = [ { name = "pandas", specifier = ">=2.0.3" }, { name = "plotly", extras = ["express"], specifier = ">=6.1.1" }, { name = "poethepoet", specifier = ">=0.30.0" }, + { name = "pyright", specifier = ">=1.1.406" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-order", specifier = ">=1.3.0" }, @@ -728,7 +731,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.17.1" +version = "1.18.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -740,47 +743,47 @@ dependencies = [ { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, { name = "pathspec", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, - { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, - { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, - { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, - { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, - { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, - { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/490ff491d8ecddf8ab91762d4f67635040202f76a44171420bcbe38ceee5/mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b", size = 12807230, upload-time = "2025-09-19T00:09:49.471Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/60076fc829645d167ece9e80db9e8375648d210dab44cc98beb5b322a826/mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133", size = 11895666, upload-time = "2025-09-19T00:10:53.678Z" }, + { url = "https://files.pythonhosted.org/packages/97/4a/1e2880a2a5dda4dc8d9ecd1a7e7606bc0b0e14813637eeda40c38624e037/mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6", size = 12499608, upload-time = "2025-09-19T00:09:36.204Z" }, + { url = "https://files.pythonhosted.org/packages/00/81/a117f1b73a3015b076b20246b1f341c34a578ebd9662848c6b80ad5c4138/mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac", size = 13244551, upload-time = "2025-09-19T00:10:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/b9f48e1714ce87c7bf0358eb93f60663740ebb08f9ea886ffc670cea7933/mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b", size = 13491552, upload-time = "2025-09-19T00:10:13.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/b2c0af3b684fa80d1b27501a8bdd3d2daa467ea3992a8aa612f5ca17c2db/mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0", size = 9765635, upload-time = "2025-09-19T00:10:30.993Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, ] [[package]] @@ -806,7 +809,7 @@ wheels = [ [[package]] name = "narwhals" -version = "2.1.2" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -814,9 +817,18 @@ resolution-markers = [ "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/37/f0/b0550d9b84759f4d045fd43da2f811e8b23dc2001e38c3254456da7f3adb/narwhals-2.1.2.tar.gz", hash = "sha256:afb9597e76d5b38c2c4b7c37d27a2418b8cc8049a66b8a5aca9581c92ae8f8bf", size = 533772, upload-time = "2025-08-15T08:24:50.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/b8/3cb005704866f1cc19e8d6b15d0467255821ba12d82f20ea15912672e54c/narwhals-2.5.0.tar.gz", hash = "sha256:8ae0b6f39597f14c0dc52afc98949d6f8be89b5af402d2d98101d2f7d3561418", size = 558573, upload-time = "2025-09-12T10:04:24.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5a/22741c5c0e5f6e8050242bfc2052ba68bc94b1735ed5bca35404d136d6ec/narwhals-2.5.0-py3-none-any.whl", hash = "sha256:7e213f9ca7db3f8bf6f7eff35eaee6a1cf80902997e1b78d49b7755775d8f423", size = 407296, upload-time = "2025-09-12T10:04:22.524Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/01/824fff6789ce92a53242d24b6f5f3a982df2f610c51020f934bf878d2a99/narwhals-2.1.2-py3-none-any.whl", hash = "sha256:136b2f533a4eb3245c54254f137c5d14cef5c4668cff67dc6e911a602acd3547", size = 392064, upload-time = "2025-08-15T08:24:48.788Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] @@ -902,7 +914,7 @@ resolution-markers = [ dependencies = [ { name = "llvmlite", version = "0.45.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/96/66dae7911cb331e99bf9afe35703317d8da0fad81ff49fed77f4855e4b60/numba-0.62.0.tar.gz", hash = "sha256:2afcc7899dc93fefecbb274a19c592170bc2dbfae02b00f83e305332a9857a5a", size = 2749680, upload-time = "2025-09-18T17:58:11.394Z" } wheels = [ @@ -1088,87 +1100,87 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.2" +version = "2.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158, upload-time = "2025-07-24T20:24:58.397Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817, upload-time = "2025-07-24T20:25:07.746Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606, upload-time = "2025-07-24T20:25:18.84Z" }, - { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652, upload-time = "2025-07-24T20:25:40.356Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816, upload-time = "2025-07-24T20:26:05.721Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512, upload-time = "2025-07-24T20:26:30.545Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947, upload-time = "2025-07-24T20:26:58.24Z" }, - { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494, upload-time = "2025-07-24T20:27:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889, upload-time = "2025-07-24T20:27:29.558Z" }, - { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560, upload-time = "2025-07-24T20:27:46.803Z" }, - { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, - { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, - { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, - { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, - { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, - { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, - { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, - { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, - { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, - { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, - { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, - { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, - { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, - { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, - { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, - { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, - { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, - { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, - { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, - { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, - { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, - { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, - { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, - { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, - { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, - { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, - { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, - { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, - { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338, upload-time = "2025-07-24T20:57:54.37Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776, upload-time = "2025-07-24T20:58:16.303Z" }, - { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882, upload-time = "2025-07-24T20:58:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405, upload-time = "2025-07-24T20:58:37.341Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651, upload-time = "2025-07-24T20:58:59.048Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166, upload-time = "2025-07-24T21:28:56.38Z" }, - { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/45/e80d203ef6b267aa29b22714fb558930b27960a0c5ce3c19c999232bb3eb/numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", size = 21259253, upload-time = "2025-09-09T15:56:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/cf2c648fccf339e59302e00e5f2bc87725a3ce1992f30f3f78c9044d7c43/numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", size = 14450980, upload-time = "2025-09-09T15:56:05.926Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/9af1082bec870188c42a1c239839915b74a5099c392389ff04215dcee812/numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", size = 5379709, upload-time = "2025-09-09T15:56:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/bfd7abca52bcbf9a4a65abc83fe18ef01ccdeb37bfb28bbd6ad613447c79/numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", size = 6913923, upload-time = "2025-09-09T15:56:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/79/55/d69adad255e87ab7afda1caf93ca997859092afeb697703e2f010f7c2e55/numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", size = 14589591, upload-time = "2025-09-09T15:56:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/10/a2/010b0e27ddeacab7839957d7a8f00e91206e0c2c47abbb5f35a2630e5387/numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", size = 16938714, upload-time = "2025-09-09T15:56:14.637Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6b/12ce8ede632c7126eb2762b9e15e18e204b81725b81f35176eac14dc5b82/numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", size = 16370592, upload-time = "2025-09-09T15:56:17.285Z" }, + { url = "https://files.pythonhosted.org/packages/b4/35/aba8568b2593067bb6a8fe4c52babb23b4c3b9c80e1b49dff03a09925e4a/numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", size = 18884474, upload-time = "2025-09-09T15:56:20.943Z" }, + { url = "https://files.pythonhosted.org/packages/45/fa/7f43ba10c77575e8be7b0138d107e4f44ca4a1ef322cd16980ea3e8b8222/numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57", size = 6599794, upload-time = "2025-09-09T15:56:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a2/a4f78cb2241fe5664a22a10332f2be886dcdea8784c9f6a01c272da9b426/numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa", size = 13088104, upload-time = "2025-09-09T15:56:25.476Z" }, + { url = "https://files.pythonhosted.org/packages/79/64/e424e975adbd38282ebcd4891661965b78783de893b381cbc4832fb9beb2/numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7", size = 10460772, upload-time = "2025-09-09T15:56:27.679Z" }, + { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, + { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, + { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload-time = "2025-09-09T15:56:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload-time = "2025-09-09T15:56:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload-time = "2025-09-09T15:56:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, + { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, + { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" }, + { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, + { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, + { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" }, + { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, + { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, + { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" }, + { url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, + { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, + { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" }, + { url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" }, + { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/7e0a37cfced2644c9563c529f29fa28acbd0960dde32ece683aafa6f4949/numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", size = 21131019, upload-time = "2025-09-09T15:58:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/3291f505297ed63831135a6cc0f474da0c868a1f31b0dd9a9f03a7a0d2ed/numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", size = 14376288, upload-time = "2025-09-09T15:58:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4b/ae02e985bdeee73d7b5abdefeb98aef1207e96d4c0621ee0cf228ddfac3c/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", size = 5305425, upload-time = "2025-09-09T15:58:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/eb/9df215d6d7250db32007941500dc51c48190be25f2401d5b2b564e467247/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", size = 6819053, upload-time = "2025-09-09T15:58:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/57/62/208293d7d6b2a8998a4a1f23ac758648c3c32182d4ce4346062018362e29/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", size = 14420354, upload-time = "2025-09-09T15:58:52.704Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/11/0cc63f9f321ccf63886ac203336777140011fb669e739da36d8db3c53b98/numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", size = 12971844, upload-time = "2025-09-09T15:58:57.359Z" }, ] [[package]] @@ -1262,7 +1274,7 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.2" +version = "3.11.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -1270,90 +1282,90 @@ resolution-markers = [ "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/df/1d/5e0ae38788bdf0721326695e65fdf41405ed535f633eb0df0f06f57552fa/orjson-3.11.2.tar.gz", hash = "sha256:91bdcf5e69a8fd8e8bdb3de32b31ff01d2bd60c1e8d5fe7d5afabdcf19920309", size = 5470739, upload-time = "2025-08-12T15:12:28.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/7b/7aebe925c6b1c46c8606a960fe1d6b681fccd4aaf3f37cd647c3309d6582/orjson-3.11.2-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6b8a78c33496230a60dc9487118c284c15ebdf6724386057239641e1eb69761", size = 226896, upload-time = "2025-08-12T15:10:22.02Z" }, - { url = "https://files.pythonhosted.org/packages/7d/39/c952c9b0d51063e808117dd1e53668a2e4325cc63cfe7df453d853ee8680/orjson-3.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc04036eeae11ad4180d1f7b5faddb5dab1dee49ecd147cd431523869514873b", size = 111845, upload-time = "2025-08-12T15:10:24.963Z" }, - { url = "https://files.pythonhosted.org/packages/f5/dc/90b7f29be38745eeacc30903b693f29fcc1097db0c2a19a71ffb3e9f2a5f/orjson-3.11.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c04325839c5754c253ff301cee8aaed7442d974860a44447bb3be785c411c27", size = 116395, upload-time = "2025-08-12T15:10:26.314Z" }, - { url = "https://files.pythonhosted.org/packages/10/c2/fe84ba63164c22932b8d59b8810e2e58590105293a259e6dd1bfaf3422c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32769e04cd7fdc4a59854376211145a1bbbc0aea5e9d6c9755d3d3c301d7c0df", size = 118768, upload-time = "2025-08-12T15:10:27.605Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ce/d9748ec69b1a4c29b8e2bab8233e8c41c583c69f515b373f1fb00247d8c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff285d14917ea1408a821786e3677c5261fa6095277410409c694b8e7720ae0", size = 120887, upload-time = "2025-08-12T15:10:29.153Z" }, - { url = "https://files.pythonhosted.org/packages/c1/66/b90fac8e4a76e83f981912d7f9524d402b31f6c1b8bff3e498aa321c326c/orjson-3.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2662f908114864b63ff75ffe6ffacf996418dd6cc25e02a72ad4bda81b1ec45a", size = 123650, upload-time = "2025-08-12T15:10:30.602Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/56143898d1689c7f915ac67703efb97e8f2f8d5805ce8c2c3fd0f2bb6e3d/orjson-3.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab463cf5d08ad6623a4dac1badd20e88a5eb4b840050c4812c782e3149fe2334", size = 121287, upload-time = "2025-08-12T15:10:31.868Z" }, - { url = "https://files.pythonhosted.org/packages/80/de/f9c6d00c127be766a3739d0d85b52a7c941e437d8dd4d573e03e98d0f89c/orjson-3.11.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:64414241bde943cbf3c00d45fcb5223dca6d9210148ba984aae6b5d63294502b", size = 119637, upload-time = "2025-08-12T15:10:33.078Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/ab70c7627022d395c1b4eb5badf6196b7144e82b46a3a17ed2354f9e592d/orjson-3.11.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7773e71c0ae8c9660192ff144a3d69df89725325e3d0b6a6bb2c50e5ebaf9b84", size = 392478, upload-time = "2025-08-12T15:10:34.669Z" }, - { url = "https://files.pythonhosted.org/packages/77/91/d890b873b69311db4fae2624c5603c437df9c857fb061e97706dac550a77/orjson-3.11.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:652ca14e283b13ece35bf3a86503c25592f294dbcfc5bb91b20a9c9a62a3d4be", size = 134343, upload-time = "2025-08-12T15:10:35.978Z" }, - { url = "https://files.pythonhosted.org/packages/47/16/1aa248541b4830274a079c4aeb2aa5d1ff17c3f013b1d0d8d16d0848f3de/orjson-3.11.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:26e99e98df8990ecfe3772bbdd7361f602149715c2cbc82e61af89bfad9528a4", size = 123887, upload-time = "2025-08-12T15:10:37.601Z" }, - { url = "https://files.pythonhosted.org/packages/95/e4/7419833c55ac8b5f385d00c02685a260da1f391e900fc5c3e0b797e0d506/orjson-3.11.2-cp310-cp310-win32.whl", hash = "sha256:5814313b3e75a2be7fe6c7958201c16c4560e21a813dbad25920752cecd6ad66", size = 124560, upload-time = "2025-08-12T15:10:38.966Z" }, - { url = "https://files.pythonhosted.org/packages/74/f8/27ca7ef3e194c462af32ce1883187f5ec483650c559166f0de59c4c2c5f0/orjson-3.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc471ce2225ab4c42ca672f70600d46a8b8e28e8d4e536088c1ccdb1d22b35ce", size = 119700, upload-time = "2025-08-12T15:10:40.911Z" }, - { url = "https://files.pythonhosted.org/packages/78/7d/e295df1ac9920cbb19fb4c1afa800e86f175cb657143aa422337270a4782/orjson-3.11.2-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:888b64ef7eaeeff63f773881929434a5834a6a140a63ad45183d59287f07fc6a", size = 226502, upload-time = "2025-08-12T15:10:42.284Z" }, - { url = "https://files.pythonhosted.org/packages/65/21/ffb0f10ea04caf418fb4e7ad1fda4b9ab3179df9d7a33b69420f191aadd5/orjson-3.11.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:83387cc8b26c9fa0ae34d1ea8861a7ae6cff8fb3e346ab53e987d085315a728e", size = 115999, upload-time = "2025-08-12T15:10:43.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/d5/8da1e252ac3353d92e6f754ee0c85027c8a2cda90b6899da2be0df3ef83d/orjson-3.11.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e35f003692c216d7ee901b6b916b5734d6fc4180fcaa44c52081f974c08e17", size = 111563, upload-time = "2025-08-12T15:10:45.301Z" }, - { url = "https://files.pythonhosted.org/packages/4f/81/baabc32e52c570b0e4e1044b1bd2ccbec965e0de3ba2c13082255efa2006/orjson-3.11.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a0a4c29ae90b11d0c00bcc31533854d89f77bde2649ec602f512a7e16e00640", size = 116222, upload-time = "2025-08-12T15:10:46.92Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/da2ad55ad80b49b560dce894c961477d0e76811ee6e614b301de9f2f8728/orjson-3.11.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:585d712b1880f68370108bc5534a257b561672d1592fae54938738fe7f6f1e33", size = 118594, upload-time = "2025-08-12T15:10:48.488Z" }, - { url = "https://files.pythonhosted.org/packages/61/be/014f7eab51449f3c894aa9bbda2707b5340c85650cb7d0db4ec9ae280501/orjson-3.11.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d08e342a7143f8a7c11f1c4033efe81acbd3c98c68ba1b26b96080396019701f", size = 120700, upload-time = "2025-08-12T15:10:49.811Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ae/c217903a30c51341868e2d8c318c59a8413baa35af54d7845071c8ccd6fe/orjson-3.11.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c0f84fc50398773a702732c87cd622737bf11c0721e6db3041ac7802a686fb", size = 123433, upload-time = "2025-08-12T15:10:51.06Z" }, - { url = "https://files.pythonhosted.org/packages/57/c2/b3c346f78b1ff2da310dd300cb0f5d32167f872b4d3bb1ad122c889d97b0/orjson-3.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:140f84e3c8d4c142575898c91e3981000afebf0333df753a90b3435d349a5fe5", size = 121061, upload-time = "2025-08-12T15:10:52.381Z" }, - { url = "https://files.pythonhosted.org/packages/00/c8/c97798f6010327ffc75ad21dd6bca11ea2067d1910777e798c2849f1c68f/orjson-3.11.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96304a2b7235e0f3f2d9363ddccdbfb027d27338722fe469fe656832a017602e", size = 119410, upload-time = "2025-08-12T15:10:53.692Z" }, - { url = "https://files.pythonhosted.org/packages/37/fd/df720f7c0e35694617b7f95598b11a2cb0374661d8389703bea17217da53/orjson-3.11.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d7612bb227d5d9582f1f50a60bd55c64618fc22c4a32825d233a4f2771a428a", size = 392294, upload-time = "2025-08-12T15:10:55.079Z" }, - { url = "https://files.pythonhosted.org/packages/ba/52/0120d18f60ab0fe47531d520372b528a45c9a25dcab500f450374421881c/orjson-3.11.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a134587d18fe493befc2defffef2a8d27cfcada5696cb7234de54a21903ae89a", size = 134134, upload-time = "2025-08-12T15:10:56.568Z" }, - { url = "https://files.pythonhosted.org/packages/ec/10/1f967671966598366de42f07e92b0fc694ffc66eafa4b74131aeca84915f/orjson-3.11.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0b84455e60c4bc12c1e4cbaa5cfc1acdc7775a9da9cec040e17232f4b05458bd", size = 123745, upload-time = "2025-08-12T15:10:57.907Z" }, - { url = "https://files.pythonhosted.org/packages/43/eb/76081238671461cfd0f47e0c24f408ffa66184237d56ef18c33e86abb612/orjson-3.11.2-cp311-cp311-win32.whl", hash = "sha256:f0660efeac223f0731a70884e6914a5f04d613b5ae500744c43f7bf7b78f00f9", size = 124393, upload-time = "2025-08-12T15:10:59.267Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/cc598c1811ba9ba935171267b02e377fc9177489efce525d478a2999d9cc/orjson-3.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:955811c8405251d9e09cbe8606ad8fdef49a451bcf5520095a5ed38c669223d8", size = 119561, upload-time = "2025-08-12T15:11:00.559Z" }, - { url = "https://files.pythonhosted.org/packages/d8/17/c48011750f0489006f7617b0a3cebc8230f36d11a34e7e9aca2085f07792/orjson-3.11.2-cp311-cp311-win_arm64.whl", hash = "sha256:2e4d423a6f838552e3a6d9ec734b729f61f88b1124fd697eab82805ea1a2a97d", size = 114186, upload-time = "2025-08-12T15:11:01.931Z" }, - { url = "https://files.pythonhosted.org/packages/40/02/46054ebe7996a8adee9640dcad7d39d76c2000dc0377efa38e55dc5cbf78/orjson-3.11.2-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:901d80d349d8452162b3aa1afb82cec5bee79a10550660bc21311cc61a4c5486", size = 226528, upload-time = "2025-08-12T15:11:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/6b6f0b4d8aea1137436546b990f71be2cd8bd870aa2f5aa14dba0fcc95dc/orjson-3.11.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:cf3bd3967a360e87ee14ed82cb258b7f18c710dacf3822fb0042a14313a673a1", size = 115931, upload-time = "2025-08-12T15:11:04.759Z" }, - { url = "https://files.pythonhosted.org/packages/ae/05/4205cc97c30e82a293dd0d149b1a89b138ebe76afeca66fc129fa2aa4e6a/orjson-3.11.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26693dde66910078229a943e80eeb99fdce6cd2c26277dc80ead9f3ab97d2131", size = 111382, upload-time = "2025-08-12T15:11:06.468Z" }, - { url = "https://files.pythonhosted.org/packages/50/c7/b8a951a93caa821f9272a7c917115d825ae2e4e8768f5ddf37968ec9de01/orjson-3.11.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad4c8acb50a28211c33fc7ef85ddf5cb18d4636a5205fd3fa2dce0411a0e30c", size = 116271, upload-time = "2025-08-12T15:11:07.845Z" }, - { url = "https://files.pythonhosted.org/packages/17/03/1006c7f8782d5327439e26d9b0ec66500ea7b679d4bbb6b891d2834ab3ee/orjson-3.11.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:994181e7f1725bb5f2d481d7d228738e0743b16bf319ca85c29369c65913df14", size = 119086, upload-time = "2025-08-12T15:11:09.329Z" }, - { url = "https://files.pythonhosted.org/packages/44/61/57d22bc31f36a93878a6f772aea76b2184102c6993dea897656a66d18c74/orjson-3.11.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb79a0476393c07656b69c8e763c3cc925fa8e1d9e9b7d1f626901bb5025448", size = 120724, upload-time = "2025-08-12T15:11:10.674Z" }, - { url = "https://files.pythonhosted.org/packages/78/a9/4550e96b4c490c83aea697d5347b8f7eb188152cd7b5a38001055ca5b379/orjson-3.11.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191ed27a1dddb305083d8716af413d7219f40ec1d4c9b0e977453b4db0d6fb6c", size = 123577, upload-time = "2025-08-12T15:11:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/3a/86/09b8cb3ebd513d708ef0c92d36ac3eebda814c65c72137b0a82d6d688fc4/orjson-3.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0afb89f16f07220183fd00f5f297328ed0a68d8722ad1b0c8dcd95b12bc82804", size = 121195, upload-time = "2025-08-12T15:11:13.399Z" }, - { url = "https://files.pythonhosted.org/packages/37/68/7b40b39ac2c1c644d4644e706d0de6c9999764341cd85f2a9393cb387661/orjson-3.11.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ab6e6b4e93b1573a026b6ec16fca9541354dd58e514b62c558b58554ae04307", size = 119234, upload-time = "2025-08-12T15:11:15.134Z" }, - { url = "https://files.pythonhosted.org/packages/40/7c/bb6e7267cd80c19023d44d8cbc4ea4ed5429fcd4a7eb9950f50305697a28/orjson-3.11.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9cb23527efb61fb75527df55d20ee47989c4ee34e01a9c98ee9ede232abf6219", size = 392250, upload-time = "2025-08-12T15:11:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6730ace05583dbca7c1b406d59f4266e48cd0d360566e71482420fb849fc/orjson-3.11.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a4dd1268e4035af21b8a09e4adf2e61f87ee7bf63b86d7bb0a237ac03fad5b45", size = 134572, upload-time = "2025-08-12T15:11:18.205Z" }, - { url = "https://files.pythonhosted.org/packages/96/0f/7d3e03a30d5aac0432882b539a65b8c02cb6dd4221ddb893babf09c424cc/orjson-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff8b155b145eaf5a9d94d2c476fbe18d6021de93cf36c2ae2c8c5b775763f14e", size = 123869, upload-time = "2025-08-12T15:11:19.554Z" }, - { url = "https://files.pythonhosted.org/packages/45/80/1513265eba6d4a960f078f4b1d2bff94a571ab2d28c6f9835e03dfc65cc6/orjson-3.11.2-cp312-cp312-win32.whl", hash = "sha256:ae3bb10279d57872f9aba68c9931aa71ed3b295fa880f25e68da79e79453f46e", size = 124430, upload-time = "2025-08-12T15:11:20.914Z" }, - { url = "https://files.pythonhosted.org/packages/fb/61/eadf057b68a332351eeb3d89a4cc538d14f31cd8b5ec1b31a280426ccca2/orjson-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:d026e1967239ec11a2559b4146a61d13914504b396f74510a1c4d6b19dfd8732", size = 119598, upload-time = "2025-08-12T15:11:22.372Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3f/7f4b783402143d965ab7e9a2fc116fdb887fe53bdce7d3523271cd106098/orjson-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:59f8d5ad08602711af9589375be98477d70e1d102645430b5a7985fdbf613b36", size = 114052, upload-time = "2025-08-12T15:11:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/0dd6b4750eb556ae4e2c6a9cb3e219ec642e9c6d95f8ebe5dc9020c67204/orjson-3.11.2-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a079fdba7062ab396380eeedb589afb81dc6683f07f528a03b6f7aae420a0219", size = 226419, upload-time = "2025-08-12T15:11:25.517Z" }, - { url = "https://files.pythonhosted.org/packages/44/d5/e67f36277f78f2af8a4690e0c54da6b34169812f807fd1b4bfc4dbcf9558/orjson-3.11.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a5f62ebbc530bb8bb4b1ead103647b395ba523559149b91a6c545f7cd4110ad", size = 115803, upload-time = "2025-08-12T15:11:27.357Z" }, - { url = "https://files.pythonhosted.org/packages/24/37/ff8bc86e0dacc48f07c2b6e20852f230bf4435611bab65e3feae2b61f0ae/orjson-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7df6c7b8b0931feb3420b72838c3e2ba98c228f7aa60d461bc050cf4ca5f7b2", size = 111337, upload-time = "2025-08-12T15:11:28.805Z" }, - { url = "https://files.pythonhosted.org/packages/b9/25/37d4d3e8079ea9784ea1625029988e7f4594ce50d4738b0c1e2bf4a9e201/orjson-3.11.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f59dfea7da1fced6e782bb3699718088b1036cb361f36c6e4dd843c5111aefe", size = 116222, upload-time = "2025-08-12T15:11:30.18Z" }, - { url = "https://files.pythonhosted.org/packages/b7/32/a63fd9c07fce3b4193dcc1afced5dd4b0f3a24e27556604e9482b32189c9/orjson-3.11.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edf49146520fef308c31aa4c45b9925fd9c7584645caca7c0c4217d7900214ae", size = 119020, upload-time = "2025-08-12T15:11:31.59Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b6/400792b8adc3079a6b5d649264a3224d6342436d9fac9a0ed4abc9dc4596/orjson-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50995bbeb5d41a32ad15e023305807f561ac5dcd9bd41a12c8d8d1d2c83e44e6", size = 120721, upload-time = "2025-08-12T15:11:33.035Z" }, - { url = "https://files.pythonhosted.org/packages/40/f3/31ab8f8c699eb9e65af8907889a0b7fef74c1d2b23832719a35da7bb0c58/orjson-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cc42960515076eb639b705f105712b658c525863d89a1704d984b929b0577d1", size = 123574, upload-time = "2025-08-12T15:11:34.433Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a6/ce4287c412dff81878f38d06d2c80845709c60012ca8daf861cb064b4574/orjson-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56777cab2a7b2a8ea687fedafb84b3d7fdafae382165c31a2adf88634c432fa", size = 121225, upload-time = "2025-08-12T15:11:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/69/b0/7a881b2aef4fed0287d2a4fbb029d01ed84fa52b4a68da82bdee5e50598e/orjson-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07349e88025b9b5c783077bf7a9f401ffbfb07fd20e86ec6fc5b7432c28c2c5e", size = 119201, upload-time = "2025-08-12T15:11:37.642Z" }, - { url = "https://files.pythonhosted.org/packages/cf/98/a325726b37f7512ed6338e5e65035c3c6505f4e628b09a5daf0419f054ea/orjson-3.11.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:45841fbb79c96441a8c58aa29ffef570c5df9af91f0f7a9572e5505e12412f15", size = 392193, upload-time = "2025-08-12T15:11:39.153Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4f/a7194f98b0ce1d28190e0c4caa6d091a3fc8d0107ad2209f75c8ba398984/orjson-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13d8d8db6cd8d89d4d4e0f4161acbbb373a4d2a4929e862d1d2119de4aa324ac", size = 134548, upload-time = "2025-08-12T15:11:40.768Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/b84caa2986c3f472dc56343ddb0167797a708a8d5c3be043e1e2677b55df/orjson-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51da1ee2178ed09c00d09c1b953e45846bbc16b6420965eb7a913ba209f606d8", size = 123798, upload-time = "2025-08-12T15:11:42.164Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5b/e398449080ce6b4c8fcadad57e51fa16f65768e1b142ba90b23ac5d10801/orjson-3.11.2-cp313-cp313-win32.whl", hash = "sha256:51dc033df2e4a4c91c0ba4f43247de99b3cbf42ee7a42ee2b2b2f76c8b2f2cb5", size = 124402, upload-time = "2025-08-12T15:11:44.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/429e4608e124debfc4790bfc37131f6958e59510ba3b542d5fc163be8e5f/orjson-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d91d74942b7436f29b5d1ed9bcfc3f6ef2d4f7c4997616509004679936650d", size = 119498, upload-time = "2025-08-12T15:11:45.864Z" }, - { url = "https://files.pythonhosted.org/packages/7b/04/f8b5f317cce7ad3580a9ad12d7e2df0714dfa8a83328ecddd367af802f5b/orjson-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:4ca4fb5ac21cd1e48028d4f708b1bb13e39c42d45614befd2ead004a8bba8535", size = 114051, upload-time = "2025-08-12T15:11:47.555Z" }, - { url = "https://files.pythonhosted.org/packages/74/83/2c363022b26c3c25b3708051a19d12f3374739bb81323f05b284392080c0/orjson-3.11.2-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3dcba7101ea6a8d4ef060746c0f2e7aa8e2453a1012083e1ecce9726d7554cb7", size = 226406, upload-time = "2025-08-12T15:11:49.445Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a7/aa3c973de0b33fc93b4bd71691665ffdfeae589ea9d0625584ab10a7d0f5/orjson-3.11.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:15d17bdb76a142e1f55d91913e012e6e6769659daa6bfef3ef93f11083137e81", size = 115788, upload-time = "2025-08-12T15:11:50.992Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/e45f233dfd09fdbb052ec46352363dca3906618e1a2b264959c18f809d0b/orjson-3.11.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:53c9e81768c69d4b66b8876ec3c8e431c6e13477186d0db1089d82622bccd19f", size = 111318, upload-time = "2025-08-12T15:11:52.495Z" }, - { url = "https://files.pythonhosted.org/packages/3e/23/cf5a73c4da6987204cbbf93167f353ff0c5013f7c5e5ef845d4663a366da/orjson-3.11.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d4f13af59a7b84c1ca6b8a7ab70d608f61f7c44f9740cd42409e6ae7b6c8d8b7", size = 121231, upload-time = "2025-08-12T15:11:53.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/1d/47468a398ae68a60cc21e599144e786e035bb12829cb587299ecebc088f1/orjson-3.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bde64aa469b5ee46cc960ed241fae3721d6a8801dacb2ca3466547a2535951e4", size = 119204, upload-time = "2025-08-12T15:11:55.409Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d9/f99433d89b288b5bc8836bffb32a643f805e673cf840ef8bab6e73ced0d1/orjson-3.11.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b5ca86300aeb383c8fa759566aca065878d3d98c3389d769b43f0a2e84d52c5f", size = 392237, upload-time = "2025-08-12T15:11:57.18Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dc/1b9d80d40cebef603325623405136a29fb7d08c877a728c0943dd066c29a/orjson-3.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24e32a558ebed73a6a71c8f1cbc163a7dd5132da5270ff3d8eeb727f4b6d1bc7", size = 134578, upload-time = "2025-08-12T15:11:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/72e7a4c5b6485ef4e83ef6aba7f1dd041002bad3eb5d1d106ca5b0fc02c6/orjson-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e36319a5d15b97e4344110517450396845cc6789aed712b1fbf83c1bd95792f6", size = 123799, upload-time = "2025-08-12T15:12:00.352Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/a3d76b392e7acf9b34dc277171aad85efd6accc75089bb35b4c614990ea9/orjson-3.11.2-cp314-cp314-win32.whl", hash = "sha256:40193ada63fab25e35703454d65b6afc71dbc65f20041cb46c6d91709141ef7f", size = 124461, upload-time = "2025-08-12T15:12:01.854Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/75c6a596ff8df9e4a5894813ff56695f0a218e6ea99420b4a645c4f7795d/orjson-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7c8ac5f6b682d3494217085cf04dadae66efee45349ad4ee2a1da3c97e2305a8", size = 119494, upload-time = "2025-08-12T15:12:03.337Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3d/9e74742fc261c5ca473c96bb3344d03995869e1dc6402772c60afb97736a/orjson-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:21cf261e8e79284242e4cb1e5924df16ae28255184aafeff19be1405f6d33f67", size = 114046, upload-time = "2025-08-12T15:12:04.87Z" }, - { url = "https://files.pythonhosted.org/packages/4f/08/8ebc6dcac0938376b7e61dff432c33958505ae4c185dda3fa1e6f46ac40b/orjson-3.11.2-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:957f10c7b5bce3d3f2ad577f3b307c784f5dabafcce3b836229c269c11841c86", size = 226498, upload-time = "2025-08-12T15:12:06.51Z" }, - { url = "https://files.pythonhosted.org/packages/ff/74/a97c8e2bc75a27dfeeb1b289645053f1889125447f3b7484a2e34ac55d2a/orjson-3.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a669e31ab8eb466c9142ac7a4be2bb2758ad236a31ef40dcd4cf8774ab40f33", size = 111529, upload-time = "2025-08-12T15:12:08.21Z" }, - { url = "https://files.pythonhosted.org/packages/78/c3/55121b5722a1a4e4610a411866cfeada5314dc498cd42435b590353009d2/orjson-3.11.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:adedf7d887416c51ad49de3c53b111887e0b63db36c6eb9f846a8430952303d8", size = 116213, upload-time = "2025-08-12T15:12:09.776Z" }, - { url = "https://files.pythonhosted.org/packages/54/d3/1c810fa36a749157f1ec68f825b09d5b6958ed5eaf66c7b89bc0f1656517/orjson-3.11.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ad8873979659ad98fc56377b9c5b93eb8059bf01e6412f7abf7dbb3d637a991", size = 118594, upload-time = "2025-08-12T15:12:11.363Z" }, - { url = "https://files.pythonhosted.org/packages/09/9c/052a6619857aba27899246c1ac9e1566fe976dbb48c2d2d177eb269e6d92/orjson-3.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9482ef83b2bf796157566dd2d2742a8a1e377045fe6065fa67acb1cb1d21d9a3", size = 120706, upload-time = "2025-08-12T15:12:13.265Z" }, - { url = "https://files.pythonhosted.org/packages/4b/91/ed0632b8bafa5534d40483ca14f4b7b7e8f27a016f52ff771420b3591574/orjson-3.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73cee7867c1fcbd1cc5b6688b3e13db067f968889242955780123a68b3d03316", size = 123412, upload-time = "2025-08-12T15:12:14.807Z" }, - { url = "https://files.pythonhosted.org/packages/90/3d/058184ae52a2035098939329f8864c5e28c3bbd660f80d4f687f4fd3e629/orjson-3.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:465166773265f3cc25db10199f5d11c81898a309e26a2481acf33ddbec433fda", size = 121011, upload-time = "2025-08-12T15:12:16.352Z" }, - { url = "https://files.pythonhosted.org/packages/57/ab/70e7a2c26a29878ad81ac551f3d11e184efafeed92c2ea15301ac71e2b44/orjson-3.11.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc000190a7b1d2d8e36cba990b3209a1e15c0efb6c7750e87f8bead01afc0d46", size = 119387, upload-time = "2025-08-12T15:12:17.88Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f1/532be344579590c2faa3d9926ec446e8e030d6d04359a8d6f9b3f4d18283/orjson-3.11.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:df3fdd8efa842ccbb81135d6f58a73512f11dba02ed08d9466261c2e9417af4e", size = 392280, upload-time = "2025-08-12T15:12:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/eb/90/dfb90d82ee7447ba0c5315b1012f36336d34a4b468f5896092926eb2921b/orjson-3.11.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3dacfc621be3079ec69e0d4cb32e3764067726e0ef5a5576428f68b6dc85b4f6", size = 134127, upload-time = "2025-08-12T15:12:22.053Z" }, - { url = "https://files.pythonhosted.org/packages/17/cb/d113d03dfaee4933b0f6e0f3d358886db1468302bb74f1f3c59d9229ce12/orjson-3.11.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fdff73a029cde5f4a1cf5ec9dbc6acab98c9ddd69f5580c2b3f02ce43ba9f9f", size = 123722, upload-time = "2025-08-12T15:12:23.642Z" }, - { url = "https://files.pythonhosted.org/packages/55/78/a89748f500d7cf909fe0b30093ab87d256c279106048e985269a5530c0a1/orjson-3.11.2-cp39-cp39-win32.whl", hash = "sha256:b1efbdc479c6451138c3733e415b4d0e16526644e54e2f3689f699c4cda303bf", size = 124391, upload-time = "2025-08-12T15:12:25.143Z" }, - { url = "https://files.pythonhosted.org/packages/e8/50/e436f1356650cf96ff62c386dbfeb9ef8dd9cd30c4296103244e7fae2d15/orjson-3.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:c9ec0cc0d4308cad1e38a1ee23b64567e2ff364c2a3fe3d6cbc69cf911c45712", size = 119547, upload-time = "2025-08-12T15:12:26.77Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/64/4a3cef001c6cd9c64256348d4c13a7b09b857e3e1cbb5185917df67d8ced/orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7", size = 238600, upload-time = "2025-08-26T17:44:36.875Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/0c8c87f54f79d051485903dc46226c4d3220b691a151769156054df4562b/orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120", size = 123526, upload-time = "2025-08-26T17:44:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/249497e861f2d438f45b3ab7b7b361484237414945169aa285608f9f7019/orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467", size = 128075, upload-time = "2025-08-26T17:44:40.672Z" }, + { url = "https://files.pythonhosted.org/packages/e5/64/00485702f640a0fd56144042a1ea196469f4a3ae93681871564bf74fa996/orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873", size = 130483, upload-time = "2025-08-26T17:44:41.788Z" }, + { url = "https://files.pythonhosted.org/packages/64/81/110d68dba3909171bf3f05619ad0cf187b430e64045ae4e0aa7ccfe25b15/orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a", size = 132539, upload-time = "2025-08-26T17:44:43.12Z" }, + { url = "https://files.pythonhosted.org/packages/79/92/dba25c22b0ddfafa1e6516a780a00abac28d49f49e7202eb433a53c3e94e/orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b", size = 135390, upload-time = "2025-08-26T17:44:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/1d/ca2230fd55edbd87b58a43a19032d63a4b180389a97520cc62c535b726f9/orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf", size = 132966, upload-time = "2025-08-26T17:44:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b9/96bbc8ed3e47e52b487d504bd6861798977445fbc410da6e87e302dc632d/orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4", size = 131349, upload-time = "2025-08-26T17:44:46.862Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3c/418fbd93d94b0df71cddf96b7fe5894d64a5d890b453ac365120daec30f7/orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc", size = 404087, upload-time = "2025-08-26T17:44:48.079Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a9/2bfd58817d736c2f63608dec0c34857339d423eeed30099b126562822191/orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569", size = 146067, upload-time = "2025-08-26T17:44:49.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/ba/29023771f334096f564e48d82ed855a0ed3320389d6748a9c949e25be734/orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6", size = 135506, upload-time = "2025-08-26T17:44:50.558Z" }, + { url = "https://files.pythonhosted.org/packages/39/62/b5a1eca83f54cb3aa11a9645b8a22f08d97dbd13f27f83aae7c6666a0a05/orjson-3.11.3-cp310-cp310-win32.whl", hash = "sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc", size = 136352, upload-time = "2025-08-26T17:44:51.698Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c0/7ebfaa327d9a9ed982adc0d9420dbce9a3fec45b60ab32c6308f731333fa/orjson-3.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770", size = 131539, upload-time = "2025-08-26T17:44:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/360674cd817faef32e49276187922a946468579fcaf37afdfb6c07046e92/orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f", size = 238238, upload-time = "2025-08-26T17:44:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/05/3d/5fa9ea4b34c1a13be7d9046ba98d06e6feb1d8853718992954ab59d16625/orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91", size = 127713, upload-time = "2025-08-26T17:44:55.596Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/e18367823925e00b1feec867ff5f040055892fc474bf5f7875649ecfa586/orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904", size = 123241, upload-time = "2025-08-26T17:44:57.185Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bd/3c66b91c4564759cf9f473251ac1650e446c7ba92a7c0f9f56ed54f9f0e6/orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6", size = 127895, upload-time = "2025-08-26T17:44:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/82/b5/dc8dcd609db4766e2967a85f63296c59d4722b39503e5b0bf7fd340d387f/orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d", size = 130303, upload-time = "2025-08-26T17:44:59.491Z" }, + { url = "https://files.pythonhosted.org/packages/48/c2/d58ec5fd1270b2aa44c862171891adc2e1241bd7dab26c8f46eb97c6c6f1/orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038", size = 132366, upload-time = "2025-08-26T17:45:00.654Z" }, + { url = "https://files.pythonhosted.org/packages/73/87/0ef7e22eb8dd1ef940bfe3b9e441db519e692d62ed1aae365406a16d23d0/orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb", size = 135180, upload-time = "2025-08-26T17:45:02.424Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/e5bf7b70883f374710ad74faf99bacfc4b5b5a7797c1d5e130350e0e28a3/orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2", size = 132741, upload-time = "2025-08-26T17:45:03.663Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0c/4577fd860b6386ffaa56440e792af01c7882b56d2766f55384b5b0e9d39b/orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55", size = 131104, upload-time = "2025-08-26T17:45:04.939Z" }, + { url = "https://files.pythonhosted.org/packages/66/4b/83e92b2d67e86d1c33f2ea9411742a714a26de63641b082bdbf3d8e481af/orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1", size = 403887, upload-time = "2025-08-26T17:45:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e5/9eea6a14e9b5ceb4a271a1fd2e1dec5f2f686755c0fab6673dc6ff3433f4/orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824", size = 145855, upload-time = "2025-08-26T17:45:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/8d4f5ad0c80ba9bf8ac4d0fc71f93a7d0dc0844989e645e2074af376c307/orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f", size = 135361, upload-time = "2025-08-26T17:45:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5f/16386970370178d7a9b438517ea3d704efcf163d286422bae3b37b88dbb5/orjson-3.11.3-cp311-cp311-win32.whl", hash = "sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204", size = 136190, upload-time = "2025-08-26T17:45:10.962Z" }, + { url = "https://files.pythonhosted.org/packages/09/60/db16c6f7a41dd8ac9fb651f66701ff2aeb499ad9ebc15853a26c7c152448/orjson-3.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b", size = 131389, upload-time = "2025-08-26T17:45:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2a/bb811ad336667041dea9b8565c7c9faf2f59b47eb5ab680315eea612ef2e/orjson-3.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e", size = 126120, upload-time = "2025-08-26T17:45:13.515Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" }, + { url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" }, + { url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" }, + { url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", size = 136248, upload-time = "2025-08-26T17:45:32.567Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", size = 131437, upload-time = "2025-08-26T17:45:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", size = 125978, upload-time = "2025-08-26T17:45:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, + { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, + { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" }, + { url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" }, + { url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", size = 136266, upload-time = "2025-08-26T17:46:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", size = 131351, upload-time = "2025-08-26T17:46:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", size = 125985, upload-time = "2025-08-26T17:46:16.67Z" }, + { url = "https://files.pythonhosted.org/packages/99/a6/18d88ccf8e5d8f711310eba9b4f6562f4aa9d594258efdc4dcf8c1550090/orjson-3.11.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c", size = 238221, upload-time = "2025-08-26T17:46:18.113Z" }, + { url = "https://files.pythonhosted.org/packages/ee/18/e210365a17bf984c89db40c8be65da164b4ce6a866a2a0ae1d6407c2630b/orjson-3.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa", size = 123209, upload-time = "2025-08-26T17:46:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/26/43/6b3f8ec15fa910726ed94bd2e618f86313ad1cae7c3c8c6b9b8a3a161814/orjson-3.11.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045", size = 127881, upload-time = "2025-08-26T17:46:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ed/f41d2406355ce67efdd4ab504732b27bea37b7dbdab3eb86314fe764f1b9/orjson-3.11.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f", size = 130306, upload-time = "2025-08-26T17:46:22.914Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a1/1be02950f92c82e64602d3d284bd76d9fc82a6b92c9ce2a387e57a825a11/orjson-3.11.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de", size = 132383, upload-time = "2025-08-26T17:46:24.33Z" }, + { url = "https://files.pythonhosted.org/packages/39/49/46766ac00c68192b516a15ffc44c2a9789ca3468b8dc8a500422d99bf0dd/orjson-3.11.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f", size = 135159, upload-time = "2025-08-26T17:46:25.741Z" }, + { url = "https://files.pythonhosted.org/packages/47/e1/27fd5e7600fdd82996329d48ee56f6e9e9ae4d31eadbc7f93fd2ff0d8214/orjson-3.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f", size = 132690, upload-time = "2025-08-26T17:46:27.271Z" }, + { url = "https://files.pythonhosted.org/packages/d8/21/f57ef08799a68c36ef96fe561101afeef735caa80814636b2e18c234e405/orjson-3.11.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2", size = 131086, upload-time = "2025-08-26T17:46:33.067Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/a3a24306a9dc482e929232c65f5b8c69188136edd6005441d8cc4754f7ea/orjson-3.11.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a", size = 403884, upload-time = "2025-08-26T17:46:34.55Z" }, + { url = "https://files.pythonhosted.org/packages/11/98/fdae5b2c28bc358e6868e54c8eca7398c93d6a511f0436b61436ad1b04dc/orjson-3.11.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7", size = 145837, upload-time = "2025-08-26T17:46:36.46Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a9/2fe5cd69ed231f3ed88b1ad36a6957e3d2c876eb4b2c6b17b8ae0a6681fc/orjson-3.11.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7", size = 135325, upload-time = "2025-08-26T17:46:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/7d4c8aefb45f6c8d7d527d84559a3a7e394b9fd1d424a2b5bcaf75fa68e7/orjson-3.11.3-cp39-cp39-win32.whl", hash = "sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225", size = 136184, upload-time = "2025-08-26T17:46:39.542Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1f/1d6a24d22001e96c0afcf1806b6eabee1109aebd2ef20ec6698f6a6012d7/orjson-3.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb", size = 131373, upload-time = "2025-08-26T17:46:41.227Z" }, ] [[package]] @@ -1419,7 +1431,7 @@ resolution-markers = [ dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil", marker = "python_full_version >= '3.9'" }, { name = "pytz", marker = "python_full_version >= '3.9'" }, { name = "tzdata", marker = "python_full_version >= '3.9'" }, @@ -1531,6 +1543,9 @@ wheels = [ name = "pillow" version = "10.4.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, @@ -1614,13 +1629,132 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/ae/2dbfc38cc4fd14aceea14bc440d5151b21f64c4c3ba3f6f4191610b7ee5d/pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", size = 2554652, upload-time = "2024-07-01T09:48:38.789Z" }, ] +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + [[package]] name = "plotly" version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals", version = "1.42.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "narwhals", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "narwhals", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/64/850de5076f4436410e1ce4f6a69f4313ef6215dfea155f3f6559335cad29/plotly-6.3.0.tar.gz", hash = "sha256:8840a184d18ccae0f9189c2b9a2943923fd5cae7717b723f36eef78f444e5a73", size = 6923926, upload-time = "2025-08-12T20:22:14.127Z" } @@ -1633,7 +1767,7 @@ express = [ { name = "numpy", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [[package]] @@ -1731,6 +1865,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyright" +version = "1.1.406" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/6b4fbdd1fef59a0292cbb99f790b44983e390321eccbc5921b4d161da5d1/pyright-1.1.406.tar.gz", hash = "sha256:c4872bc58c9643dac09e8a2e74d472c62036910b3bd37a32813989ef7576ea2c", size = 4113151, upload-time = "2025-10-02T01:04:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/a2/e309afbb459f50507103793aaef85ca4348b66814c86bc73908bdeb66d12/pyright-1.1.406-py3-none-any.whl", hash = "sha256:1d81fb43c2407bf566e97e57abb01c811973fdb21b2df8df59f870f688bdca71", size = 5980982, upload-time = "2025-10-02T01:04:43.137Z" }, +] + [[package]] name = "pytest" version = "8.3.5" @@ -1753,7 +1901,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -1770,9 +1918,9 @@ dependencies = [ { name = "pygments", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] @@ -1792,7 +1940,7 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -1802,12 +1950,12 @@ resolution-markers = [ ] dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] [[package]] @@ -1816,7 +1964,7 @@ version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/66/02ae17461b14a52ce5a29ae2900156b9110d1de34721ccc16ccd79419876/pytest_order-1.3.0.tar.gz", hash = "sha256:51608fec3d3ee9c0adaea94daa124a5c4c1d2bb99b00269f098f414307f23dde", size = 47544, upload-time = "2024-08-22T12:29:54.512Z" } wheels = [ @@ -1829,7 +1977,7 @@ version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } wheels = [ @@ -1864,7 +2012,7 @@ resolution-markers = [ ] dependencies = [ { name = "execnet", marker = "python_full_version >= '3.9'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ @@ -2131,7 +2279,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } wheels = [ @@ -2364,7 +2512,7 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -2372,9 +2520,9 @@ resolution-markers = [ "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +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/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { 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" }, ] [[package]] @@ -2473,7 +2621,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging", marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ]