Ir para o conteúdo

Capacidades nativas

As capacidades (native/) são adaptadores de Web API expostos como awaitables tipados em Python. Você escreve await geolocation.get() e recebe um Position tipado — sem tocar em JavaScript. 📡

Em construção (Trilho N)

Esta camada é o Trilho N do roadmap. As fases N0–N4 estão detalhadas no plano de design. Esta página descreve a superfície planejada e o modelo de dois backends.

Dois backends, uma API Python

O princípio central: cada capacidade tem dois backends, mas a API Python é a mesma. O --mode escolhe o caminho, não o seu código.

A chamada vai direto na Web API via pyodide.ffi, dentro do browser. Sem rede.

pos = await geolocation.get()   # chama navigator.geolocation no browser

A chamada é proxiada por um round-trip: o servidor emite um pedido nativo pelo transporte (WS/SSE), o cliente executa a Web API e devolve o resultado tipado.

pos = await geolocation.get()   # MESMA linha; dispara native_call/native_result

O contrato é o mesmo

O envelope native_call/native_result está no contrato de fronteira. Só o transporte difere — a assinatura tipada mora no contrato, não no transporte.

As capacidades planejadas

Capacidade API Python Espelha (React SDK)
http (N0) await http.request(...), upload, poll, idempotency_key createApiClient/retry
audio (N1) await audio.play(src, volume=...), audio.stop() playAudio/useAudio
share (N2) await share(title=..., url=...)ShareResult share/isShareSupported
geolocation (N3) await geolocation.get()Position
clipboard (N3) await clipboard.read() / clipboard.write(text)
storage (N3) put/get/list (sobre IndexedDB) createOfflineStore
camera (N4) await camera.capture() → bytes/Blob

Exemplo: HTTP tipado com retry

O native.http (N0) é a base do replay offline. Uma requisição com retry e idempotency key:

from tempestweb.native import http
from tempestweb.native.http import RetryOptions


async def submit_order(payload: dict[str, object]) -> dict[str, object]:
    """Submit an order with retry and an idempotency key.

    Args:
        payload: The order body to POST.

    Returns:
        The decoded JSON response.
    """
    key = http.generate_idempotency_key()
    response = await http.request(
        "POST",
        "/api/orders",
        json=payload,
        retry=RetryOptions(attempts=3, backoff=0.5),
        idempotency_key=key,
    )
    return response.json()

Idempotency key evita duplicar efeito

Se o retry reentrega a mesma requisição, a idempotency_key garante que o servidor aplica o efeito uma só vez. Essa é a peça que torna a fila offline do Trilho P segura.

Exemplo: geolocalização

from tempestweb.native import geolocation


async def center_map(app: object) -> None:
    """Read the device position and update the app state.

    Args:
        app: The running app handle.
    """
    pos = await geolocation.get()   # Position(lat=..., lon=...)
    app.set_state(lambda s: setattr(s, "center", (pos.lat, pos.lon)))

Permissão é caminho normal, não exceção fatal

Geolocation, clipboard e camera exigem permissão e contexto seguro (HTTPS). Trate a negação como um fluxo normal — uma exceção tipada que sua UI apresenta com elegância, não um crash.

Câmera no Modo B (sempre no cliente)

A captura de câmera sempre acontece no cliente, mesmo no Modo B. Quando você chama await camera.capture() "no servidor", o round-trip dispara a captura no browser e a foto volta tipada (base64 ou referência de blob).

from tempestweb.native import camera


async def take_photo() -> bytes:
    """Capture a photo from the device camera.

    Returns:
        The captured image bytes.
    """
    blob = await camera.capture()   # captura no cliente; volta tipado no Modo B
    return blob.data

Comprima antes de subir

No Modo B a foto atravessa a rede no round-trip. Comprima no cliente antes de devolver para manter o payload pequeno.

Inferência ONNX no browser (native.onnx)

onnxruntime (a extensão C do CPython) não tem wheel Pyodide — Python no browser não roda um grafo ONNX em-processo. A capacidade onnx cobre o vão: o grafo roda em JavaScript via onnxruntime-web (build WASM), dirigido pela mesma costura native_call. Você faz o pré/pós-processamento em Python (numpy + pillow, ambos no Pyodide) e atravessa só a execução do tensor.

from tempestweb.native import onnx
from tempestweb.native.onnx import Tensor


async def detect(input_b64: str) -> dict[str, Tensor]:
    """Run a YOLO ONNX model loaded same-origin from the artifact."""
    model = await onnx.load("./models/detect.onnx")       # compila a sessão (cache no JS)
    feeds = {model.input_name: Tensor(data_base64=input_b64, dims=[1, 3, 640, 640])}
    return await onnx.run(model.session_id, feeds)         # → {nome: Tensor}

Carregue o onnxruntime-web por [wasm].scripts e vendore-o (e os .onnx) por [wasm].assets, para o service worker precachear tudo e a inferência rodar offline. O provedor wasm é forçado (o build web não tem alguns kernels sob WebGPU). Tensores cruzam como bytes base64 + shape + dtype — a capacidade é numpy-free; o lado Python (que tem numpy) serializa.

Salvar arquivo gerado (native.file)

O browser não tem escrita síncrona de arquivo. file.save entrega um blob gerado em Python por navigator.share({files}) (quando a plataforma aceita) ou por download via <a download> (desktop), reportando qual caminho rodou.

from tempestweb.native import file


async def export_zip(zip_bytes: bytes) -> None:
    """Share or download a generated ZIP."""
    await file.save("historico.zip", zip_bytes, mime_type="application/zip")

Instalação do PWA (native.install)

Exponha o fluxo de instalação do PWA ao Python: saber se o app é instalável (beforeinstallprompt capturado) ou já instalado, e disparar o prompt após um gesto real do usuário.

from tempestweb.native import install


async def on_install_tap() -> None:
    """Fire the native install prompt from a button handler."""
    outcome = await install.prompt()   # "accepted" | "dismissed" | "unavailable"


async def maybe_show_install_button() -> bool:
    """Whether to show an Install button."""
    state = await install.state()      # InstallState(can_install, installed)
    return state.can_install and not state.installed

client/native/install.js envolve o controlador soft de client/pwa/install-prompt.js (suprime o mini-infobar e guarda o evento).

Extras de build do Modo A ([wasm])

Capacidades que dependem de pacotes Pyodide extras, módulos Python próprios, assets estáticos ou libs JS declaram-se no tempestweb.toml:

[wasm]
packages = ["numpy", "pillow"]                 # loadPackage além do pydantic do core
modules  = ["famacha", "ort_vision_sdk"]        # pacotes Python bundlados junto do app.py
assets   = ["models/*.onnx", "vendor/ort/*"]    # copiados (path preservado) + precache
scripts  = ["./vendor/ort/ort.wasm.min.js"]     # <script> injetado antes do bootstrap

De onde vem cada module

Cada nome em modules é resolvido em duas etapas, nesta ordem:

  1. Cópia vendida ao lado do app.py (<projeto>/<module>/), se existir — o comportamento histórico, em que uma cópia versionada no repo vence.
  2. Pacote instalado no ambiente (importlib) — se não houver cópia vendida, o módulo é puxado direto do site-packages do seu .venv.

Ou seja: uma dependência que você instala (uv add ...) não precisa ser clonada e jogada na raiz do repositório para ir pro bundle — basta listá-la em modules. Um nome que não é cópia vendida nem importável falha o build com uma mensagem clara.

Nem precisa listar à mão: tempestweb sync

Para não ter o trabalho de manter modules em dia, rode:

tempestweb sync            # preenche [wasm].modules; --dry-run só mostra

Ele lê as [project.dependencies] do seu pyproject.toml, mantém as que estão instaladas e são puro-Python, e escreve os nomes de import em [wasm].modules — preservando o que já estava lá (o pacote do seu app, cópias vendidas). Pacotes com código nativo (numpy, pillow) são pulados — eles vêm do Pyodide via [wasm].packages — assim como o próprio framework (tempestweb, pydantic). É idempotente: rodar de novo sem mudar o ambiente não escreve nada. Basta ter as dependências no .venv e rodar o comando. 🚀

Recap

  • Capacidades são Web APIs expostas como awaitables tipados em Python.
  • Dois backends, uma API: Modo A chama direto; Modo B proxia por round-trip.
  • O envelope é o native_call/native_result do contrato de fronteira.
  • Permissões negadas são fluxo normal, tratadas como exceção tipada.

A capacidade storage se conecta à camada offline — veja PWA e offline. 🚀