Skip to main content

©2026 Axis Communications AB. AXIS COMMUNICATIONS, AXIS, ARTPEC and VAPIX are registered trademarks of Axis AB in various jurisdictions. All other trademarks are the property of their respective owners.

asanypath logo

AsAnyPath gives Python applications one asynchronous, pathlib-inspired API for local files and cloud object storage. Use it when an application must copy, read, list, or synchronize data without coupling its business logic to a single storage provider.

The same code works with local disk, Amazon S3, Google Cloud Storage, Azure Blob Storage, Artifactory, ssh, ftp, ftps, and plain HTTP(S). Typical uses include moving data between cloud providers, writing backup tools, and testing storage workflows locally before connecting to a cloud account.

Requirements

  • Python 3.10 through 3.14.
  • Access credentials for each cloud service that your application uses.

S3 authentication accepts explicit keys or uses the AWS SDK credential provider chain (including shared profiles and IAM roles) when keys are not supplied.

  • A POSIX-compatible shell such as Bash or Zsh for the terminal commands below. Windows users can use Windows Subsystem for Linux or adapt the commands for PowerShell.
  • Rust and a supported C compiler only when building the native extension from source. Published package wheels do not require a local Rust toolchain.

Usage

from asanypath import AsAnyPath

# The same API works for every backend — just change the URI scheme
path = AsAnyPath("s3://bucket/data.parquet")  # or gs://, az://, art://, file://, /local/path

data = await path.read_bytes()
await path.write_bytes(b"new content")
exists = await path.exists()
info = await path.stat()

# Iterate directories
async for child in path.parent.iterdir():
    print(child.name)

# Glob
async for p in AsAnyPath("s3://bucket/logs/").glob("**/*.json"):
    print(p)

# Walk (like os.walk)
async for root, dirs, files in AsAnyPath("az://container/prefix").walk():
    for f in files:
        print(root / f)

# Presigned URLs (S3, GCS, Azure)
url = await path.presign(expires=3600, method="GET")

File-like open (lazy range reads)

# Binary streaming — only fetches the bytes you read (no full download)
async with path.open("rb") as f:
    header = f.read(64)

# Text mode with custom buffer size (controls prefetch chunk)
async with path.open("r", buffering=2_000_000) as f:
    first_line = f.readline()

# Sync works too
with path.open("rb") as f:
    f.seek(1024)
    chunk = f.read(512)

Files smaller than the buffer size are fetched in one shot automatically. Supported on S3, GCS, Azure, and Artifactory.

Copy and recursive delete

# Copy a single file (cross-backend supported)
await src.copy(dst)

# Copy entire directory tree
await src.copy(dst, recursive=True)

# Remove directory and all its contents
await path.rmdir(recursive=True)

Copies automatically use spare transfer concurrency for large ranged reads. The default target is 8 streams, with an 8 MiB minimum range size and at most 8 range streams per file. Files smaller than 16 MiB stay single-stream. When enough files are active to meet the target, automatic copies use one request per file instead of adding ranges. The target is best-effort, not a minimum number of requests that must always be launched.

S3, GCS, Azure, Artifactory, SSH, and range-capable HTTP/HTTPS sources use the shared reader for local downloads and source reads in cross-backend copies. FTP/FTPS, sources without size metadata, and HTTP servers without advertised range support retain single-stream downloads. Local-only copies and native server-side copy/rename operations retain their existing implementations. Large S3/Azure uploads also use concurrent parts within this shared budget. GCS resumable sessions remain sequential because the protocol requires ordered offsets.

Concurrent library copies share a default budget within the event loop. To configure a batch explicitly:

import asyncio
from asanypath import TransferBudget

with TransferBudget(floor=8, ceiling=32, min_range_size=8 * 1024 * 1024, per_file_cap=8):
    await asyncio.gather(*(src.copy(dst) for src, dst in pairs))

The ceiling bounds concurrent range reads and upload parts in this budget, not metadata requests, single-request writes, or other processes. Parallel buffers are bounded by active streams times their chunk sizes. Cross-backend copies spool the downloaded source to a temporary file before using the destination's upload API. S3/Azure then upload concurrent parts; destinations without a streaming upload implementation can still buffer the complete write. Same-backend server-side copies are unchanged. floor=1 disables automatic range splitting and makes upload parts sequential. Explicit chunk_size requests retain sequential streaming when splitting would be inappropriate, and chunk_size=0 disables streaming to local paths.

S3/Azure ETags, GCS generations, and HTTP conditional requests pin the source version when available. Other sources are checked against fresh metadata after the download, which cannot guarantee a snapshot if the backend does not expose a reliable change token. Use atomic=True to keep an existing local destination intact on failure. Moves remove the source only after a successful copy. The updated Python package requires the matching native extension for conditional cloud reads.

For synchronous code (no await needed):

from asanypath.sync import AsAnyPath

p = AsAnyPath("s3://bucket/data.parquet")
data = p.read_bytes()  # blocks until complete
p.write_bytes(b"new content")

for child in p.parent.iterdir():  # yields synchronously
    print(child.name)

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:

# 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")

S3 multipart and Azure block uploads send up to 8 parts concurrently by default, filling freed slots without waiting for a whole batch. Concurrent files share the budget rather than each multiplying the target concurrency. At the default 8 MiB part size, a single large upload buffers up to 64 MiB of part payload, plus file spooling, transport and runtime overhead. Native part requests borrow immutable Python bytes instead of copying each buffer again. 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; direct write_bytes() remains a single write.)

Tuning throughput and memory. Buffered part payload scales with part size times concurrent upload slots. A slot is reserved before reading its part, so queued uploads do not allocate unbounded buffers. Tune concurrency with TransferBudget(floor=..., ceiling=..., per_file_cap=...), or CLI --concurrency-floor (also used by cp, mv, and sync). To limit a batch to one in-flight part/range, use TransferBudget(floor=1, ceiling=1). Set part size globally via env, or per call:

export ASANYPATH_UPLOAD_CHUNK_SIZE=33554432   # 32 MiB parts (bytes)
export ASANYPATH_SPOOL_MAX_SIZE=67108864      # 64 MiB before open("wb") spills to disk
# 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.

On S3 failure or cancellation, in-flight workers are cancelled and joined before aborting the multipart upload; completion lists remain in source order. Azure only commits the ordered block list after all stages succeed; abandoned uncommitted blocks are left for Azure's automatic expiry.

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 UnsupportedProtocolError on use.

Download caching (opt-in)

Reading the same object or URL twice always re-downloads it — there is no cache by default, and the read path is unchanged unless you opt in, so non-caching callers pay zero overhead.

Caching is included in the native extension with no extra dependencies or installation extras. Opt in per read with use_cache=True:

from asanypath import AsAnyPath

p = AsAnyPath("s3://bucket/model.bin")
data = await p.read_bytes(use_cache=True)   # first call fetches + stores
data = await p.read_bytes(use_cache=True)   # subsequent calls served from disk
text = await p.read_text(use_cache=True)

Configure the location and limits once (location defaults to ~/.cache/asanypath, honoring $XDG_CACHE_HOME and $ASANYPATH_CACHE_DIR):

from asanypath import configure_cache

configure_cache(
    directory="/var/cache/asanypath",  # default: ~/.cache/asanypath
    size_limit=2 * 1024**3,            # stored key + payload budget (default 1 GiB, LRU)
    ttl=3600,                          # per-entry expiry in seconds (default: none)
    enabled=True,                      # make use_cache default to True process-wide
)

Or via environment: ASANYPATH_CACHE_DIR, ASANYPATH_CACHE_SIZE_LIMIT (bytes), ASANYPATH_CACHE_TTL (seconds).

Behaviour and trade-offs:

  • Off by default, opt-in per call. With configure_cache(enabled=True) the default flips process-wide; an explicit use_cache=False always bypasses.
  • Works for every remote backend (S3, GCS, Azure, Artifactory, HTTP, SSH, FTP). Local paths are never cached.
  • Size-bounded, LRU-evicted; disk I/O runs in a worker thread so the event loop stays responsive. Concurrent reads of the same key collapse to a single fetch within a cache instance. The byte budget counts UTF-8 keys and payloads; SQLite metadata and transaction journals add filesystem overhead. Objects larger than the budget are returned without being cached.
  • Byte-only native storage: SQLite stores raw bytes, never Python pickles. The database lives under native-v1/ in the configured cache directory. Legacy DiskCache files are ignored, not migrated or deleted. Multiple processes can share the new store; use the same size limit across them.
  • Staleness is on you. Entries are keyed by URL with no revalidation — a cached object that changes upstream is served stale until it expires (ttl) or is evicted. Prefer caching immutable/versioned objects, or set a ttl.

Resumable downloads

download(dst) streams an object to a local file and resumes an interrupted transfer instead of starting over:

await AsAnyPath("s3://bucket/huge.bin").download("/data/huge.bin")
# process killed mid-download... run the same call again — it continues
await AsAnyPath("s3://bucket/huge.bin").download("/data/huge.bin")

Bytes stream into <dst>.part with a <dst>.part.meta sidecar recording the object's ETag (or Last-Modified) and total size. On resume the identity is re-checked: if the upstream object is unchanged the download continues from the saved offset; if it changed, the partial is discarded and it restarts cleanly. On completion <dst>.part is atomically renamed to dst.

# tune the range-request size; disable resume to always start fresh
await path.download("/data/huge.bin", chunk_size=32 * 1024 * 1024, resume=False)

Resume needs range reads and a HEAD probe, so it's available on the object stores (S3/GCS/Azure) and Artifactory/HTTP where the server supports it. Backends without those (e.g. FTP) download in a single pass automatically. This needs no extra dependency — resume state is plain sidecar files, independent of the optional download cache.

Installation

Most cloud backends (S3, GCS, Azure, Artifactory, HTTP) are included in the base install — no per-backend extras needed.

The following commands install from a checked-out source repository in a POSIX-compatible shell. Public package-install instructions will be added when the PyPI distributions are published.

# Core development environment (includes the most common cloud backends)
uv sync

# With CLI
uv sync --extra cli

# With FTP backend support (SSH/SFTP is built in — no extra needed)
uv sync --extra ftp

# With OS keyring-backed credential cache
uv sync --extra credstore

# With benchmark dependencies
uv sync --extra bench

# Full install all cloud backends
uv sync --extra all

Cloud Backend Status

Backend Status Authentication Operations
Local FS ✅ Full N/A All async pathlib methods
S3 ✅ Full AWS credentials / SigV4 read/write/list/delete/stat/checksums/presign
GCS ✅ Full Service account / token read/write/list/delete/stat/checksums/presign
Azure ✅ Full Account key / SAS token read/write/list/delete/stat/checksums/presign
Artifactory ✅ Full Bearer token read/write/list/delete/stat/checksums
HTTP(S) ✅ Full N/A read (GET)

CLI Usage

# Install with CLI support
uv sync --extra cli

# List supported protocols
asanypath protocols

# Check if a file exists
asanypath exists s3://bucket/file.txt

# Print file contents
asanypath cat s3://bucket/file.txt

# List directory (Artifactory with token)
asanypath ls art://repo/path/ --token mytoken

# Copy local → cloud
asanypath cp /local/file.txt s3://bucket/file.txt

# Copy cloud → local
asanypath cp s3://bucket/file.txt /local/file.txt

# Allow 32 concurrent files; fill spare capacity toward 8 streams for large files
asanypath cp -r s3://bucket/data/ ./data/ -j 32 --concurrency-floor 8

# Disable automatic range splitting (also supported by mv and sync)
asanypath cp s3://bucket/file.txt ./file.txt --concurrency-floor 1

# Delete a file
asanypath rm s3://bucket/file.txt --force

# Move / rename
asanypath mv s3://bucket/old.txt s3://bucket/new.txt

# Create directory
asanypath mkdir s3://bucket/new-prefix/ --parents

# File statistics
asanypath stat s3://bucket/file.txt

# Presigned URLs (S3, GCS, Azure — 1h default)
asanypath presign s3://bucket/file.txt
asanypath presign gs://bucket/file.txt --expires 7200
asanypath presign az://container/file.txt --method PUT

# Auth diagnostics
asanypath auth show

asanypath auth show s3://bucket

Shell Completions (Beta)

Path completions are available for all subcommands that take cloud paths: ls, cp, mv, rm, stat, cat, exists, touch, checksums, mkdir, presign, and sync.

Completions complete cloud paths up to 50 items deep with shallow enumeration (immediate children only), and are cached for 30 seconds per prefix to avoid redundant backend requests.

Bash Setup:

# One-time for current shell
eval "$(asanypath completion bash)"

# Persist in ~/.bashrc
echo 'eval "$(asanypath completion bash)"' >> ~/.bashrc

Zsh Setup:

# One-time for current shell
eval "$(_ASANYPATH_COMPLETE=zsh_source asanypath)"

# Persist in ~/.zshrc
echo 'eval "$(_ASANYPATH_COMPLETE=zsh_source asanypath)"' >> ~/.zshrc

If you run asanypath from a virtual environment path, use that exact executable in the eval command, for example:

eval "$(/path/to/venv/bin/asanypath completion bash --exe /path/to/venv/bin/asanypath)"

If completions still do not appear, verify and rebind in the current shell:

# Ensure shell command cache is refreshed
hash -r

# Confirm which executable is active
command -v asanypath

# Remove stale completion binding (if any)
complete -r asanypath 2>/dev/null || true

# Rebind completions against the active executable
eval "$(_ASANYPATH_COMPLETE=bash_source "$(command -v asanypath)")"

# Verify completion is registered
complete -p asanypath

Tip: if asanypath comes from a project venv, prefer putting the explicit venv path in your shell rc file instead of relying on PATH ordering.

Usage:

asanypath ls s3://bucket/pre[TAB]      # Completes to matching S3 prefixes
asanypath cp s3://src/fi[TAB] ./[TAB]  # Completes both source and destination
asanypath rm s3://[TAB]                # Lists buckets

Completion respects environment variables for cloud credentials (e.g., AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY for S3). Bearer token auth (for custom endpoints) is not supported in completions; only env var-based auth is used.

Benchmarks

Multi-backend benchmark suite in scripts/bench.py compares asanypath against fsspec, cloudpathlib, and native async SDKs. Run against live services or local emulators (compose.yml provides MinIO, Azurite, and fake-gcs-server).

# Local only (no cloud creds needed)
uv run --extra bench python scripts/bench.py --backend local --rounds 3

# Single backend
uv run --extra bench python scripts/bench.py --backend s3 --rounds 5

# All backends
uv run --extra bench python scripts/bench.py --backend all --rounds 3 --objects 10

# Sweep concurrency levels
uv run --extra bench python scripts/bench.py --backend art --rounds 3 --concurrency-levels 1,10,50

For multipart upload throughput, use scripts/bench_upload.py with real shard files and an explicitly selected S3-compatible endpoint:

uv run --extra bench python scripts/bench_upload.py \
  --destination s3://my-bucket/bench --endpoint-url https://my-storage-endpoint \
  --part-mib 8 --streams 1 8 --rounds 3 /data/shard-001 /data/shard-002

This compares asanypath and boto3 using one shared request budget per client. It warms up each case, reports JSON timings and MiB/s, verifies uploaded contents outside the timed interval, and deletes only the uniquely named benchmark objects it created. Both clients use normal AWS credentials (or --profile). Run with one large shard and again with several representative shards; results from a local test server are not evidence of production endpoint throughput.

S3 — live S3-compatible service (median of 20 rounds, 10 objects, c=1)

Operation asanypath s3fs aiobotocore cloudpathlib
write 8.80 ms 12.67 ms 28.07 ms 41.75 ms
read 3.78 ms 9.10 ms 23.15 ms 12.83 ms
exists 3.57 ms 4.25 ms 21.92 ms 4.02 ms
iterdir 40.55 ms 39.16 ms 56.49 ms 39.07 ms

Azure — Azurite emulator (median of 20 rounds, 10 objects, c=1)

Operation asanypath azure-sdk adlfs cloudpathlib
write 1.24 ms 1.79 ms 2.34 ms 7.88 ms
read 0.51 ms 1.15 ms 2.44 ms 3.19 ms
exists 0.35 ms 0.95 ms 0.98 ms 1.04 ms
iterdir 0.88 ms 2.40 ms 4.11 ms 2.72 ms

GCS — fake-gcs-server emulator (median of 20 rounds, 10 objects, c=1)

Operation asanypath gcloud-aio cloudpathlib gcsfs
write 0.45 ms 0.51 ms 3.53 ms 0.58 ms
read 0.22 ms 0.18 ms 1.48 ms 0.30 ms
exists 0.15 ms 0.18 ms 0.50 ms 0.46 ms
iterdir 0.19 ms 0.25 ms 0.69 ms 0.33 ms

Artifactory — live server (median of 20 rounds, 10 objects, c=1)

Operation asanypath aiohttp requests
write 54.49 ms 54.75 ms 57.57 ms
read 2.83 ms 3.09 ms 12.01 ms
exists 2.33 ms 2.18 ms 4.61 ms
iterdir 2.85 ms 3.07 ms 7.88 ms

SSH / SFTP — localhost emulator (median of 20 rounds, 10 objects, c=1)

asanypath uses a built-in native russh SFTP transport (off-GIL); asyncssh is shown only as a benchmark baseline.

Operation asanypath asyncssh
write 0.16 ms 0.33 ms
read 0.21 ms 0.36 ms
exists 0.07 ms 0.11 ms
iterdir 0.23 ms 0.44 ms

FTP — localhost emulator (median of 20 rounds, 10 objects, c=1)

Operation asanypath aioftp
write 0.35 ms 0.41 ms
read 0.35 ms 0.40 ms
exists 1.00 ms 1.13 ms
iterdir 1.09 ms 1.04 ms

Latencies are per-operation medians over 20 rounds. S3 and Artifactory ran against live services; Azure, GCS, SSH/SFTP, and FTP against local emulators (Azurite, fake-gcs-server, and localhost sftp/ftp), so those figures reflect client-library overhead, not WAN latency. The SSH/SFTP transport is a built-in native russh backend that runs SFTP off-GIL and beats asyncssh on every operation. asanypath matches or beats dedicated client libraries on most operations while providing a single unified API across all backends.

How It Works

AsAnyPath dispatches on the URI scheme to return the right path class:

Protocol Class Transport
file:// or bare path AsyncPath anyio / pathlib
s3:// S3Path reqwest + SigV4
gs://, gcs:// GCSPath reqwest + JSON API
az://, azure:// AzurePath reqwest + SharedKey
art:// ArtifactoryPath reqwest + Bearer
http://, https:// HTTPPath / HTTPSPath reqwest
ssh:// SSHPath native russh (SFTP)
ftp:// FTPPath aioftp
ftps:// FTPSPath aioftp + TLS
any other scheme UnsupportedProtocolPath none — pure-path only

Cloud and HTTP network I/O is handled by asanypath-native (Rust/PyO3 + reqwest) with jittered exponential backoff retry for transient errors. SSH/SFTP uses a built-in off-GIL native russh transport (no Python SSH dependency); FTP/FTPS use aioftp. Unknown schemes construct an UnsupportedProtocolPath: pure-path operations (.name, .parent, joins) work, while any backend operation raises UnsupportedProtocolError.

API Reference

AsAnyPath(path, **kwargs)

Factory that returns the appropriate path instance based on the URI scheme.

Async Methods

All path implementations support:

  • 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
  • download(dst, resume, chunk_size) — stream an object to a local file, resuming an interrupted transfer
  • read_bytes() / write_bytes(data) — read_bytes(use_cache=True) serves from / stores in the opt-in download cache
  • read_text(encoding) / write_text(data, encoding) — read_text also accepts use_cache
  • exists() / is_file() / is_dir()
  • stat() / checksums() (cloud backends)
  • mkdir(parents, exist_ok) / touch(exist_ok)
  • unlink(missing_ok) / rmdir(recursive)
  • iterdir() / rename(target)
  • get_access_policy() / update_access_policy(policy_patch)

Backend Operation Options

Operations which make provider requests accept a keyword-only backend_options value for provider-native configuration. Options are attached to one operation, not to the path or its credentials, and remain distinct when uploads are batched. copy(), move(), rename(), and replace() use destination_backend_options because they configure the destination request.

from asanypath import BackendOptions, S3Path

path = S3Path("s3://bucket/report.json")
await path.write_bytes(
    payload,
    backend_options=BackendOptions(
        headers={"content-type": "application/json", "x-amz-meta-source": "etl"},
        query={"x-id": "PutObject"},
    ),
)

BackendOptions has three independent mappings:

Field Purpose Availability
headers Additional HTTP request headers S3, GCS, Azure, and Artifactory data operations
query Additional HTTP query parameters S3, GCS, Azure, and Artifactory data operations
provider Structured backend-specific operation settings GCS object-resource fields; local policy symlink behavior; Artifactory permission-target selection

Authentication, Host, and Content-Length remain owned by asanypath. On GCS uploads, provider supplies object-resource fields and selects a multipart upload. Other data backends reject unsupported provider values rather than silently dropping them. provider is also used by policy operations as described below; it is not automatically forwarded as HTTP request data.

For S3 metadata replacement, provide both x-amz-metadata-directive: REPLACE and the desired x-amz-meta-* headers. Headers and query parameters are additive: they cannot override credentials or protocol-critical values.

Access Policies

get_access_policy() returns an AccessPolicy with owner, group, normalized grants, and a provider-native provider mapping. A grant principal is backend-defined: local, SSH, and FTP paths use owner, group, and everyone; object stores use provider principal strings such as user:reader@example.com or canonical-user:<id>.

Normalized actions are read, write, and execute. They deliberately do not flatten every provider-specific permission. The complete provider document is kept in policy.provider so a read-modify-replace workflow does not discard permissions, principals, or scope that lack a portable equivalent.

from asanypath import AccessGrant, AccessPolicyPatch, AsyncPath

path = AsyncPath("report.txt")
await path.update_access_policy(
    AccessPolicyPatch(
        grants=(
            AccessGrant("group", frozenset({"read"})),
            AccessGrant("everyone", frozenset()),
        )
    )
)

For local paths, grants map to POSIX owner, group, and other mode bits; owner and group map to ownership changes. Backend-specific controls are passed through BackendOptions, for example BackendOptions(provider={"follow_symlinks": False}) on local paths.

Backend Policy scope Read Update
Local Path POSIX mode and local owner/group names POSIX mode and ownership; follow_symlinks option
SSH/SFTP Path POSIX mode and numeric uid/gid SFTP chmod and chown
FTP/FTPS Path MLST unix.mode, when provided by the server SITE CHMOD, when supported; ownership is unavailable
S3 Object Object ACL Complete s3_acl document
GCS Object Object ACL Complete gcs_acl document
Azure Blob Container Public access and stored SAS policies Complete azure_container_acl document
Artifactory Named permission target Target selected by permission_target option Complete artifactory_permission_target document

S3 and GCS normalize only their unambiguous read/write permissions. Azure maps public blob/container access to an everyone read grant. These calls do not evaluate S3 bucket policies, GCS IAM, Azure RBAC, ADLS Gen2 ACLs, or effective access.

Cloud ACL updates require the complete native document returned by the matching read. This makes replacement explicit and preserves fields outside the portable policy model:

from asanypath import AccessPolicyPatch, AsyncPath

path = AsyncPath("s3://bucket/report.json")
policy = await path.get_access_policy()
await path.update_access_policy(AccessPolicyPatch(provider={"s3_acl": policy.provider["s3_acl"]}))

Artifactory operations are target-scoped rather than artifact-scoped because a target can cover several repositories and path patterns. Select it explicitly for both calls:

from asanypath import AccessPolicyPatch, AsyncPath, BackendOptions

path = AsyncPath("art://artifactory.example.com/artifactory/generic/report.json")
options = BackendOptions(provider={"permission_target": "release-readers"})
policy = await path.get_access_policy(backend_options=options)
await path.update_access_policy(
    AccessPolicyPatch(
        provider={"artifactory_permission_target": policy.provider["artifactory_permission_target"]}
    ),
    backend_options=options,
)

The remote server remains authoritative. Missing privileges, disabled ACL features, unsupported FTP extensions, and invalid provider documents raise the provider's normal authorization or validation error. Missing required local policy context, such as an Artifactory permission target or complete cloud ACL document, raises ValueError before a request is sent.

The remaining cloud backends raise NotImplementedError rather than silently translating provider IAM or ACL models.

Properties

  • name, stem, suffix, parent, parts
  • protocol — URI scheme (s3, gs, az, art, file, …)

Development

Run these commands in a POSIX-compatible shell such as Bash or Zsh:

git clone https://github.com/AxisCommunications/asanypath.git
cd asanypath

# Install with dev dependencies
uv sync --extra dev

# Run tests
uv run pytest

# Build Rust native accelerator (optional)
cd rust && PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv tool run maturin develop --release

License

MIT

Metadata

Release files for asanypath 2.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for asanypath 2.3.1
File Size Uploaded
asanypath-2.3.1.tar.gz 435.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for asanypath 2.3.1
File Interpreter ABI Platform
asanypath-2.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 556.9 kB

Release files / asanypath-2.3.1.tar.gz

Download URL asanypath-2.3.1.tar.gz
Size 435.1 kB
Tags Source
SHA-256 checksum
How to use checksums
63e03970e5ea055aa21d3442fe752d18ad4b3c318d71513268b62f8ccbebaa7d
BLAKE2b-256 checksum
How to use checksums
643d131688fcea7b3955b68ac97df0b810e7f1e93bb8b810c806a88958415f90
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / asanypath-2.3.1-py3-none-any.whl

Download URL asanypath-2.3.1-py3-none-any.whl
Size 121.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7f1bbf782f0025fa2d47ad65d674905c2f8ea81adc19f28422de11611e263065
BLAKE2b-256 checksum
How to use checksums
364df19ffd57305c708bb50d4e68e58ea2066b36f24173b464d10019dca01672
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.3.1 This release

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.1

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page