Skip to content

WASM (Pyodide)

WASM (Pyodide)

The packages are pure Python wheels, so micropip installs them:

import micropip

await micropip.install("capo-s3")
from capo_s3 import AsyncS3Client

async with AsyncS3Client(
    region="eu-central-1",
    credentials={"access_key": "ASIA...", "secret_key": "...", "session_token": "..."},
) as s3:
    await s3.put_object("bucket", "hello.txt", body=b"hi from the browser")

    async with s3.get_object("bucket", "hello.txt") as response:
        async for chunk in response["body"]:
            print(chunk)

In a page:

<script src="https://cdn.jsdelivr.net/pyodide/v314.0.7/full/pyodide.js"></script>
<script type="module">
  const pyodide = await loadPyodide();
  await pyodide.loadPackage("micropip");
  await pyodide.runPythonAsync(`
    import micropip
    await micropip.install("capo-s3")

    from capo_s3 import AsyncS3Client

    async with AsyncS3Client(region="eu-central-1", credentials={...}) as s3:
        async for bucket in s3.iter_list_buckets():
            print(bucket)
  `);
</script>

There is no ~/.aws and no metadata service in a browser, so pass credentials= or fetch them from your backend:

from datetime import datetime

from zapros import AsyncClient

from capo_s3 import AsyncS3Client, CachedProvider, Credentials, CredentialsProvider


class BackendCredentials(CredentialsProvider):
    def resolve_identity(self) -> Credentials:
        raise NotImplementedError("async only")

    async def aresolve_identity(self) -> Credentials:
        response = await AsyncClient().get("https://api.example.com/aws-credentials")
        data = response.json
        return {
            "access_key": data["access_key"],
            "secret_key": data["secret_key"],
            "session_token": data["session_token"],
            "expiration": datetime.fromisoformat(data["expiration"]),
        }


AsyncS3Client(region="eu-central-1", credentials_provider=CachedProvider(BackendCredentials()))

Requests go through the browser's fetch, so the bucket needs a CORS rule for your origin:

await s3.put_bucket_cors(
    "bucket",
    cors_configuration={
        "cors_rules": [
            {
                "allowed_origins": ["https://app.example.com"],
                "allowed_methods": ["GET", "PUT"],
                "allowed_headers": ["*"],
                "expose_headers": ["ETag"],
            }
        ]
    },
)

What is different under Pyodide:

  • Only the Async* clients work; S3Client() raises RuntimeError. asyncio only, no trio.
  • The HTTP handler is zapros' AsyncPyodideHandler, picked automatically.
  • No threads, so Body.async_from_path is unavailable. Pass bytes, an async iterator, or a Body with your own opener.
  • Node works too; that is how the integration tests run in CI.