diff --git a/README.md b/README.md index da67f51..75c7377 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,49 @@ for child in p.parent.iterdir(): # yields synchronously Local paths (`file://` or bare `/path`) bypass the proxy entirely and use `pathlib.Path` directly — zero threading overhead. +### Large uploads stream automatically + +Writing or copying a large object never buffers the whole thing in memory — it +streams in bounded-size parts using each backend's native mechanism (S3 +multipart, Azure block blobs, GCS resumable, SSH chunked writes, FTP data +connection, Artifactory streamed `PUT`). This is the **default** behaviour — no +flag, no API change: + +```python +# open("wb") spills to disk past 16 MiB and uploads in parts on close +async with AsAnyPath("s3://bucket/huge.bin").open("wb") as f: + f.write(data) + +# local -> cloud copy streams straight from the source file (async and sync) +await AsAnyPath("/data/movie.mp4").copy("s3://bucket/movie.mp4") +``` + +Peak memory per upload is one part — **8 MiB by default**. Objects smaller than +one part upload in a single request. (`write_bytes(data)` / `write_text(...)` +take the whole payload in memory by definition — use `open("wb")` or `copy()` +for large data.) + +**Tuning the memory footprint.** The part size *is* the max RAM held per +in-flight chunk (parts upload sequentially, so there is no concurrency +multiplier). Set it globally via env, or per call: + +```bash +export ASANYPATH_UPLOAD_CHUNK_SIZE=33554432 # 32 MiB parts (bytes) +export ASANYPATH_SPOOL_MAX_SIZE=67108864 # 64 MiB before open("wb") spills to disk +``` + +```python +# per call: copy(chunk_size=...) or, for writes, open(..., buffering=...) +await src.copy("s3://bucket/huge.bin", chunk_size=32 * 1024 * 1024) +async with dst.open("wb", buffering=32 * 1024 * 1024) as f: + f.write(data) +``` + +Bigger parts mean fewer requests and a larger single-object ceiling (S3 allows +10,000 parts, so 8 MiB parts cap an object at ~80 GB). Backend minimums are +applied automatically: **S3** clamps to ≥ 5 MiB parts, **GCS** rounds down to a +256 KiB multiple. + Unknown URI schemes now construct as `UnsupportedProtocolPath` placeholders. Pure path operations (e.g. `.name`, `.parent`, joins) still work, while backend operations (e.g. `.exists()`, `.read_bytes()`, `.open()`) raise @@ -383,8 +426,8 @@ Factory that returns the appropriate path instance based on the URI scheme. All path implementations support: -- `open(mode, buffering)` — file-like streaming with lazy range reads -- `copy(dst, recursive)` — copy file or tree (cross-backend) +- `open(mode, buffering)` — file-like streaming with lazy range reads; for writes, `buffering` sets the upload part size +- `copy(dst, recursive, chunk_size)` — copy file or tree (cross-backend); `chunk_size` tunes the upload part size - `read_bytes()` / `write_bytes(data)` - `read_text(encoding)` / `write_text(data, encoding)` - `exists()` / `is_file()` / `is_dir()` diff --git a/pyproject.toml b/pyproject.toml index 58dbe3b..71ebbae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,7 @@ disable_error_code = ["attr-defined", "arg-type", "assignment"] module = "asanypath.local" disallow_untyped_defs = false warn_return_any = false -disable_error_code = ["type-var", "misc", "override", "return-value"] +disable_error_code = ["type-var", "misc", "override", "return-value", "assignment", "no-redef"] [[tool.mypy.overrides]] module = "asanypath.common" @@ -120,19 +120,31 @@ disable_error_code = ["override", "type-arg", "return-value", "assignment", "att module = ["asanypath.s3", "asanypath.gcs", "asanypath.azure", "asanypath.artifactory"] disallow_untyped_defs = false warn_return_any = false -disable_error_code = ["no-any-return", "attr-defined", "assignment", "override"] +disable_error_code = ["no-any-return", "attr-defined", "assignment", "override", "arg-type", "misc"] + +[[tool.mypy.overrides]] +module = ["asanypath.ssh", "asanypath.ftp"] +disallow_untyped_defs = false +warn_return_any = false +disable_error_code = ["attr-defined", "override", "arg-type", "misc", "no-any-return", "assignment"] + +[[tool.mypy.overrides]] +module = "asanypath.sync" +disallow_untyped_defs = false +warn_return_any = false +disable_error_code = ["no-any-return", "misc", "attr-defined", "arg-type"] [[tool.mypy.overrides]] module = "asanypath.cloud" disallow_untyped_defs = false warn_return_any = false -disable_error_code = ["no-any-return", "arg-type"] +disable_error_code = ["no-any-return", "arg-type", "attr-defined", "misc", "assignment"] [[tool.mypy.overrides]] module = "asanypath.asanypath" disallow_untyped_defs = false warn_return_any = false -disable_error_code = ["no-any-return", "misc", "union-attr", "type-var"] +disable_error_code = ["no-any-return", "misc", "union-attr", "type-var", "attr-defined"] [[tool.mypy.overrides]] module = "asanypath.batcher" diff --git a/src/asanypath/artifactory.py b/src/asanypath/artifactory.py index 8000a0f..b0c9c9b 100644 --- a/src/asanypath/artifactory.py +++ b/src/asanypath/artifactory.py @@ -39,7 +39,13 @@ ) from asanypath.cloud import CloudPathMixin -from asanypath.options import AccessGrant, AccessPolicy, AccessPolicyPatch, BackendOptions +from asanypath.options import ( + UPLOAD_CHUNK_SIZE, + AccessGrant, + AccessPolicy, + AccessPolicyPatch, + BackendOptions, +) def _jfrog_conf_token(base_url: str) -> str | None: @@ -99,9 +105,9 @@ async def main(): protocol: str = "art" _supports_range_read: bool = True - # Artifactory has no multipart API: large objects stream a single PUT body - # from disk (bounded memory) instead of buffering the whole object. - _STREAM_THRESHOLD = 8 * 1024 * 1024 + # Artifactory has no multipart API: objects at/above this size stream a single + # PUT body from disk (bounded memory) instead of buffering the whole object. + _STREAM_THRESHOLD = UPLOAD_CHUNK_SIZE _copy_batch_fn = staticmethod(art_copy_batch) @staticmethod @@ -201,9 +207,11 @@ def _native_kwargs(self) -> dict: # Backend-specific operations # ------------------------------------------------------------------ - async def _upload_buffer(self, fileobj, size: int, *, backend_options=None) -> None: + async def _upload_buffer( + self, fileobj, size: int, *, backend_options=None, chunk_size: int | None = None + ) -> None: """Upload a spooled write buffer; large objects stream a single PUT from disk.""" - if size < self._STREAM_THRESHOLD: + if size < (chunk_size or self._STREAM_THRESHOLD): data = fileobj.read() if backend_options is None: await self.write_bytes(data) diff --git a/src/asanypath/azure.py b/src/asanypath/azure.py index 15e02fd..7ed382d 100644 --- a/src/asanypath/azure.py +++ b/src/asanypath/azure.py @@ -38,7 +38,13 @@ ) from asanypath.cloud import CloudPathMixin -from asanypath.options import AccessGrant, AccessPolicy, AccessPolicyPatch, BackendOptions +from asanypath.options import ( + UPLOAD_CHUNK_SIZE, + AccessGrant, + AccessPolicy, + AccessPolicyPatch, + BackendOptions, +) AZURE_API_VERSION = "2023-11-03" @@ -85,9 +91,8 @@ class AzurePath(CloudPathMixin): protocol: str = "az" _supports_range_read: bool = True - # Single PUT below the threshold, else staged block-blob parts. - _MULTIPART_THRESHOLD = 8 * 1024 * 1024 - _MULTIPART_PART_SIZE = 8 * 1024 * 1024 + # Bytes per staged block; objects below one block upload in a single PUT. + _MULTIPART_PART_SIZE = UPLOAD_CHUNK_SIZE _is_dir_batch_fn = staticmethod(az_is_dir_batch) _copy_batch_fn = staticmethod(az_copy_batch) @@ -186,18 +191,21 @@ def _native_kwargs(self) -> dict: # Backend-specific operations # ------------------------------------------------------------------ - async def _upload_buffer(self, fileobj, size: int, *, backend_options=None) -> None: + async def _upload_buffer( + self, fileobj, size: int, *, backend_options=None, chunk_size: int | None = None + ) -> None: """Upload a spooled write buffer, using block blobs for large objects.""" - if size < self._MULTIPART_THRESHOLD: + part = chunk_size or self._MULTIPART_PART_SIZE + if size < part: data = fileobj.read() if backend_options is None: await self.write_bytes(data) else: await self.write_bytes(data, backend_options=backend_options) return - await self._block_upload(fileobj, backend_options=backend_options) + await self._block_upload(fileobj, part, backend_options=backend_options) - async def _block_upload(self, fileobj, *, backend_options=None) -> None: + async def _block_upload(self, fileobj, part_size: int, *, backend_options=None) -> None: """Stage fixed-size blocks then commit the list (memory bounded to one block).""" options_json = ( msgspec.json.encode(backend_options).decode() if backend_options is not None else "{}" @@ -205,7 +213,7 @@ async def _block_upload(self, fileobj, *, backend_options=None) -> None: block_ids: list[str] = [] index = 0 while True: - chunk = fileobj.read(self._MULTIPART_PART_SIZE) + chunk = fileobj.read(part_size) if not chunk: break # Block ids must be equal-length base64 strings. diff --git a/src/asanypath/cloud.py b/src/asanypath/cloud.py index 73e0d9f..b604fb9 100644 --- a/src/asanypath/cloud.py +++ b/src/asanypath/cloud.py @@ -35,7 +35,7 @@ ) from asanypath.batcher import H2_BATCH_THRESHOLD, MicroBatcher, make_batcher from asanypath.common import SCHEME_SEP, CommonPurePathMixin -from asanypath.options import AccessPolicy, AccessPolicyPatch, BackendOptions +from asanypath.options import SPOOL_MAX_SIZE, AccessPolicy, AccessPolicyPatch, BackendOptions class CloudPathMixin(CommonPurePathMixin): @@ -354,12 +354,18 @@ async def write_text( return await self.write_bytes(encoded, backend_options=backend_options) async def _upload_buffer( - self, fileobj: IO[bytes], size: int, *, backend_options: BackendOptions | None = None + self, + fileobj: IO[bytes], + size: int, + *, + backend_options: BackendOptions | None = None, + chunk_size: int | None = None, ) -> None: """Upload a spooled write buffer (default: a single PUT of the whole buffer). Backends with native multipart override this to bound memory for large objects; ``size`` is the byte length and ``fileobj`` is positioned at 0. + ``chunk_size`` tunes the part size for those backends (ignored here). """ data = fileobj.read() if backend_options is None: @@ -989,7 +995,7 @@ class _CloudFile: _DEFAULT_CHUNK = 8 * 1024 * 1024 # 8 MiB — good default for cloud latency # Write buffer spills to disk past this size so many concurrently-open write # handles (e.g. sharded writers) can't accumulate whole objects in memory. - _SPOOL_MAX_SIZE = 16 * 1024 * 1024 # 16 MiB resident per open write handle + _SPOOL_MAX_SIZE = SPOOL_MAX_SIZE # resident bytes per open write handle before spilling def __init__(self, path, mode, buffering, encoding, errors, newline, backend_options=None): self._path = path @@ -1012,6 +1018,11 @@ def _chunk_size(self) -> int: return self._DEFAULT_CHUNK return self._buffering + @property + def _write_chunk_size(self) -> int | None: + """Write-mode: a positive ``buffering`` sets the upload part size.""" + return self._buffering if self._buffering and self._buffering > 0 else None + def _make_buffer(self, data: bytes | None = None) -> io.IOBase: """Create the backing buffer: in-memory for reads, disk-spilling for writes.""" if "r" in self._mode: @@ -1100,7 +1111,12 @@ def __exit__(self, exc_type, exc_val, exc_tb): if ("w" in self._mode or "a" in self._mode) and exc_type is None: buf, size = self._spool_for_upload() _SyncRunner.get().run( - self._path._upload_buffer(buf, size, backend_options=self._backend_options) + self._path._upload_buffer( + buf, + size, + backend_options=self._backend_options, + chunk_size=self._write_chunk_size, + ) ) finally: if self._reader is not None: @@ -1137,7 +1153,12 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): try: if ("w" in self._mode or "a" in self._mode) and exc_type is None: buf, size = self._spool_for_upload() - await self._path._upload_buffer(buf, size, backend_options=self._backend_options) + await self._path._upload_buffer( + buf, + size, + backend_options=self._backend_options, + chunk_size=self._write_chunk_size, + ) finally: if self._reader is not None: self._reader.close() diff --git a/src/asanypath/ftp.py b/src/asanypath/ftp.py index a4459a7..69f9b19 100644 --- a/src/asanypath/ftp.py +++ b/src/asanypath/ftp.py @@ -59,12 +59,19 @@ from datetime import datetime, timezone from os import getenv from types import SimpleNamespace -from typing import IO, TYPE_CHECKING, TypeVar +from typing import IO, TYPE_CHECKING, TypeVar, cast import aioftp +from yarl import URL from asanypath.cloud import CloudPathMixin -from asanypath.options import AccessGrant, AccessPolicy, AccessPolicyPatch, BackendOptions +from asanypath.options import ( + UPLOAD_CHUNK_SIZE, + AccessGrant, + AccessPolicy, + AccessPolicyPatch, + BackendOptions, +) if TYPE_CHECKING: from typing import Self @@ -155,10 +162,8 @@ class FTPPath(CloudPathMixin): _tls: bool = False _default_port: int = 21 _supports_range_read: bool = False - # Large objects stream to the data connection in chunks instead of buffering - # the whole payload in memory. - _STREAM_THRESHOLD = 8 * 1024 * 1024 - _STREAM_CHUNK = 1024 * 1024 + # Bytes per streamed read; objects below this write in a single call. + _STREAM_CHUNK = UPLOAD_CHUNK_SIZE def __init__( self, @@ -171,10 +176,11 @@ def __init__( ) -> None: super().__init__(*parts) cfg = self.env_config - self._host = host or self._path.host or cfg.host - self._port = port or self._path.port or cfg.port - url_pw = self._path.password - url_user = self._path.user + url = cast(URL, self._path) + self._host = host or url.host or cfg.host + self._port = port or url.port or cfg.port + url_pw = url.password + url_user = url.user # Username precedence: kwarg > URL > netrc > env default. # Password precedence: kwarg > URL > netrc > anonymous default. netrc_user, netrc_pw, netrc_account = _netrc_lookup(self._host) @@ -218,7 +224,7 @@ def _create_env_config(cls) -> SimpleNamespace: @property def _item_path(self) -> str: - return self._path.path or "/" + return cast(URL, self._path).path or "/" @property def _native_kwargs(self) -> dict: @@ -404,10 +410,16 @@ async def _write(client: aioftp.Client) -> int: return await self._op(_write) async def _upload_buffer( - self, fileobj: IO[bytes], size: int, *, backend_options: BackendOptions | None = None + self, + fileobj: IO[bytes], + size: int, + *, + backend_options: BackendOptions | None = None, + chunk_size: int | None = None, ) -> None: """Stream large objects to the FTP data connection (bounded memory).""" - if size < self._STREAM_THRESHOLD: + chunk_bytes = chunk_size or self._STREAM_CHUNK + if size < chunk_bytes: await self.write_bytes(fileobj.read()) return path = self._item_path @@ -415,7 +427,7 @@ async def _upload_buffer( async def _stream(client: aioftp.Client) -> int: async with client.upload_stream(path) as stream: while True: - chunk = fileobj.read(self._STREAM_CHUNK) + chunk = fileobj.read(chunk_bytes) if not chunk: break await stream.write(chunk) diff --git a/src/asanypath/gcs.py b/src/asanypath/gcs.py index a3e05e8..b45e6c2 100644 --- a/src/asanypath/gcs.py +++ b/src/asanypath/gcs.py @@ -44,7 +44,14 @@ ) from asanypath.cloud import CloudPathMixin -from asanypath.options import AccessGrant, AccessPolicy, AccessPolicyPatch, BackendOptions +from asanypath.options import ( + GCS_CHUNK_ALIGN, + UPLOAD_CHUNK_SIZE, + AccessGrant, + AccessPolicy, + AccessPolicyPatch, + BackendOptions, +) GCS_API_BASE = "https://storage.googleapis.com" @@ -65,9 +72,8 @@ class GCSPath(CloudPathMixin): protocol: str = "gs" _supports_range_read: bool = True - # Single PUT below the threshold, else resumable upload in 256 KiB-aligned chunks. - _MULTIPART_THRESHOLD = 8 * 1024 * 1024 - _MULTIPART_PART_SIZE = 8 * 1024 * 1024 + # Bytes per resumable chunk; objects below one chunk upload in a single PUT. + _MULTIPART_PART_SIZE = UPLOAD_CHUNK_SIZE _is_dir_batch_fn = staticmethod(gcs_is_dir_batch) _copy_batch_fn = staticmethod(gcs_copy_batch) @@ -150,24 +156,29 @@ def _native_kwargs(self) -> dict: # Backend-specific operations # ------------------------------------------------------------------ - async def _upload_buffer(self, fileobj, size: int, *, backend_options=None) -> None: + async def _upload_buffer( + self, fileobj, size: int, *, backend_options=None, chunk_size: int | None = None + ) -> None: """Upload a spooled write buffer, using a resumable session for large objects.""" - if size < self._MULTIPART_THRESHOLD: + # GCS resumable chunks must be 256 KiB-aligned (except the last). + raw = chunk_size or self._MULTIPART_PART_SIZE + part = max(raw - raw % GCS_CHUNK_ALIGN, GCS_CHUNK_ALIGN) + if size < part: data = fileobj.read() if backend_options is None: await self.write_bytes(data) else: await self.write_bytes(data, backend_options=backend_options) return - await self._resumable_upload(fileobj, size) + await self._resumable_upload(fileobj, size, part) - async def _resumable_upload(self, fileobj, size: int) -> None: + async def _resumable_upload(self, fileobj, size: int, part_size: int) -> None: """Stream the buffer via a GCS resumable session (memory bounded to one chunk).""" native_kwargs = await self._get_native_kwargs() session_uri = await gcs_start_resumable(object=self._item_path, total=size, **native_kwargs) offset = 0 while True: - chunk = fileobj.read(self._MULTIPART_PART_SIZE) + chunk = fileobj.read(part_size) if not chunk: break await gcs_upload_chunk(session_uri=session_uri, data=chunk, offset=offset, total=size) diff --git a/src/asanypath/local.py b/src/asanypath/local.py index 7c28255..9222e47 100644 --- a/src/asanypath/local.py +++ b/src/asanypath/local.py @@ -294,7 +294,7 @@ def _copy_remote_file() -> int: run_sync_maybe(remote_dst.parent.mkdir(parents=True, exist_ok=True)) size = self._path.stat().st_size with self._path.open("rb") as src_file: - run_sync_maybe(remote_dst._upload_buffer(src_file, size)) + run_sync_maybe(remote_dst._upload_buffer(src_file, size, chunk_size=chunk_size)) return size def _copy_remote_tree() -> int: @@ -315,7 +315,11 @@ def _copy_remote_tree() -> int: run_sync_maybe(destination_file.parent.mkdir(parents=True, exist_ok=True)) size = source_file._path.stat().st_size with source_file._path.open("rb") as src_file: - run_sync_maybe(destination_file._upload_buffer(src_file, size)) + run_sync_maybe( + destination_file._upload_buffer( + src_file, size, chunk_size=chunk_size + ) + ) total += size return total @@ -596,7 +600,7 @@ async def _copy_remote_file() -> int: await maybe_mkdir size = self._path.stat().st_size with self._path.open("rb") as src_file: - await remote_dst._upload_buffer(src_file, size) + await remote_dst._upload_buffer(src_file, size, chunk_size=chunk_size) return size async def _copy_remote_tree() -> int: @@ -619,7 +623,9 @@ async def _copy_remote_tree() -> int: await maybe_mkdir size = source_file._path.stat().st_size with source_file._path.open("rb") as src_file: - await destination_file._upload_buffer(src_file, size) + await destination_file._upload_buffer( + src_file, size, chunk_size=chunk_size + ) total += size return total diff --git a/src/asanypath/options.py b/src/asanypath/options.py index 3ef505a..920d7d3 100644 --- a/src/asanypath/options.py +++ b/src/asanypath/options.py @@ -8,12 +8,42 @@ from __future__ import annotations +import os from collections.abc import Mapping from typing import Any, Literal import msgspec +def _env_bytes(name: str, default: int) -> int: + """Read a positive integer byte count from *name*, else return *default*.""" + raw = os.getenv(name) + if not raw: + return default + try: + value = int(raw) + except ValueError: + return default + return value if value > 0 else default + + +# Bytes held in memory per in-flight upload part/chunk. Objects smaller than this +# upload in a single request; larger ones stream in parts of this size — so this +# is the peak resident memory per upload (parts upload sequentially, no fan-out). +# Tunable via ``ASANYPATH_UPLOAD_CHUNK_SIZE``; also overridable per call through +# ``copy(chunk_size=...)`` and, for writes, ``open(..., buffering=...)``. +UPLOAD_CHUNK_SIZE: int = _env_bytes("ASANYPATH_UPLOAD_CHUNK_SIZE", 8 * 1024 * 1024) + +# Bytes an open write handle keeps in memory before spilling to a temp file. +# Tunable via ``ASANYPATH_SPOOL_MAX_SIZE``. +SPOOL_MAX_SIZE: int = _env_bytes("ASANYPATH_SPOOL_MAX_SIZE", 16 * 1024 * 1024) + +# S3 rejects multipart parts smaller than 5 MiB (except the last); GCS resumable +# chunks must be 256 KiB-aligned. Backends clamp the effective size accordingly. +S3_MIN_PART_SIZE: int = 5 * 1024 * 1024 +GCS_CHUNK_ALIGN: int = 256 * 1024 + + class BackendOptions(msgspec.Struct, frozen=True, kw_only=True): """Provider-native options attached to one backend operation. diff --git a/src/asanypath/s3.py b/src/asanypath/s3.py index b7c03fd..dbfb883 100644 --- a/src/asanypath/s3.py +++ b/src/asanypath/s3.py @@ -37,6 +37,8 @@ from asanypath.cloud import CloudPathMixin from asanypath.options import ( + S3_MIN_PART_SIZE, + UPLOAD_CHUNK_SIZE, AccessAction, AccessGrant, AccessPolicy, @@ -55,9 +57,9 @@ def _default_s3_endpoint(region: str) -> str: class S3Path(CloudPathMixin): protocol: str = "s3" _supports_range_read: bool = True - # AWS CLI defaults: single PUT below the threshold, else fixed-size parts. - _MULTIPART_THRESHOLD = 8 * 1024 * 1024 - _MULTIPART_PART_SIZE = 8 * 1024 * 1024 + # Bytes per part; objects below one part upload in a single PUT. Shared + # default, overridable per class or per call (copy/open). + _MULTIPART_PART_SIZE = UPLOAD_CHUNK_SIZE @staticmethod def _batcher_key_fn( @@ -177,18 +179,22 @@ async def _get_native_kwargs(self) -> dict: # Backend-specific operations # ------------------------------------------------------------------ - async def _upload_buffer(self, fileobj, size: int, *, backend_options=None) -> None: + async def _upload_buffer( + self, fileobj, size: int, *, backend_options=None, chunk_size: int | None = None + ) -> None: """Upload a spooled write buffer, using multipart for large objects.""" - if size < self._MULTIPART_THRESHOLD: + # S3 requires >= 5 MiB parts (except the last), so clamp the request. + part = max(chunk_size or self._MULTIPART_PART_SIZE, S3_MIN_PART_SIZE) + if size < part: data = fileobj.read() if backend_options is None: await self.write_bytes(data) else: await self.write_bytes(data, backend_options=backend_options) return - await self._multipart_upload(fileobj, backend_options=backend_options) + await self._multipart_upload(fileobj, part, backend_options=backend_options) - async def _multipart_upload(self, fileobj, *, backend_options=None) -> None: + async def _multipart_upload(self, fileobj, part_size: int, *, backend_options=None) -> None: """Stream the buffer to S3 as multipart parts (memory bounded to one part).""" options_json = ( msgspec.json.encode(backend_options).decode() if backend_options is not None else "{}" @@ -201,7 +207,7 @@ async def _multipart_upload(self, fileobj, *, backend_options=None) -> None: try: part_number = 1 while True: - chunk = fileobj.read(self._MULTIPART_PART_SIZE) + chunk = fileobj.read(part_size) if not chunk: break etag = await s3_upload_part( diff --git a/src/asanypath/ssh.py b/src/asanypath/ssh.py index 15929d5..4b29c79 100644 --- a/src/asanypath/ssh.py +++ b/src/asanypath/ssh.py @@ -46,7 +46,7 @@ from pathlib import Path from time import time from types import SimpleNamespace -from typing import IO, TYPE_CHECKING, NoReturn, overload +from typing import IO, TYPE_CHECKING, NoReturn, cast, overload from asanypath_native import ( ssh_disconnect_all, @@ -64,9 +64,16 @@ ssh_write, ssh_write_chunk, ) +from yarl import URL from asanypath.cloud import CloudPathMixin -from asanypath.options import AccessGrant, AccessPolicy, AccessPolicyPatch, BackendOptions +from asanypath.options import ( + UPLOAD_CHUNK_SIZE, + AccessGrant, + AccessPolicy, + AccessPolicyPatch, + BackendOptions, +) if TYPE_CHECKING: from typing import Self @@ -171,10 +178,9 @@ class SSHPath(CloudPathMixin): protocol: str = "ssh" _supports_range_read: bool = True - # Large objects stream to the remote file in chunks (first truncates, rest - # append) instead of buffering the whole payload in memory. - _STREAM_THRESHOLD = 8 * 1024 * 1024 - _STREAM_CHUNK = 8 * 1024 * 1024 + # Bytes per streamed chunk; objects below one chunk write in a single call. + # First chunk truncates, the rest append, so no offset tracking is needed. + _STREAM_CHUNK = UPLOAD_CHUNK_SIZE def __init__( self, @@ -191,9 +197,10 @@ def __init__( # Distinguish "typed" values (kwarg/URL) from env defaults so # ssh_config gets a chance to fill in port/user before we fall # back to SSH_PORT/SSH_USER. - typed_host = host or self._path.host - typed_port = port or self._path.port - typed_user = username or self._path.user + url = cast(URL, self._path) + typed_host = host or url.host + typed_port = port or url.port + typed_user = username or url.user self._typed_port = typed_port self._typed_user = typed_user self._host = typed_host or cfg.host @@ -273,7 +280,7 @@ def _create_env_config(cls) -> SimpleNamespace: def _item_path(self) -> str: # ``/~`` / ``/~/foo`` encode "remote home" (scp-style) -- translate # to SFTP cwd (= user's home after login). - p = self._path.path or "/" + p = cast(URL, self._path).path or "/" if p == "/~": return "." if p.startswith("/~/"): @@ -464,15 +471,21 @@ async def write_bytes(self, data: bytes) -> int: _map_native_error(e, str(self)) async def _upload_buffer( - self, fileobj: IO[bytes], size: int, *, backend_options: BackendOptions | None = None + self, + fileobj: IO[bytes], + size: int, + *, + backend_options: BackendOptions | None = None, + chunk_size: int | None = None, ) -> None: """Stream large objects chunk-by-chunk (bounded memory) via APPEND writes.""" - if size < self._STREAM_THRESHOLD: + chunk_bytes = chunk_size or self._STREAM_CHUNK + if size < chunk_bytes: await self.write_bytes(fileobj.read()) return truncate = True while True: - chunk = fileobj.read(self._STREAM_CHUNK) + chunk = fileobj.read(chunk_bytes) if not chunk: break try: diff --git a/tests/test_cloudpathmixin.py b/tests/test_cloudpathmixin.py index c7758a5..05b7b02 100644 --- a/tests/test_cloudpathmixin.py +++ b/tests/test_cloudpathmixin.py @@ -638,7 +638,7 @@ async def test_open_write_spills_to_disk(self, cloud_path): payload = b"x" * (_CloudFile._SPOOL_MAX_SIZE + 4096) captured = {} - async def fake_upload(self, fileobj, size, *, backend_options=None): + async def fake_upload(self, fileobj, size, *, backend_options=None, chunk_size=None): captured["rolled"] = fileobj._rolled captured["size"] = size captured["data"] = fileobj.read() diff --git a/tests/test_ftp.py b/tests/test_ftp.py index d493a41..8c22123 100644 --- a/tests/test_ftp.py +++ b/tests/test_ftp.py @@ -247,10 +247,7 @@ async def test_upload_buffer_streams_chunks(self, client): s = _stream() client.upload_stream.return_value = s - with ( - patch.object(FTPPath, "_STREAM_THRESHOLD", 4), - patch.object(FTPPath, "_STREAM_CHUNK", 4), - ): + with patch.object(FTPPath, "_STREAM_CHUNK", 4): await _ftp()._upload_buffer(BytesIO(b"abcdefghij"), 10) written = b"".join(c.args[0] for c in s.write.await_args_list) assert written == b"abcdefghij" # streamed in chunks, reassembled intact diff --git a/tests/test_gcs.py b/tests/test_gcs.py index 78ea438..50eb707 100644 --- a/tests/test_gcs.py +++ b/tests/test_gcs.py @@ -439,3 +439,29 @@ async def upload_chunk(*, session_uri, data, offset, total, **kwargs): # Contiguous, ascending offsets that reassemble the payload intact. assert [off for off, _ in chunks] == [0, part, 2 * part] assert b"".join(d for _, d in chunks) == payload + + +@pytest.mark.asyncio +async def test_chunk_size_aligned_to_256kib(): + from io import BytesIO + + from asanypath.options import GCS_CHUNK_ALIGN + + p = _make_gcs_path("gs://bucket/big.bin") + requested = GCS_CHUNK_ALIGN * 3 + 100 # not 256 KiB-aligned + payload = b"z" * (requested * 2) + sizes: list[int] = [] + + async def start(*, total, **kwargs): + return "http://session/uri" + + async def upload_chunk(*, data, **kwargs): + sizes.append(len(data)) + + with ( + patch("asanypath.gcs.gcs_start_resumable", new=start), + patch("asanypath.gcs.gcs_upload_chunk", new=upload_chunk), + ): + await p._upload_buffer(BytesIO(payload), len(payload), chunk_size=requested) + assert sizes[0] % GCS_CHUNK_ALIGN == 0 # floored to a 256 KiB multiple + assert sizes[0] == GCS_CHUNK_ALIGN * 3 diff --git a/tests/test_local.py b/tests/test_local.py index 5a338b4..e5a7a88 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -98,7 +98,7 @@ def __str__(self): def __truediv__(self, other): return self.children.setdefault(str(other), _AsyncRemoteDestination()) - async def _upload_buffer(self, fileobj, size, *, backend_options=None): + async def _upload_buffer(self, fileobj, size, *, backend_options=None, chunk_size=None): await self.write_bytes(fileobj.read()) @@ -120,7 +120,7 @@ def __str__(self): def __truediv__(self, other): return self.children.setdefault(str(other), _SyncRemoteDestination()) - async def _upload_buffer(self, fileobj, size, *, backend_options=None): + async def _upload_buffer(self, fileobj, size, *, backend_options=None, chunk_size=None): self.write_bytes(fileobj.read()) diff --git a/tests/test_options.py b/tests/test_options.py new file mode 100644 index 0000000..d782525 --- /dev/null +++ b/tests/test_options.py @@ -0,0 +1,32 @@ +# Copyright (C) 2026 Axis Communications AB, Lund, Sweden +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +"""Upload tuning configuration (env-var parsing).""" + +from __future__ import annotations + +import pytest + +from asanypath.options import _env_bytes + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (None, 777), # unset -> default + ("", 777), # empty -> default + ("1048576", 1048576), # explicit bytes + ("0", 777), # non-positive -> default + ("-5", 777), # negative -> default + ("notanumber", 777), # invalid -> default + ], +) +def test_env_bytes(monkeypatch, value, expected): + if value is None: + monkeypatch.delenv("ASANYPATH_TEST_KNOB", raising=False) + else: + monkeypatch.setenv("ASANYPATH_TEST_KNOB", value) + assert _env_bytes("ASANYPATH_TEST_KNOB", 777) == expected diff --git a/tests/test_s3.py b/tests/test_s3.py index 9eb5b9c..21d8b33 100644 --- a/tests/test_s3.py +++ b/tests/test_s3.py @@ -624,7 +624,7 @@ async def complete(*, upload_id, parts, **kwargs): @pytest.mark.asyncio async def test_multipart_aborts_on_error(): p = _make_s3path("s3://mybucket/big.bin") - payload = b"A" * (S3Path._MULTIPART_THRESHOLD + 10) + payload = b"A" * (S3Path._MULTIPART_PART_SIZE + 10) async def create(**kwargs): return "UPID" @@ -679,3 +679,57 @@ async def upload_part(*, part_number, data, **kwargs): await AsAnyPath(str(src)).copy(dst) assert b"".join(d for _, d in received) == payload # streamed as parts, intact + + +@pytest.mark.asyncio +async def test_chunk_size_clamped_to_s3_minimum(): + from io import BytesIO + + from asanypath.options import S3_MIN_PART_SIZE + + p = _make_s3path("s3://mybucket/big.bin") + payload = b"x" * (S3_MIN_PART_SIZE * 2 + 10) + sizes: list[int] = [] + + async def create(**kwargs): + return "U" + + async def upload_part(*, data, part_number, **kwargs): + sizes.append(len(data)) + return f'"e{part_number}"' + + with ( + patch("asanypath.s3.s3_create_multipart", new=create), + patch("asanypath.s3.s3_upload_part", new=upload_part), + patch("asanypath.s3.s3_complete_multipart", new=AsyncMock()), + patch("asanypath.s3.s3_abort_multipart", new=AsyncMock()), + ): + # Request 1 MiB parts; S3 clamps up to its 5 MiB minimum. + await p._upload_buffer(BytesIO(payload), len(payload), chunk_size=1024 * 1024) + assert sizes[0] == S3_MIN_PART_SIZE + + +@pytest.mark.asyncio +async def test_chunk_size_honors_larger_request(): + from io import BytesIO + + p = _make_s3path("s3://mybucket/big.bin") + chunk = 12 * 1024 * 1024 + payload = b"y" * (chunk + 1000) + sizes: list[int] = [] + + async def create(**kwargs): + return "U" + + async def upload_part(*, data, part_number, **kwargs): + sizes.append(len(data)) + return f'"e{part_number}"' + + with ( + patch("asanypath.s3.s3_create_multipart", new=create), + patch("asanypath.s3.s3_upload_part", new=upload_part), + patch("asanypath.s3.s3_complete_multipart", new=AsyncMock()), + patch("asanypath.s3.s3_abort_multipart", new=AsyncMock()), + ): + await p._upload_buffer(BytesIO(payload), len(payload), chunk_size=chunk) + assert sizes == [chunk, 1000] # honors the requested part size diff --git a/tests/test_ssh.py b/tests/test_ssh.py index e2d1369..65beef8 100644 --- a/tests/test_ssh.py +++ b/tests/test_ssh.py @@ -377,7 +377,6 @@ async def write_chunk(*, path, data, truncate, **kwargs): return len(data) with ( - patch.object(SSHPath, "_STREAM_THRESHOLD", 4), patch.object(SSHPath, "_STREAM_CHUNK", 4), patch("asanypath.ssh.ssh_write_chunk", new=write_chunk), ): diff --git a/uv.lock b/uv.lock index 2ba7236..277fc70 100644 --- a/uv.lock +++ b/uv.lock @@ -309,7 +309,7 @@ requires-dist = [ { name = "keyring", marker = "extra == 'all'", specifier = ">=24" }, { name = "keyring", marker = "extra == 'credstore'", specifier = ">=24" }, { name = "keyring", marker = "extra == 'dev'", specifier = ">=24" }, - { name = "msgspec", specifier = ">=0.21.1" }, + { name = "msgspec", specifier = ">=0.22.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3" }, @@ -1700,58 +1700,91 @@ wheels = [ [[package]] name = "msgspec" -version = "0.21.1" -source = { registry = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/simple" } -sdist = { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } -wheels = [ - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/96/38/d591d9f66d43d897ecbd249f2833665823d19c8b043f16619bc8343e23df/msgspec-0.21.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72d9cd03241b8b2edb2e12dcc66c500fa480d8cbd71a8bac105809d468882064", size = 195172, upload-time = "2026-04-12T21:43:45.062Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/69/1a/6899188b5982ec1324e0c629b7801eed2db987f6634fab58abd9fc82d317/msgspec-0.21.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed2ab278200e743a1d2610a4e0c8fc74f6cecb8548544cdec43f927bd9265238", size = 188316, upload-time = "2026-04-12T21:43:46.641Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/9e/95/7e591b4fa11fdbbf9891164473c23420a8c781ef553295abe416bf335f42/msgspec-0.21.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd677e3001fdfed9186de72eab434da2976303cd5eb9550921d3d0c3e3e168ce", size = 216565, upload-time = "2026-04-12T21:43:48.081Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/19/86/714feeaf3b84cf2027235681725593840153dedd2868578f9f2715e296bb/msgspec-0.21.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f667b90b37fad734a91671abd68e0d7f4d066862771b87e91c53996dcb7a9027", size = 222689, upload-time = "2026-04-12T21:43:49.385Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/7d/b9/4384243e814f2579e5205e17d170b9c1a30121afd1393298d904817a7fa7/msgspec-0.21.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49880fd20fdbcfe1b793f07dd83f12572bab679c9800352c8b2240289aa46a06", size = 222343, upload-time = "2026-04-12T21:43:50.612Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/04/01/4b227d9c4057346271043632bad41979cf8c3dca372e41bb1f7d546395b2/msgspec-0.21.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae0162e22849a5e91eaad907766525107523b0daea3df267a9fcb5ba4e0936ae", size = 225607, upload-time = "2026-04-12T21:43:52.129Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c1/ce/27021d1c3e5da837743092a7b7a5e8818397e1f4c05ee8b068bd7d1fd78a/msgspec-0.21.1-cp310-cp310-win_amd64.whl", hash = "sha256:f041a2279f31e3a53319005e4d60ba77c085cfcbe394cdc7ce803c2d01fe9449", size = 188392, upload-time = "2026-04-12T21:43:53.384Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/80/2b/daf7a8d6d7cf00e0dcd0439178b284ade701234abdcadf3385601da04fbd/msgspec-0.21.1-cp310-cp310-win_arm64.whl", hash = "sha256:1bf17cbd7b28a5dffc7e764c654eed8ccde5e0f1de7970628608304640d4ce4e", size = 174191, upload-time = "2026-04-12T21:43:54.6Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/ba/7f/bbc4e74cd33d316b75541149e4d35b163b63bce066530ae185a2ec3b5bfc/msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87", size = 193131, upload-time = "2026-04-12T21:43:56.094Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597, upload-time = "2026-04-12T21:43:57.242Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856, upload-time = "2026-04-12T21:44:00.015Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314, upload-time = "2026-04-12T21:44:01.548Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215, upload-time = "2026-04-12T21:44:02.884Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/09/af/8bf15736a6dd3cb4f90c5467f6dc39197d2daaf10754490cdc0aa17b7312/msgspec-0.21.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6faffe5bb644ec884052679af4dfd776d4b5ca90e4a7ec7e7e319e4e6b93a6e", size = 188554, upload-time = "2026-04-12T21:44:04.151Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/ef/29/cc7db3a165b62d16e64a83f82eccb79655055cb5bc1f60459a6f9d7c82f2/msgspec-0.21.1-cp311-cp311-win_arm64.whl", hash = "sha256:ee9e3f11fa94603f7d673bf795cfa31b549c4a2c723bc39b45beb1e7f5a3fb99", size = 174517, upload-time = "2026-04-12T21:44:05.66Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" }, - { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, +version = "0.22.0" +source = { registry = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/simple" } +sdist = { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/d0/e6/6dcf9306ff3c5e486578f3bf29ed11dfbdbbc2a8bf0caf7e07d392887fda/msgspec-0.22.0.tar.gz", hash = "sha256:0a13624a4969159fe35d8c2a3d377b2b61bbd8585e327440d5e52725affcce38", size = 343188, upload-time = "2026-09-29T14:14:11.422Z" } +wheels = [ + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c7/5e/78d4fa2073bb3a891753e7f915d51094e2ded5aa5e9b20402518929b373e/msgspec-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f3413e3647275f787b21b4dfb4836a59a1a5acf1018ab1d45843b1d7edf15c22", size = 200076, upload-time = "2026-09-29T14:12:07.599Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/38/f8/59701da04584af4ccd55f42200da303ebf146cd6867186a8b9b1e127a4a2/msgspec-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:38c5b9bd347bc9abbcee40752be3c5117854e891ea7a1881a56d4b3dec58c5e7", size = 192337, upload-time = "2026-09-29T14:12:09.198Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/eb/dd/bd4131da741aa349656fe32a5cca0c4266c58d7b5ad75485bed29565f7cd/msgspec-0.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57c282f474e17acf6bcf84f393c73afd45d6eba47cccff8b76b79c4fbb8a3b54", size = 222888, upload-time = "2026-09-29T14:12:10.691Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c6/46/01fe71c42b3342f00e2dd6c5a8837f5dc4d0e1596b4c74c054fb13075201/msgspec-0.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12a887c4c06e4a771a2db32c9a80c7bb21866b12458025f636dcdc2253331c28", size = 227838, upload-time = "2026-09-29T14:12:12.178Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/62/8f/1a459825e0a5510de882af461459bd7f0525342b3c0bf1000e27be7aeef5/msgspec-0.22.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6c8a3f210421e29d8f7e9815f106cf59d758665b7fe5428e61152ce24fe65d7", size = 235818, upload-time = "2026-09-29T14:12:13.586Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/3c/2e/9d37b6f1190101b452f6c455e8715cc9960afad231e18cf9545af58710b9/msgspec-0.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebd211d7af79ed8710c64e9e8d4c0d02749bc20170e7ab4e1c5801ca7c99d25b", size = 228019, upload-time = "2026-09-29T14:12:15.156Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c1/d5/33723137c96b8f244d8e6fc57a0a8d3b57b3599ce9b4a4dd58dc55a46d1c/msgspec-0.22.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:27d9ef46c80884f9c4f323e0b18bec464287e872121e70f2cbe47335780bf597", size = 236406, upload-time = "2026-09-29T14:12:16.908Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/44/4a/f0e4a9ab970ce0a31f191acb772d3e1af67eeb73e1d73b70c079252aed02/msgspec-0.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ec108e96fdaa8fdbe5bb993ec97a9d1faa69b3a521eecd71a6e5acbe0e29ae69", size = 231028, upload-time = "2026-09-29T14:12:18.497Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/0a/e8/3de7345a8944a5bcfc9dd861d30fcea5f20f51057bcafacbbff9164e55fc/msgspec-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:21c887d4de397355f6635c2a037b1c067882dac5d132a1793d63bbf7cf5ca78e", size = 190753, upload-time = "2026-09-29T14:12:20.291Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/66/c9/f0d3bd2dfc3753806ab70b8d00a1613019c39148a87da797771d7f72a0a9/msgspec-0.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a663a8d7f6ad56ac1dbcba91e046ba8ebab7773ae72ef3dd3c47f8226919184", size = 188939, upload-time = "2026-09-29T14:12:21.645Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/9d/22/45c17acb1a85360b10afb95f66777f76bc2634993c66db8b7833832bd343/msgspec-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb1e129b81ac8fcf9ec649b081c6c8da1c7ea6f87cab336d46386abc2cd855c1", size = 198231, upload-time = "2026-09-29T14:12:23.016Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/34/79/1cf725694125051e866066d74e6199206838d1465cbfc35081dc29b6e366/msgspec-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dce29a04966e31abf9b83b697c6d672486526dc5d03fcd6970cb56d5dc1fbeea", size = 190911, upload-time = "2026-09-29T14:12:24.636Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/bc/b2/e0ace038031a2988aa2e85c431c4d7aef734fbba4749ace6bc5bf310b769/msgspec-0.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b962000e11dd34fb210a5a2c57a8a62b2d92b381c8cb3b05c075a83e38f8d645", size = 220343, upload-time = "2026-09-29T14:12:26.111Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/7b/e6/16ddb09185d79dc00177994cf0bdb1cd8e5cc44a1d1bfba61bdda5f382cb/msgspec-0.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6db3806b3b76ca78064255eac6fa101a8a64fe6f698d80fbaf81fdfa21217d4", size = 225251, upload-time = "2026-09-29T14:12:27.559Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/16/c2/a6af0d38fb0e72f02851ed084c4b8175140cfaf3eaf48b38da0c3941db26/msgspec-0.22.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a88d939d3fe4b8c7314645ebcd6e86c8c8a512ea7820d6550355973e803bc0f1", size = 233488, upload-time = "2026-09-29T14:12:28.996Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/0b/9b/b1c4208cdf487e2ba7af145f721b279444ff76af05a9f8fce992ed0588ee/msgspec-0.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b31746da07cba0e330c6433a94a4699ad77d3aeb9638d1a320a7686b69f6249", size = 225688, upload-time = "2026-09-29T14:12:30.351Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/83/54/b9240d908674ef7c41d02cb909731ad6d9931c23bd6a27d8d10776c6f964/msgspec-0.22.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:6ae370f92f3517f0e6f209ba7cc649c957b444868439197e046be07154667551", size = 234250, upload-time = "2026-09-29T14:12:31.887Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/df/c0/d498798aaab3bd191a33955de47b40f07fae7667d86a33b705443a7e9491/msgspec-0.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a696f23f7c1ffb31fae308502e01a3965c3891d5c400f01d0d1096dbe77519e", size = 228337, upload-time = "2026-09-29T14:12:33.365Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/fa/51/5e9ae5a5ddc254e15435749328161e95598750e5df644bb00fa9e2297122/msgspec-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:024138c51afd335d0b4dce401be33902caafac2b64f8c9f2509a378986175d98", size = 190962, upload-time = "2026-09-29T14:12:34.847Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/12/38/fb64a18543bcbebc53a375cb00b1c93bf264a0b6c7bbe9e38b37cc5f0768/msgspec-0.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:4600dbec738ed74e4c9bd35503e84701200ea7db344cfdeda80677b3ee53eb64", size = 189458, upload-time = "2026-09-29T14:12:36.277Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/a4/87/3e017dca361d09ed1cd09dc981a6df21b32e830fbec3470f7486d38b6be5/msgspec-0.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab1e9e7531e353653b906cdd12a0220cc288a1e8e3436aabc65f4508d91b14d9", size = 201301, upload-time = "2026-09-29T14:12:38.048Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/fb/02/109165edaafb895668d87177972a32ade9126a54f3736123d8e44be9096d/msgspec-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b60b43425a47eb9cfe987f6874e354ca7c760e58e295b4e2273ff03574df28a1", size = 193044, upload-time = "2026-09-29T14:12:39.46Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/54/a5/65de05f8804492f76ea121b21a125cdf1d97ec461c677bfa0ba354d6fbdd/msgspec-0.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5a169b5b03f0f2c7a296c002647db1dab75d2cd501bca34e32b71cab0261b56", size = 224035, upload-time = "2026-09-29T14:12:40.876Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/4a/cc/aa1a47f8c92280d37498a5ea56a2a36606d034383e3e6472d64cbb56cf85/msgspec-0.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c401861c5bb3a57f7d6423ea7ed4352cd57aa3f04f4fbe9f3e3e4564a10f08", size = 230377, upload-time = "2026-09-29T14:12:42.796Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/61/50/f8bcdb3d613a4a4b92704297a12eba5c985cf572a64ee1a004d265759c69/msgspec-0.22.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08826f5e5b0fa2f7a88592c396a243cfcc63d37e19f9d4fbe3b3f1be2fbdc404", size = 237390, upload-time = "2026-09-29T14:12:44.282Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/cf/8a/473fa423f8fdd1b810b8652594323d7301df6920b62844d860daa0feff34/msgspec-0.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21460f54cee9208239b1a8421fdf25bffc77293e1daba88f585711ad839b9758", size = 227733, upload-time = "2026-09-29T14:12:45.839Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/03/1d/272ce23adae6c71b3f763aed3ee6e115cccc56124ed8ee0e3e3d2681e2c8/msgspec-0.22.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cfc3d9557de9c806318725b702f3e664db33167bb42892079b693c69893fd33b", size = 236783, upload-time = "2026-09-29T14:12:47.234Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/f6/26/29e0b9a8605c8819a3c718158e345a616ac42c092dd7d7ab248c2f2b0a72/msgspec-0.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0b25dcbc108783cb72503ed705b9fbb8c3cb02ee5801923f44b5f038c91cc365", size = 232728, upload-time = "2026-09-29T14:12:48.792Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/e1/a6/99597c281d716da6c662b48dcc3f734669f716b41d5df2af367dac9e7c21/msgspec-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:6ad64f5c260866b0d543f89f50cee43628989c1433c5de7ce820281fa28a2611", size = 192885, upload-time = "2026-09-29T14:12:50.274Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/46/80/85fff923d448b886ec3a85900c578d9367f08dad54fe48879495b4c6d055/msgspec-0.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:0922714feff5300aacd8ecd65fa828317ce4bf5212b3139258c0bfc0253cd80e", size = 191223, upload-time = "2026-09-29T14:12:51.699Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/7f/62/5374fba2ede0408f4bd8b9b3a6c8464f8d0ea7ae9a2a064bd81ca492bd1e/msgspec-0.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f13c127a945479bc9db057eb253b8851075c8e1ae07ffc967bfa1c5676203a86", size = 201355, upload-time = "2026-09-29T14:12:53.145Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/cc/e3/357baa8d2a9164a98dfd7ef9d3a58125df0ed981be909945bdd337be7194/msgspec-0.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5aa24eb475d070ecbbe5b21080fc3ce4b0b76c60de25cfe0c9678d8fb44bb42f", size = 193097, upload-time = "2026-09-29T14:12:54.52Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/fa/1b/9cc07718d1dee8ed5e89a265801d565bc0f15ead435ccb198f9c7bf92574/msgspec-0.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:627bfdfe5a4b3d916b3360b30f4cddeee3a084f56593e33527c6872fa8322ff9", size = 224112, upload-time = "2026-09-29T14:12:55.983Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/46/64/f33fdfe95aca76601194a7064d14816c7c22c4eccc1b03a5335785895fa3/msgspec-0.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6c310ef83e7e291b01a63298828f848348bb99e84a1098c4b3923c05674d032", size = 230472, upload-time = "2026-09-29T14:12:57.648Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/8e/b3/8ceaa9981c230adf43c45a6e8da25da23a381eddc7ed05aeaca1d5e7928b/msgspec-0.22.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c1e76c6bd523141b9c05c2f8a70979cd0efedbd68855a66f292f8892c0b8fc7", size = 237382, upload-time = "2026-09-29T14:12:59.414Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/88/a6/7b5c4fb39e0bf2dabc8be923c33c39b07ba769a0ce6f0afbbdfaadb1f2f2/msgspec-0.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc374dedd5f85a5f4de2386dc5f737894ccb8c1ac18e9566ce66fd9839e6285d", size = 227717, upload-time = "2026-09-29T14:13:00.88Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b8/5b/2334ee638880e756c8bc54a1177bd65877c786433693a43594ef5ecbe2d8/msgspec-0.22.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:feafe612034d49e9144340c0b5168ee4e22c2af4aaa2c1db11ae84e1aac9543b", size = 236781, upload-time = "2026-09-29T14:13:02.468Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/6c/e5/b4c5323b17ecfce45350695d40fc93e16856db957a53cbcf2f53007d6e12/msgspec-0.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f48317f05312bfdf78248f53933f830f07ab75cc1c813ac3ca4220cb3b5b019", size = 232777, upload-time = "2026-09-29T14:13:04.025Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/01/33/e591f9d3d8d6c9cfc02ae95f3e3c44920f2d18050f3f252c244e0f293a0e/msgspec-0.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:0739b068f31f2004a364f97679ba91f2f5ecd6ec2a5b4b890188ab5c57d20672", size = 192829, upload-time = "2026-09-29T14:13:05.519Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/d1/cd/a011a5b8732cd781e2ea6da5b38d71ae4a9a329338411d1f008a58f5edbf/msgspec-0.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:508278300dd4efbd21cd3a4b2b016160a5feac98bc880d3673f6c06697baaf62", size = 191258, upload-time = "2026-09-29T14:13:06.909Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/53/f9/ac027b35477e6b83bcee32b3d9675b37abfa130f098dd6500fa67d768852/msgspec-0.22.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:221cbcbfa4478152b91d37dcfd4830e2be92773e8139e883f43773450ebacef8", size = 201276, upload-time = "2026-09-29T14:13:08.311Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/13/6b/2bffffa31662b1353a62e672442865d51c291ad778352fd490de16361dc6/msgspec-0.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dd9568695911055440d2bb7099ed9098fc181d335daa772d0eb3fe8f31ba4efb", size = 193233, upload-time = "2026-09-29T14:13:09.943Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/14/bc/4066416ff6aa918d1ef9295edee0041e4629e4079ad3839bdd8a68fd87f0/msgspec-0.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f039ef5207b847f075a0a43020ee6140cd47505f890e47e157f2deb485c2dc96", size = 225101, upload-time = "2026-09-29T14:13:11.391Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/63/ba/a8d390d5bd4c7d9ccde87c95cf071ada934cc9ca2c6af4d3d50b38f2d718/msgspec-0.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4f7e09cceac7dbf4c0761b8ae7df51c55b5df5e9af7aff2c895aac1ebea015", size = 230505, upload-time = "2026-09-29T14:13:12.869Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/9c/89/979664fdc913c624ef88a139b40e3a95ddf2a47c89e8b5c4147f69ee9c48/msgspec-0.22.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:614e2c827e0a3f934f3cf0cf4ba65210df8132b75a69a8a1f51bb3b2caf0ac5a", size = 237382, upload-time = "2026-09-29T14:13:14.317Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/07/3f/7d44c614376ae008ac6099be5f589b322c4ad44e32c6dbb0edd256215028/msgspec-0.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3689b9dfcc663358ef23ba4299d7460f01108515b041a7d30d05908ac9c32f", size = 228962, upload-time = "2026-09-29T14:13:15.763Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/0b/59/bf8504e6f63f6769d01fb66f8bd856cf0ed39a07fde354f440d711640054/msgspec-0.22.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2f950239ff1fc7322c6f9634807310265149cb168270d3ddcdda5b6ada13a28", size = 236691, upload-time = "2026-09-29T14:13:17.195Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/2b/40/5a9d2bde12af16a22ddbf371990a81d3e3c0dcd4bb4ef3b3f9616b033c14/msgspec-0.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c789b5ccd07c0a3c09767108ee06e089b2875f2309a4569c2648f30a8d31dfa", size = 232750, upload-time = "2026-09-29T14:13:18.691Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/75/5d/c0e6bdb81a87f6bd56a663a330c271af7670490c80d8d635d9fa21ad1adf/msgspec-0.22.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a66b1766311e42371e509c996c3933b161c7ae0eabdf361af5316dec197e1022", size = 136814, upload-time = "2026-09-29T14:13:20.415Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b9/c0/b0cfc6d33608e5ea8871f3be31f9146c56699e737a7d8862bf018484f278/msgspec-0.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:749899563d26b211379f142b8ffd7e2d7da149a51717798f0ce994dce50324f0", size = 197097, upload-time = "2026-09-29T14:13:21.869Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/42/1f/571f7fe7c725380605d680fc4c0084212b23d2dfcf6be0f2277f14462c56/msgspec-0.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:10d0d1d464960d99a949f7ca01ef8928e51c472433a5f5ab74b2d695fb830652", size = 196779, upload-time = "2026-09-29T14:13:23.62Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/ab/f3/3c87372bac651b37911e0dc6926c3958949d3fcb8cec1016adbc44d948b2/msgspec-0.22.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e79725246291516a7359caad5fb743ddc0ec66ed40d2381fb846325b5031504e", size = 205214, upload-time = "2026-09-29T14:13:25.158Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/43/4c/fbccd6e0fbbdf10c4d9b6bac8a26148dd5483b3ffff6d6c5a376ff1f5cb1/msgspec-0.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:38f7022fbe91954b31afe3888a0af1b652e0f370fafdeb1d425f4a814d789c9f", size = 196941, upload-time = "2026-09-29T14:13:26.637Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/55/04/8db7186d3ae8818356bc623cc132db8b77da37ce4b1345f35719c8ad5726/msgspec-0.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6d3ca19a8ff28d0a67a1824e2bff7ec649ec795c80a265f20ade4caa63080de", size = 229934, upload-time = "2026-09-29T14:13:28.285Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/17/24/a249f3491cabbe77cc65a1a6f87c128582aa39357227149be61cac8e554f/msgspec-0.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8b98ae215a102cbf6635f7df45f5c4af12f77fad1f7b71b9808fcf868a5735d", size = 234378, upload-time = "2026-09-29T14:13:29.821Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/87/ee/6dbcb1b5de8e9d47e8f0fde9a288628dc178c1749a570b98251218fa10c4/msgspec-0.22.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0aa0cc3f18c35bab79bd7b87fde95d6274a9deddeebd1ea541f8066a5073165", size = 243118, upload-time = "2026-09-29T14:13:31.544Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/79/03/7dd2d0ca988600e01fc00ad0cf20d1d44bc59369a913c988654c65f6582b/msgspec-0.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c8e84789918fbc15a503b92a829115ddd7567ecd3e4778bd418c56abbb86c11", size = 234557, upload-time = "2026-09-29T14:13:33.068Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/74/e2/43f3c63bff1650efcaaea31466246e28b46927323fc9ff416c68cc6e4047/msgspec-0.22.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3ca7d4cd69fbb66bd2da6211d3e79d40542d196c16c6d99bf838f76767ad35be", size = 241288, upload-time = "2026-09-29T14:13:34.532Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/8b/70/11b93815a59674f33182dc3e873d343ca0b37e25be52ecb28f52092f1fed/msgspec-0.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:28f53f3604dd3e70225f7563c831628dbb03299b428f8e62aadb4b628e386874", size = 236432, upload-time = "2026-09-29T14:13:36.083Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b7/82/7aad0f033f8dcb3f23868773c2ede803ae162a784828ccde75aa3f9b2f9d/msgspec-0.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7293dee54de040cfa225c22151cc3d72f17cd674b5ebcb52f38fb9f5701592e6", size = 202062, upload-time = "2026-09-29T14:13:37.955Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/e3/45/cf52577926d73e2369e25927e389cb4ea1461169c489f46d3248159b5be7/msgspec-0.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c3c510aba9015c085e514b75a9b3f1ed7c4591ae5e379655821b8bba51f30cc7", size = 201686, upload-time = "2026-09-29T14:13:39.42Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/c8/63/d93937e2aae34ff1ea33b62799d1963cacc1bf432d196d6130039657a122/msgspec-0.22.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:263e110955ed76fe0af2d79f819903b50a70dc0e7a752eb7aabe79d2e0a084fb", size = 202241, upload-time = "2026-09-29T14:13:40.919Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/3b/e2/46ece11a244cd56432eb2362ffbb8014f3f02963136d84d941f71fdc2a3f/msgspec-0.22.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c6f06576eced70462179a4b4638e84cf69fdbba37f44d13a64a21739c131a830", size = 194232, upload-time = "2026-09-29T14:13:42.454Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/cf/b1/1c385f2f93006cdc2af1511cc512c347cb22e2d4f11952c205230aedf586/msgspec-0.22.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d67582478b0eaabb899f2fb255c878ee7de57dff80eb73ab24f1865524ec441", size = 226524, upload-time = "2026-09-29T14:13:43.876Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/dc/fb/c80c8842d40347cacf89a60a4986b849dae1a6dfd25830441efdd6faa65b/msgspec-0.22.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:71cbbdb39631064e2f2f9e9ac2b1b69931d72276eb5f9da4ed025726296bdbb6", size = 231816, upload-time = "2026-09-29T14:13:45.329Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/73/ac/90bbcfd890b4bda90c93f7e1b7fc24e84b270420486d9d43ae31443d15ab/msgspec-0.22.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0a5c25516e2034b2db7767081759ff8996e214def9c43b3055f61e1be1caad", size = 244241, upload-time = "2026-09-29T14:13:46.851Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/72/9a/eabdb5f1b5e6013b0e2f9f2a95790587f6864aa9ca37f9d7dece65b53878/msgspec-0.22.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a1dab6a99c759d1391ab2993388c1892746a697254f4b5dc6c059ca6e3bfbc8b", size = 230198, upload-time = "2026-09-29T14:13:48.296Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/e9/89/9f080532d4ac52f416dd7318e55c2053cc071853d17d58e24897a5b553bf/msgspec-0.22.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:a52eba5c9528fd181fcec39d22b67aaa1dccc6cfe8e24d3f5d41130e6d04289d", size = 242949, upload-time = "2026-09-29T14:13:49.829Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/11/df/6baf9b2f3523ebe2b820820c7929fd72ec5f483a93147130338ecc353fac/msgspec-0.22.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1e547966017265c0d23342bcf2e027305dde40ea042d16694a9b96b4f696a052", size = 233914, upload-time = "2026-09-29T14:13:51.5Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/bb/37/9cf650779c8c1e53291ef184c838703930a4cabb1fb37e222c85a7d49fa9/msgspec-0.22.0-cp315-cp315-win_amd64.whl", hash = "sha256:0067057df265795f742658b15dbe53f3b6f21d19dcfa53676db11088cfa41e0a", size = 197910, upload-time = "2026-09-29T14:13:53.071Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/f5/ce/2f78c93d4f69e0167a19c2d40d4fbf7bbd6f074e1047536735832a4368ee/msgspec-0.22.0-cp315-cp315-win_arm64.whl", hash = "sha256:05dbc8268e50c9232ec72b9af1c7b13049aade4d1197764e38c427048706e046", size = 197590, upload-time = "2026-09-29T14:13:54.47Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/3f/bf/282e9a443058b85b8f706c9a651e2d8cdd11cc09d16e8fa347b6c57b75bb/msgspec-0.22.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:b3113ebcceeb7693a915183c73d92c10bf5c62851dd187cab43bd025fb587419", size = 206298, upload-time = "2026-09-29T14:13:55.913Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/ef/2d/2e694fa46f55319007f72013b17341ea3868be1c77e7a597176b202dda92/msgspec-0.22.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dfadea8bdcfafc614bd031de55a8ede22b43445cfff6d8b77cc0c07d3edc8a8", size = 198145, upload-time = "2026-09-29T14:13:57.412Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/5b/2e/2fa279cb57cb47175ae604d572787f903d4ad3f0afa867201bbd99e6647e/msgspec-0.22.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7a738826936c72348c613061d260446f13c82b6fd7d5d7705b6911ab8dca2f3", size = 232362, upload-time = "2026-09-29T14:13:58.817Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/a0/58/a7e759b11b28441c27f803b29d9b5f4b5ad85150c89354b5ede1baca9258/msgspec-0.22.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2ddea9d78d09460f06c26a7a508adcd049761c3208776162b8eb79b8a032cff", size = 235885, upload-time = "2026-09-29T14:14:00.381Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/86/56/8d7ee098e94cbd9f35fa643dc497e06a4a6307b9f562cfbe48103fc3b209/msgspec-0.22.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884c28c80b0a511595b29a9b04a3a230c3797369e4a033e6d5c6d9b5427f8e09", size = 248155, upload-time = "2026-09-29T14:14:01.945Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/b9/6d/1cabb4b8a5dbf696e2b24df9e482b2e0333bb3b1b13ebb5433813e6616ec/msgspec-0.22.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f7a923bcde480065c8e25967464cfb2a687ee67000bb43157e2d57e40eca7305", size = 236416, upload-time = "2026-09-29T14:14:03.363Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/ba/43/8bf0f558eb369f1f2d494b3d5ab9d0ae0907d07ecc0cdbe11b6768b02867/msgspec-0.22.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:65eea14bc65ccfeb8f3af62cb204841871e2961f002d7fa87dbe0f79dacf1c1c", size = 247292, upload-time = "2026-09-29T14:14:04.829Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/81/33/2fbaadf98b5510cac4bb56d2b03937e0b1fb4bfcd1ae6aba20361f299583/msgspec-0.22.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:0666a1520cab86796612e794e71107e0fbf5e8ff3ddcdfcfff8f1d94b860d2f1", size = 238220, upload-time = "2026-09-29T14:14:06.408Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/f1/cc/b6be6041098ab859a8472983ccc2c08339fc2ef53f28d4f5fe7f4f34276b/msgspec-0.22.0-cp315-cp315t-win_amd64.whl", hash = "sha256:885c6e0c89d6103648525fe62aa78d600054dedf7b3713d23b15d7ddb6d66a13", size = 202939, upload-time = "2026-09-29T14:14:08.079Z" }, + { url = "https://artifacts.se.axis.com/artifactory/api/pypi/all-pypi/packages/packages/5a/c1/664578dd98be70cd4ab1a9dcf3a181b1376b83c65ec41ee162130b58c8c0/msgspec-0.22.0-cp315-cp315t-win_arm64.whl", hash = "sha256:268594d0bae5510572599a6ab0364dd9de43c867d24a30856cd9f5edb63d8dc6", size = 202117, upload-time = "2026-09-29T14:14:09.891Z" }, ] [[package]]