Python & TypeScript · one contract, many providers

Python & TypeScript · um contrato, muitos armazenamentos

Cloud storage,
quick and easy.

Armazenar na nuvem,
rápido e fácil.

Your code asks for reports/2026/summary.csv. Whether that lives on a mounted volume, in an S3 bucket, in Google Cloud Storage or on a remote SFTP server does not change the call — only which distribution you install.

Seu código pede reports/2026/summary.csv. Se isso mora num volume montado, num bucket S3, no Google Cloud Storage ou num servidor SFTP remoto, a chamada não muda — muda apenas qual distribuição você instala.

How a path resolves
Como um caminho é resolvido
bucket · directoryapp-files
prefixreports
path2026/summary.csv
resolves to resolve para app-files/reports/2026/summary.csv
pythontypescript
from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    create_storage_client,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="aws-s3",
        location=StorageLocation(
            bucket="app-files",
            prefix="reports",
        ),
    )
)

await storage.send_content("2026/summary.csv", "id,total\n1,42\n")
text = await storage.read_content("2026/summary.csv")
# app-files/reports/2026/summary.csv
await storage.close()
import { createStorageClient } from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'aws-s3',
    location: { bucket: 'app-files', prefix: 'reports' },
});

await storage.sendContent('2026/summary.csv', 'id,total\n1,42\n');
const text = await storage.readContent('2026/summary.csv');
// app-files/reports/2026/summary.csv
await storage.close();
01

Why use Storage Kernel

Por que usar o Storage Kernel

One contract in front of four providers, and what that buys you.

Um contrato na frente de quatro armazenamentos (no código, o provider_nameproviderName), e o que você ganha com isso.

  • Ask for a path, get the file. One call reads or writes text, bytes or a stream. There is no SDK to learn for each service.
  • Every provider behaves the same. Path resolution, defaults and errors do not change with the provider: a missing file is StoragePathNotFoundError, code strg_path_not_found, on a local disk, on S3, on GCP Storage and over SFTP. All four pass the same contract test suite.
  • Change providers, not code. Moving from S3 to GCP Storage changes the provider, its options and the installed package. Every read_contentreadContent call stays as written.
  • Tests without the cloud. The local-filesystem provider follows the same contract, so tests and CI make the same calls against a directory, with no network and no credentials.
  • What a service cannot do, the call says. A signed URL on a local disk raises StorageCapabilityNotSupportedError instead of returning a link that would not work.
  • Python and TypeScript, one contract. Each follows its own convention — read_content, readContent — and both share the behavior, the defaults and the error codes.
  • Peça um caminho, receba o arquivo. Uma chamada lê ou escreve texto, bytes ou um stream. Não há SDK para aprender a cada serviço.
  • Todo armazenamento se comporta igual. A resolução de caminhos, os defaults e os erros não mudam com o armazenamento: um arquivo ausente é StoragePathNotFoundError, código strg_path_not_found, num disco local, no S3, no GCP Storage e via SFTP. Os quatro passam pela mesma suíte de testes de contrato.
  • Troque o armazenamento, não o código. Sair do S3 para o GCP Storage muda o armazenamento, suas opções e o pacote instalado. Toda chamada a read_contentreadContent continua como foi escrita.
  • Testes sem nuvem. O armazenamento local-filesystem segue o mesmo contrato, então testes e CI fazem as mesmas chamadas contra um diretório, sem rede e sem credencial.
  • O que um serviço não faz, a chamada diz. Uma URL assinada num disco local levanta StorageCapabilityNotSupportedError em vez de devolver um link que não funcionaria.
  • Python e TypeScript, um contrato. Cada um segue a convenção da sua linguagem — read_content, readContent — e os dois compartilham comportamento, defaults e códigos de erro.
from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    StoragePathNotFoundError,
    create_storage_client,
)
from storage_kernel.provider_aws_s3 import (
    AwsS3StorageProviderOptions,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="aws-s3",
        location=StorageLocation(
            bucket="app-files",
            prefix="reports",
        ),
        options=AwsS3StorageProviderOptions(
            region="us-east-1",
        ),
    )
)

try:
    text = await storage.read_content("2026/summary.csv")
except StoragePathNotFoundError as error:
    error.code  # "strg_path_not_found", on any provider
from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    StoragePathNotFoundError,
    create_storage_client,
)
from storage_kernel.provider_gcp_storage import (
    GcpStorageProviderOptions,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="gcp-storage",
        location=StorageLocation(
            bucket="app-files",
            prefix="reports",
        ),
        options=GcpStorageProviderOptions(
            project_id="my-gcp-project",
        ),
    )
)

try:
    text = await storage.read_content("2026/summary.csv")
except StoragePathNotFoundError as error:
    error.code  # "strg_path_not_found", on any provider
from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    StoragePathNotFoundError,
    create_storage_client,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="local-filesystem",
        location=StorageLocation(
            directory="./var/storage",
            prefix="reports",
        ),
    )
)

try:
    text = await storage.read_content("2026/summary.csv")
except StoragePathNotFoundError as error:
    error.code  # "strg_path_not_found", on any provider
import {
    StoragePathNotFoundError,
    createStorageClient,
} from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'aws-s3',
    location: { bucket: 'app-files', prefix: 'reports' },
    options: { region: 'us-east-1' },
});

try {
    const text = await storage.readContent('2026/summary.csv');
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found', on any provider
    }
}
import {
    StoragePathNotFoundError,
    createStorageClient,
} from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'gcp-storage',
    location: { bucket: 'app-files', prefix: 'reports' },
    options: { projectId: 'my-gcp-project' },
});

try {
    const text = await storage.readContent('2026/summary.csv');
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found', on any provider
    }
}
import {
    StoragePathNotFoundError,
    createStorageClient,
} from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'local-filesystem',
    location: {
        directory: './var/storage',
        prefix: 'reports',
    },
});

try {
    const text = await storage.readContent('2026/summary.csv');
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found', on any provider
    }
}
02

Install and first read

Instalação e primeira leitura

Nothing is bundled. Install the factory and the one provider you need.

Nada vem embutido. Instale a factory e o pacote do único armazenamento de que você precisa.

Step 1 · install the factory

Passo 1 · instale a factory

Your code imports everything from the factory: the location, the option classes, the errors and the types of the contract come through it. The contract and the shared core install with it, as dependencies. No provider yet — that is step 2, and it is a separate distribution so a service that only touches S3 never pulls a Google SDK or an SSH library.

O seu código importa tudo da factory: a location, as classes de opções, os erros e os tipos do contrato vêm por ela. O contrato e o core compartilhado são instalados junto, como dependências. Nenhum armazenamento ainda — isso é o passo 2, e ele é uma distribuição separada, então um serviço que só toca S3 nunca baixa um SDK do Google nem uma biblioteca de SSH.

contracts depends on nothing. A consumer that only declares types or catches storage errors installs that one package alone.

contracts não depende de nada. Quem só declara tipos ou captura erros de storage instala apenas esse pacote.

uvnpmshell
$ uv add storage-kernel-factory
$ npm install @storage-kernel/factory

Step 2 · add a provider and read

Passo 2 · adicione um armazenamento e leia

Pick where the file lives. The call is identical on all four — only the provider name, the location field it roots on and its typed options change, which is the whole point of the contract.

Escolha onde o arquivo mora. A chamada é idêntica nos quatro — mudam apenas o nome do armazenamento, o campo da location em que ele se enraíza e suas opções tipadas, que é justamente o propósito do contrato.

For local development, CI and tests. location.directory is the root and is mandatory; every path resolves below it, and one that tries to escape is rejected with StorageInvalidPathError.

Para desenvolvimento local, CI e testes. location.directory é a raiz e é obrigatório; todo caminho resolve abaixo dela, e um que tente escapar é rejeitado com StorageInvalidPathError.

Parent directories are created for you, because create_directory_if_not_existscreateDirectoryIfNotExists defaults to true. The aliases local and filesystem also select this provider.

Os diretórios pai são criados para você, porque create_directory_if_not_existscreateDirectoryIfNotExists vem como verdadeiro. Os aliases local e filesystem também selecionam este armazenamento.

It takes no options. public_base_urlpublicBaseUrl is the only one, and only get_public_urlgetPublicUrl needs it.

Ele não precisa de opções. public_base_urlpublicBaseUrl é a única, e só get_public_urlgetPublicUrl precisa dela.

uvnpmshell
$ uv add storage-kernel-provider-local-filesystem
$ npm install @storage-kernel/provider-local-filesystem
local-filesystem
from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    StoragePathNotFoundError,
    create_storage_client,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="local-filesystem",
        location=StorageLocation(
            directory="./var/storage",
            prefix="reports",
        ),
    )
)

try:
    text = await storage.read_content("2026/summary.csv")
    # reads ./var/storage/reports/2026/summary.csv
except StoragePathNotFoundError as error:
    error.code  # "strg_path_not_found", on any provider
import {
    StoragePathNotFoundError,
    createStorageClient,
} from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'local-filesystem',
    location: { directory: './var/storage', prefix: 'reports' },
});

try {
    const text = await storage.readContent('2026/summary.csv');
    // reads ./var/storage/reports/2026/summary.csv
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found', on any provider
    }
}

For objects in Amazon S3. location.bucket is mandatory; location.prefix is optional and may be empty.

Para objetos no Amazon S3. location.bucket é obrigatório; location.prefix é opcional e pode ser vazio.

Pass credentials to be explicit, or leave them out and the AWS SDK's own credential chain applies. They are the keyword arguments boto3 takes for a client — aws_access_key_id, aws_secret_access_key. They are the SDK's own shape — accessKeyId, secretAccessKey.

Passe credentials para ser explícito, ou omita e a própria cadeia de credenciais do SDK da AWS se aplica. Elas são os argumentos nomeados que o boto3 recebe para um client — aws_access_key_id, aws_secret_access_key. Elas têm o formato do próprio SDK — accessKeyId, secretAccessKey.

uvnpmshell
$ uv add storage-kernel-provider-aws-s3
$ npm install @storage-kernel/provider-aws-s3
aws-s3
from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    StoragePathNotFoundError,
    create_storage_client,
)
from storage_kernel.provider_aws_s3 import (
    AwsS3StorageProviderOptions,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="aws-s3",
        location=StorageLocation(
            bucket="app-files",
            prefix="reports",
        ),
        options=AwsS3StorageProviderOptions(
            region="us-east-1",
            credentials={
                "aws_access_key_id": "AKIAIOSFODNN7EXAMPLE",
                "aws_secret_access_key": (
                    "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
                ),
            },
        ),
    )
)

try:
    text = await storage.read_content("2026/summary.csv")
    # reads key reports/2026/summary.csv in app-files
except StoragePathNotFoundError as error:
    error.code  # "strg_path_not_found", on any provider
import {
    StoragePathNotFoundError,
    createStorageClient,
} from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'aws-s3',
    location: { bucket: 'app-files', prefix: 'reports' },
    options: {
        region: 'us-east-1',
        credentials: {
            accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
            secretAccessKey:
                'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
        },
    },
});

try {
    const text = await storage.readContent('2026/summary.csv');
    // reads key reports/2026/summary.csv in app-files
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found', on any provider
    }
}

For objects in Google Cloud Storage. location.bucket is mandatory; location.prefix is optional and may be empty.

Para objetos no Google Cloud Storage. location.bucket é obrigatório; location.prefix é opcional e pode ser vazio.

credentials is the service account key file, as Google ships it, and project_idprojectId names the project. Leave them out and Application Default Credentials apply.

credentials é o arquivo de chave da service account, como o Google o entrega, e project_idprojectId nomeia o projeto. Omita e as Application Default Credentials se aplicam.

uvnpmshell
$ uv add storage-kernel-provider-gcp-storage
$ npm install @storage-kernel/provider-gcp-storage
gcp-storage
import json
from pathlib import Path

from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    StoragePathNotFoundError,
    create_storage_client,
)
from storage_kernel.provider_gcp_storage import (
    GcpStorageProviderOptions,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="gcp-storage",
        location=StorageLocation(
            bucket="app-files",
            prefix="reports",
        ),
        options=GcpStorageProviderOptions(
            project_id="my-gcp-project",
            credentials=json.loads(
                Path("service-account.json").read_text()
            ),
        ),
    )
)

try:
    text = await storage.read_content("2026/summary.csv")
    # reads object reports/2026/summary.csv in app-files
except StoragePathNotFoundError as error:
    error.code  # "strg_path_not_found", on any provider
import { readFileSync } from 'node:fs';
import {
    StoragePathNotFoundError,
    createStorageClient,
} from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'gcp-storage',
    location: { bucket: 'app-files', prefix: 'reports' },
    options: {
        projectId: 'my-gcp-project',
        credentials: JSON.parse(
            readFileSync('service-account.json', 'utf8'),
        ),
    },
});

try {
    const text = await storage.readContent('2026/summary.csv');
    // reads object reports/2026/summary.csv in app-files
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found', on any provider
    }
}

For directories on a remote server over SSH. The root is location.directory, then root_directoryrootDirectory, then the server's .. The alias ssh-sftp also selects this provider.

Para diretórios num servidor remoto via SSH. A raiz é location.directory, depois root_directoryrootDirectory, depois o . do servidor. O alias ssh-sftp também seleciona este armazenamento.

host, username, a password or a key, and exactly one way to verify the server are required. There is no default host policy — see Providers.

host, username, uma senha ou uma chave, e exatamente uma forma de verificar o servidor são obrigatórios. Não existe política de host padrão — veja Armazenamentos.

uvnpmshell
$ uv add storage-kernel-provider-sftp
$ npm install @storage-kernel/provider-sftp
sftp
from storage_kernel.factory import (
    CreateStorageClientConfig,
    StorageLocation,
    StoragePathNotFoundError,
    create_storage_client,
)
from storage_kernel.provider_sftp import (
    SftpStorageProviderOptions,
)

storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="sftp",
        location=StorageLocation(
            directory="/srv/upload",
            prefix="reports",
        ),
        options=SftpStorageProviderOptions(
            host="sftp.example.com",
            username="app",
            private_key_path="/run/secrets/id_ed25519",
            known_hosts_path="/home/app/.ssh/known_hosts",
        ),
    )
)

try:
    text = await storage.read_content("2026/summary.csv")
    # reads /srv/upload/reports/2026/summary.csv
except StoragePathNotFoundError as error:
    error.code  # "strg_path_not_found", on any provider
import {
    StoragePathNotFoundError,
    createStorageClient,
} from '@storage-kernel/factory';

const storage = await createStorageClient({
    providerName: 'sftp',
    location: { directory: '/srv/upload', prefix: 'reports' },
    options: {
        host: 'sftp.example.com',
        username: 'app',
        privateKeyPath: '/run/secrets/id_ed25519',
        knownHostsPath: '/home/app/.ssh/known_hosts',
    },
});

try {
    const text = await storage.readContent('2026/summary.csv');
    // reads /srv/upload/reports/2026/summary.csv
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found', on any provider
    }
}
03

How a path resolves

Como um caminho é resolvido

A location roots the client. The path each call asks for is resolved under it, and the provider maps the result to whatever its service calls a place.

Uma location enraíza o client. O caminho que cada chamada pede é resolvido abaixo dela, e o armazenamento mapeia o resultado para o que o serviço dele chama de lugar.

location.bucket · location.prefix · path
app-files    /reports    /2026/summary.csv
app-files/reports/2026/summary.csv

On S3 that is an object key under a bucket; on the local filesystem a file under a directory; over SFTP a path under a remote directory. The call that produced it was the same in all three.

No S3 isso é uma chave de objeto sob um bucket; no filesystem local, um arquivo sob um diretório; via SFTP, um caminho sob um diretório remoto. A chamada que produziu isso foi a mesma nos três casos.

FieldCampo Used byUsado por RuleRegra
location.directory local-filesystem
sftp
The filesystem root. Mandatory and non-empty for the local provider. For SFTP it selects the remote root, falling back to root_directoryrootDirectory and then to .. A raiz no filesystem. Obrigatória e não-vazia no armazenamento local. No SFTP ela seleciona a raiz remota, caindo para root_directoryrootDirectory e depois para ..
location.bucket aws-s3
gcp-storage
The object-storage bucket. Mandatory for both cloud providers, and the only place a bucket name is ever written. O bucket de object storage. Obrigatório nos dois armazenamentos de nuvem, e o único lugar onde um nome de bucket é escrito.
location.prefix all fouros quatro A namespace below the root. Optional, and may be empty. Directory entries come back with it already stripped off, so a listing reads in the same terms the call was written in. Um namespace abaixo da raiz. Opcional, e pode ser vazio. As entradas de diretório voltam já sem ele, então uma listagem se lê nos mesmos termos em que a chamada foi escrita.
path all fouros quatro What the call asks for, as a plain string. Always / separated, whatever the platform underneath. O que a chamada pede, como string simples. Sempre separado por /, seja qual for a plataforma embaixo.

A field a provider does not read is ignored, not rejected. A directory handed to S3 never becomes part of the object key, and nothing reports it — the write lands at the bucket root instead of under the namespace you meant.

Um campo que o armazenamento não lê é ignorado, não rejeitado. Um directory entregue ao S3 nunca entra na chave do objeto, e nada avisa — a escrita cai na raiz do bucket em vez de dentro do namespace que você quis.

Normalization

Normalização

The same everywhere: backslashes become /, duplicate separators collapse, a leading ./ and any leading or trailing slash are dropped, and a .. segment is rejected with StorageInvalidPathError.

Igual em todo lugar: contrabarras viram /, separadores duplicados colapsam, um ./ inicial e qualquer barra no começo ou no fim caem fora, e um segmento .. é rejeitado com StorageInvalidPathError.

The local provider adds one more check after resolving: the absolute result has to stay below the configured directory, so a symlink or an oddly encoded path cannot walk out of it.

O armazenamento local acrescenta mais uma checagem depois de resolver: o resultado absoluto precisa continuar abaixo do diretório configurado, então um symlink ou um caminho com encoding estranho não consegue sair dele.

An operation-level location replaces the client's entirely. It is not merged field by field, so naming another bucket for one call cannot leave the old prefix quietly attached.

Uma location no nível da operação substitui a do client por inteiro. Ela não é mesclada campo a campo, então nomear outro bucket numa chamada não deixa o prefix antigo grudado em silêncio.

per-call locationlocation por chamada
from storage_kernel.factory import (
    StorageLocation,
    StorageOperationOptions,
)

# archive/reports/2026/summary.csv — another bucket,
# and the prefix comes from this location, not the client's
await storage.read_binary(
    "2026/summary.csv",
    StorageOperationOptions(
        location=StorageLocation(
            bucket="archive", prefix="reports"
        )
    ),
)

# rejected: StorageInvalidPathError
await storage.read_binary("../etc/passwd")
// archive/reports/2026/summary.csv — another bucket,
// and the prefix comes from this location, not the client's
await storage.readBinary(
    '2026/summary.csv',
    { location: { bucket: 'archive', prefix: 'reports' } },
);

// rejected: StorageInvalidPathError
await storage.readBinary('../etc/passwd');
04

Text, bytes and streams

Texto, bytes e streams

Seventeen methods in the contract, and the four you reach for first move whole values.

Dezessete métodos no contrato, e os quatro que você usa primeiro movem valores inteiros.

Whole values

Valores inteiros

Text goes through send_contentsendContent and read_contentreadContent, UTF-8 unless encoding says otherwise. Bytes go through send_binarysendBinary and read_binaryreadBinary.

Texto passa por send_contentsendContent e read_contentreadContent, em UTF-8 a menos que encoding diga outra coisa. Bytes passam por send_binarysendBinary e read_binaryreadBinary.

A full read buffers the entire payload, which is exactly why the stream methods exist beside them. A missing file is StoragePathNotFoundError, on every provider.

Uma leitura completa carrega o payload inteiro na memória, que é justamente por que os métodos de stream existem ao lado. Um arquivo ausente é StoragePathNotFoundError, em todo armazenamento.

A write can carry content_typecontentType and object metadata, and can turn overwrite off for that call alone.

Uma escrita pode carregar content_typecontentType e metadata do objeto, e pode desligar o overwrite só naquela chamada.

whole valuesvalores inteiros
from storage_kernel.factory import SendContentOptions

await storage.send_content(
    "docs/readme.md",
    "# Hello",
    SendContentOptions(
        content_type="text/markdown",
        overwrite=False,
    ),
)

text = await storage.read_content("docs/readme.md")
data = await storage.read_binary("images/avatar.png")
await storage.sendContent(
    'docs/readme.md',
    '# Hello',
    { contentType: 'text/markdown', overwrite: false },
);

const text = await storage.readContent('docs/readme.md');
const data = await storage.readBinary('images/avatar.png');

Streams

Streams

read_streamreadStream hands back an async byte iterable that carries close(). Reading to the end releases it; a reader that stops early has to call close(), which is idempotent, so calling it anyway is always correct.

read_streamreadStream devolve um iterável assíncrono de bytes que carrega close(). Ler até o fim o libera; quem para antes precisa chamar close(), que é idempotente, então chamá-lo de qualquer jeito está sempre certo.

create_upload_streamcreateUploadStream hands back a writable handle with five methods: write, write_textwriteText, write_text_linewriteTextLine, then close() to finish or abort() to discard. close() waits for the provider to actually finish, and after either one another write fails with a normalized error rather than quietly doing nothing.

create_upload_streamcreateUploadStream devolve um handle de escrita com cinco métodos: write, write_textwriteText, write_text_linewriteTextLine, e então close() para concluir ou abort() para descartar. close() espera o armazenamento realmente concluir, e depois de qualquer um dos dois outra escrita falha com um erro normalizado em vez de não fazer nada em silêncio.

send_streamsendStream takes text, bytes or a stream and writes it in parts through that same handle, never buffering the whole payload.

send_streamsendStream recebe texto, bytes ou um stream e escreve em partes por esse mesmo handle, sem nunca acumular o payload inteiro.

On S3 the provider runs a multipart upload of part_size parts, tuned by MultipartUploadOptions with queue_size and leave_parts_on_error; enabled=False buffers to a single PutObject. On GCP Storage the upload is resumable, and part_size sets its chunk size, 256 KiB at least.

No S3 o armazenamento faz um upload multipart em partes de part_size, ajustado por MultipartUploadOptions com queue_size e leave_parts_on_error; enabled=False acumula tudo num único PutObject. No GCP Storage o upload é retomável, e part_size define o tamanho de cada pedaço, no mínimo 256 KiB.

On S3 this is the SDK's managed upload, started with the stream. MultipartUploadOptions tunes partSize, queueSize and leavePartsOnError; enabled: false buffers to a single PutObject. On GCP Storage, enabled maps to a resumable upload.

No S3 isso é o upload gerenciado do SDK, iniciado junto com o stream. MultipartUploadOptions ajusta partSize, queueSize e leavePartsOnError; enabled: false acumula tudo num único PutObject. No GCP Storage, enabled vira um upload retomável.

streaming both waysstream nos dois sentidos
# read without holding the file in memory
stream = await storage.read_stream("archive/2026.zip")
try:
    async for chunk in stream:
        process(chunk)
finally:
    await stream.close()

# write progressively; close() commits, abort() discards
upload = await storage.create_upload_stream("logs/app.ndjson")
try:
    for event in events:
        await upload.write_text_line(json.dumps(event))
    await upload.close()
except BaseException:
    await upload.abort()
    raise
// read without holding the file in memory
const stream = await storage.readStream('archive/2026.zip');
try {
    for await (const chunk of stream) {
        process(chunk);
    }
} finally {
    await stream.close();
}

// write progressively; close() commits, abort() discards
const upload = await storage.createUploadStream(
    'logs/app.ndjson',
);
try {
    for (const event of events) {
        await upload.writeTextLine(JSON.stringify(event));
    }
    await upload.close();
} catch (error) {
    await upload.abort(error);
    throw error;
}
MethodMétodo ContractContrato
Read · 3Leitura · 3
read_binaryreadBinaryLoads the complete file into memory.Carrega o arquivo inteiro na memória.
read_contentreadContentLoads and decodes complete text; UTF-8 by default.Carrega e decodifica o texto inteiro; UTF-8 por padrão.
read_streamreadStreamReturns an async byte iterable that carries close().Devolve um iterável assíncrono de bytes que carrega close().
Write · 4Escrita · 4
send_contentsendContentEncodes and writes complete text.Codifica e escreve o texto inteiro.
send_binarysendBinaryWrites complete bytes.Escreve os bytes inteiros.
send_streamsendStreamSends text, bytes or a stream in parts, never buffering the whole payload.Envia texto, bytes ou um stream em partes, sem nunca acumular o payload inteiro.
create_upload_streamcreateUploadStreamReturns a progressive upload handle.Devolve um handle de upload progressivo.
Delete · 2Remoção · 2
delete_filedeleteFileIdempotently deletes one file. Deleting what is already gone succeeds.Remove um arquivo de forma idempotente. Remover o que já sumiu dá certo.
delete_directorydeleteDirectoryDeletes a directory or object prefix; recursive by default.Remove um diretório ou prefixo de objetos; recursivo por padrão.
Directory and metadata · 4Diretório e metadados · 4
read_directoryreadDirectoryReturns sorted, normalized entries.Devolve entradas normalizadas e ordenadas.
get_directory_content_lengthgetDirectoryContentLengthSums the listed file sizes in bytes; directory entries count zero.Soma em bytes os tamanhos dos arquivos listados; entradas de diretório contam zero.
check_path_existscheckPathExistsChecks a file, or the provider's notion of a directory.Checa um arquivo, ou a noção de diretório do armazenamento.
get_file_infogetFileInfoReturns reliable metadata, or a result that simply says it does not exist.Devolve metadados confiáveis, ou um resultado que apenas diz que não existe.
Copy and URLs · 3Cópia e URLs · 3
native_copy_filenativeCopyFileCopies within one provider using its own mechanism.Copia dentro de um armazenamento usando o mecanismo dele.
create_signed_urlcreateSignedUrlCreates a signed read, write or delete URL, where the service has them.Cria uma URL assinada de leitura, escrita ou remoção, onde o serviço as tem.
get_public_urlgetPublicUrlResolves a provider or configured public URL.Resolve uma URL pública do armazenamento ou configurada.
Lifecycle · 1Ciclo de vida · 1
closeReleases only what the kernel created. An injected client is left alone.Libera apenas o que o kernel criou. Um client injetado fica intocado.
05

Directories and metadata

Diretórios e metadados

Listings are recursive and include directories, unless you say otherwise. Object storage has no directories, so they are synthesized — the same four combinations behave the same on all four providers.

As listagens são recursivas e incluem diretórios, a menos que você diga o contrário. Object storage não tem diretórios, então eles são sintetizados — as mesmas quatro combinações se comportam igual nos quatro armazenamentos.

read_directoryreadDirectory returns entries sorted by path, with the configured prefix already stripped, so a listing reads in the same terms the call was written in.

read_directoryreadDirectory devolve entradas ordenadas por caminho, já sem o prefix configurado, então a listagem se lê nos mesmos termos em que a chamada foi escrita.

Both defaults are exported rather than hidden: DEFAULT_READ_DIRECTORY_RECURSIVE and DEFAULT_READ_DIRECTORY_INCLUDE_DIRECTORIES, both true.

Os dois defaults são exportados em vez de escondidos: DEFAULT_READ_DIRECTORY_RECURSIVE e DEFAULT_READ_DIRECTORY_INCLUDE_DIRECTORIES, os dois verdadeiros.

On S3 and GCP Storage a directory exists when at least one object lives below the prefix — that is what check_path_existscheckPathExists answers there.

No S3 e no GCP Storage um diretório existe quando ao menos um objeto mora abaixo do prefixo — é isso que check_path_existscheckPathExists responde ali.

get_file_infogetFileInfo does not raise for a path that is not there. It returns the path you asked for with exists=Falseexists: false, because "is it there" and "something went wrong" are different questions.

get_file_infogetFileInfo não levanta erro para um caminho que não está lá. Devolve o caminho que você pediu com exists=Falseexists: false, porque "está lá?" e "deu errado?" são perguntas diferentes.

Deleting a directory is recursive by default. Non-recursively, a cloud provider refuses while nested objects still exist rather than deleting part of the tree.

Remover um diretório é recursivo por padrão. Sem recursão, um armazenamento de nuvem recusa enquanto ainda houver objetos aninhados, em vez de apagar parte da árvore.

listinglistagem
from storage_kernel.factory import ReadDirectoryOptions

# everything below reports/, files and directories
entries = await storage.read_directory("reports")

# only the files directly inside reports/
files = await storage.read_directory(
    "reports",
    ReadDirectoryOptions(
        recursive=False, include_directories=False
    ),
)

total = await storage.get_directory_content_length("reports")
info = await storage.get_file_info("reports/2026/summary.csv")
# info.exists, info.content_length, info.updated_at
// everything below reports/, files and directories
const entries = await storage.readDirectory('reports');

// only the files directly inside reports/
const files = await storage.readDirectory(
    'reports',
    { recursive: false, includeDirectories: false },
);

const total = await storage.getDirectoryContentLength(
    'reports',
);
const info = await storage.getFileInfo(
    'reports/2026/summary.csv',
);
// info.exists, info.contentLength, info.updatedAt
recursive include_directoriesincludeDirectories ReturnsDevolve
✓✓ Everything below the path, files and directories. The default. Tudo abaixo do caminho, arquivos e diretórios. O padrão.
✓— Every file below the path, at any depth. Todo arquivo abaixo do caminho, em qualquer profundidade.
—✓ Direct files, plus the directories directly inside. Arquivos diretos, mais os diretórios imediatamente dentro.
—— Direct files only. Só os arquivos diretos.
06

What each provider can actually do

O que cada armazenamento realmente faz

The differences sit in the contract rather than behind it. A capability a service does not have raises at the call, with the provider named, instead of being faked.

As diferenças ficam no contrato, não atrás dele. Uma capacidade que um serviço não tem levanta erro na chamada, com o armazenamento nomeado, em vez de ser fingida.

CapabilityCapacidade local aws-s3 gcp-storage sftp
Empty path addresses the rootCaminho vazio endereça a raiz —✓✓✓
Real read streamStream de leitura real ✓✓✓✓
Progressive upload streamStream de upload progressivo ✓✓✓✓
Native copyCópia nativa ✓✓✓ streamedvia streamin memoryem memória
Signed URL — read, write, deleteURL assinada — leitura, escrita, remoção —✓✓—
Public URL without configurationURL pública sem configuração —✓✓—
Public URL from a configured baseURL pública a partir de uma base configurada ✓✓✓✓
Caller-owned injected clientClient injetado, do chamador —✓✓✓

✓ supported · — not available, and the call raises rather than pretending: StorageCapabilityNotSupportedError for a signed URL, StorageKernelConfigurationError for a public URL with no public_base_urlpublicBaseUrl. The local filesystem provider has no SDK client to inject.

✓ suportado · — não disponível, e a chamada levanta erro em vez de fingir: StorageCapabilityNotSupportedError para uma URL assinada, StorageKernelConfigurationError para uma URL pública sem public_base_urlpublicBaseUrl. O armazenamento de filesystem local não tem client de SDK para injetar.

URLs and copies

URLs e cópias

create_signed_urlcreateSignedUrl takes an action — read, write or delete — and expires_in_secondsexpiresInSeconds. A signed write forwards content_typecontentType and string metadata to the signature.

create_signed_urlcreateSignedUrl recebe uma action — read, write ou delete — e expires_in_secondsexpiresInSeconds. Uma escrita assinada encaminha content_typecontentType e metadados de string para a assinatura.

Both clouds sign with Signature Version 4, so a URL lives seven days at most. A boto3 client you inject has to be built with Config(signature_version="s3v4").

As duas nuvens assinam com a Signature Version 4, então uma URL vive no máximo sete dias. Um client boto3 que você injeta precisa ser construído com Config(signature_version="s3v4").

get_public_urlgetPublicUrl percent-encodes each path segment and keeps /, so a # or ? in a name survives. It resolves a URL and nothing else: it does not change permissions and does not promise the object is readable. Unlike a signed URL it carries no expiry, because a public URL is public for as long as the object is.

get_public_urlgetPublicUrl aplica percent-encoding em cada segmento e preserva /, então um # ou ? num nome sobrevive. Ela resolve uma URL e nada mais: não muda permissões nem promete que o objeto é legível. Diferente de uma URL assinada, ela não tem validade, porque uma URL pública é pública enquanto o objeto for.

native_copy_filenativeCopyFile copies inside one provider using its own mechanism — CopyObject on S3, a filesystem copy locally, and over SFTP a remote read stream piped into a remote write streama remote read into memory and a remote write. The destination resolves from destination_locationdestinationLocation, then the operation's location, then the client's, so a copy given only a location lands beside its source. The overwrite check always runs at the destination.

native_copy_filenativeCopyFile copia dentro de um armazenamento usando o mecanismo dele — CopyObject no S3, uma cópia de filesystem localmente, e no SFTP um stream de leitura remoto canalizado para um stream de escrita remotouma leitura remota para a memória e uma escrita remota. O destino resolve a partir de destination_locationdestinationLocation, depois da location da operação, depois da do client, então uma cópia que só recebe uma location cai ao lado da origem. A verificação de overwrite sempre roda no destino.

signed URL and copyURL assinada e cópia
from storage_kernel.factory import (
    CreateSignedUrlOptions,
    NativeCopyFileOptions,
    StorageLocation,
    StorageSignedUrlAction,
)

signed = await storage.create_signed_url(
    "2026/summary.csv",
    CreateSignedUrlOptions(
        action=StorageSignedUrlAction.READ,
        expires_in_seconds=900,
    ),
)
# signed.url, signed.expires_at, signed.method

await storage.native_copy_file(
    "2026/summary.csv",
    "2026/summary.csv",
    NativeCopyFileOptions(
        destination_location=StorageLocation(bucket="archive"),
    ),
)
const signed = await storage.createSignedUrl(
    '2026/summary.csv',
    { action: 'read', expiresInSeconds: 900 },
);
// signed.url, signed.expiresAt, signed.method

await storage.nativeCopyFile(
    '2026/summary.csv',
    '2026/summary.csv',
    { destinationLocation: { bucket: 'archive' } },
);
07

Four providers, one call

Quatro armazenamentos, uma chamada

Provider options are typed per provider and never reach the shared contract. Each is its own distribution, so you install exactly the SDKs you use.

As opções de cada armazenamento são tipadas por armazenamento e nunca chegam ao contrato compartilhado. Cada um é uma distribuição própria, então você instala exatamente os SDKs que usa.

Provider DistributionDistribuição Roots onEnraíza em Use forUsar para
local-filesystem storage-kernel-provider-local-filesystem@storage-kernel/provider-local-filesystem directory Local development, CI and tests. Also local and filesystem. Desenvolvimento local, CI e testes. Também local e filesystem.
aws-s3 storage-kernel-provider-aws-s3@storage-kernel/provider-aws-s3 bucket Objects in Amazon S3. Objetos no Amazon S3.
gcp-storage storage-kernel-provider-gcp-storage@storage-kernel/provider-gcp-storage bucket Objects in Google Cloud Storage. Objetos no Google Cloud Storage.
sftp storage-kernel-provider-sftp@storage-kernel/provider-sftp directory Remote directories over SSH. Also ssh-sftp. Diretórios remotos sobre SSH. Também ssh-sftp.

Credentials and lifecycle

Credenciais e ciclo de vida

Credentials are explicit when you pass them and fall back to the cloud SDK's own chain when you do not — the AWS provider chain, or Application Default Credentials on GCP.

As credenciais são explícitas quando você as passa, e caem para a própria cadeia do SDK da nuvem quando você não passa — a cadeia de credenciais da AWS, ou as Application Default Credentials no GCP.

Injecting a client keeps that SDK client under your lifecycle. close() releases only what the kernel created: the S3 client and the SFTP connection it opened, and the Google client it built. On GCP it is a no-op, because the SDK exposes no matching operation, and that is stated rather than papered over.

Injetar um client mantém aquele client de SDK sob o seu ciclo de vida. O close() libera apenas o que o kernel criou: o client S3 e a conexão SFTP que ele abriu, e o client do Google que ele construiu. No GCP ele é um no-op, porque o SDK não expõe operação equivalente, e isso é dito em vez de disfarçado.

To configure a client the kernel does build, the S3 provider takes client_optionsclientOptions. It is merged over the provider's own defaults field by field, so setting one bound keeps the others rather than dropping back to the SDK's unbounded ones — and it is ignored entirely when you inject a client, because then the configuration was yours to make.

Para configurar um client que o kernel realmente constrói, o armazenamento S3 recebe client_optionsclientOptions. Ele é mesclado campo a campo sobre os defaults do próprio armazenamento, então definir um limite preserva os outros em vez de cair para os do SDK, que não têm limite — e é totalmente ignorado quando você injeta um client, porque aí a configuração já era sua.

gcp-storage
from storage_kernel.factory import (
    StorageClientOptions,
    StorageLocation,
)
from storage_kernel.provider_gcp_storage import (
    GcpStorageProviderOptions,
    create_gcp_storage_client,
)

# the client you built stays yours; close() will not touch it
storage = create_gcp_storage_client(
    StorageClientOptions(
        location=StorageLocation(bucket="app-files"),
    ),
    GcpStorageProviderOptions(
        project_id="my-project",
        client=my_gcs_client,
    ),
)
import {
    GcpStorageClient,
} from '@storage-kernel/provider-gcp-storage';

// the client you built stays yours; close() will not touch it
const storage = new GcpStorageClient(
    { location: { bucket: 'app-files' } },
    { projectId: 'my-project', client: myGcsClient },
);

SFTP host identity

Identidade do host SFTP

The SFTP provider will not open a connection without being told how to verify the server. Exactly one policy is required, and there is no default:

O armazenamento SFTP não abre conexão sem que se diga como verificar o servidor. Exatamente uma política é obrigatória, e não existe default:

  • known_hosts_pathknownHostsPath — an OpenSSH known_hosts file.
  • host_key_sha256hostKeySha256 — a fingerprint in SHA256:<base64> form.
  • host_verifierhostVerifier — your own predicate over the host key.
  • dangerously_disable_host_key_verificationdangerouslyDisableHostKeyVerification — the bypass. It accepts any host key, which means accepting a machine-in-the-middle, so it belongs to a disposable local fixture and nowhere else. It is named the way it is so it cannot be switched on without reading what it does.
  • known_hosts_pathknownHostsPath — um arquivo known_hosts do OpenSSH.
  • host_key_sha256hostKeySha256 — uma fingerprint no formato SHA256:<base64>.
  • host_verifierhostVerifier — seu próprio predicado sobre a chave do host.
  • dangerously_disable_host_key_verificationdangerouslyDisableHostKeyVerification — o bypass. Ele aceita qualquer chave de host, o que significa aceitar um ataque de intermediário, então serve para uma fixture local descartável e nada mais. O nome é esse justamente para não ser ligado sem se ler o que ele faz.

Two policies at once, or a malformed fingerprint, is a StorageKernelConfigurationError rather than a quiet fallback to something weaker.

Duas políticas ao mesmo tempo, ou uma fingerprint malformada, é um StorageKernelConfigurationError em vez de uma queda silenciosa para algo mais fraco.

Each connection phase is bounded, and the two implementations bound them differently — the SDKs underneath are not the same. Python names four: connect_timeout at 10s, banner_timeout at 15s, auth_timeout at 30s and channel_timeout at 60s. The first one matters most: paramiko leaves the connect socket blocking when no timeout is given, so without it a TCP handshake is bounded only by the operating system. TypeScript takes readyTimeout, in milliseconds, over the whole handshake.

Cada fase da conexão tem um limite, e as duas implementações limitam de formas diferentes — os SDKs por baixo não são os mesmos. O Python nomeia quatro: connect_timeout em 10s, banner_timeout em 15s, auth_timeout em 30s e channel_timeout em 60s. O primeiro é o que mais importa: o paramiko deixa o socket de conexão bloqueante quando nenhum timeout é dado, então sem ele o handshake TCP fica limitado apenas pelo sistema operacional. O TypeScript recebe readyTimeout, em milissegundos, sobre o handshake inteiro.

A failure names both sides: path is always the path you passed, and the message adds the remote one — Storage path not found in sftp: missing.txt (provider path: /upload/missing.txt).

Uma falha nomeia os dois lados: path é sempre o caminho que você passou, e a mensagem acrescenta o remoto — Storage path not found in sftp: missing.txt (provider path: /upload/missing.txt).

The Python provider also disables SSH agent and default-key discovery, so a key is used because you named it, not because it happened to be in the environment.

O armazenamento Python também desliga o SSH agent e a descoberta de chaves default, então uma chave é usada porque você a nomeou, não porque ela por acaso estava no ambiente.

sftp
SftpStorageProviderOptions(
    host="sftp.example.com",
    username="app",
    private_key_path="/run/secrets/id_ed25519",
    # exactly one of the four policies
    known_hosts_path="/home/app/.ssh/known_hosts",
    # host_key_sha256="SHA256:…",
    # host_verifier=my_verifier,
    # dangerously_disable_host_key_verification=True,
)
const options = {
    host: 'sftp.example.com',
    username: 'app',
    privateKeyPath: '/run/secrets/id_ed25519',
    // exactly one of the four policies
    knownHostsPath: '/home/app/.ssh/known_hosts',
    // hostKeySha256: 'SHA256:…',
    // hostVerifier: myVerifier,
    // dangerouslyDisableHostKeyVerification: true,
};
08

Errors are part of the contract

Erros fazem parte do contrato

Every error carries name and code, plus provider, path, provider_codeproviderCode and cause when the failure exposes them.

Todo erro carrega name e code, mais provider, path, provider_codeproviderCode e cause quando a falha os expõe.

Three fields identify a failure. name is the class. code is the kernel's own, always present and frozen, the same string in both languages — compare it at a process boundary, where a class cannot travel. provider_codeproviderCode is the vendor's — NoSuchKey on S3, 404 on GCP Storage, SFTP status 2, ENOENT on a local disk, ECONNREFUSED when the service never answered — and is absent when the SDK gave none.

Três campos identificam uma falha. name é a classe. code é do próprio kernel, sempre presente e congelado, a mesma string nas duas linguagens — compare-o numa fronteira entre processos, onde uma classe não viaja. provider_codeproviderCode é o do fornecedor — NoSuchKey no S3, 404 no GCP Storage, status SFTP 2, ENOENT num disco local, ECONNREFUSED quando o serviço nem respondeu — e fica ausente quando o SDK não deu nenhum.

Only not found and permission denied are normalized, because only those two are what a caller branches on without knowing which service is behind the contract. Their message names the provider and the path: Storage path not found in aws-s3: 2026/summary.csv.

Só não encontrado e permissão negada são normalizados, porque só esses dois são aquilo em que quem chama ramifica sem saber qual serviço está atrás do contrato. A mensagem deles nomeia o armazenamento e o caminho: Storage path not found in aws-s3: 2026/summary.csv.

Everything else becomes StorageProviderError and keeps the message the provider produced. Wrapping adds context, it never translates: provider, path and provider_codeproviderCode are attached and the original stays in cause.

Todo o resto vira StorageProviderError e preserva a mensagem que o armazenamento produziu. O wrap acrescenta contexto, nunca traduz: provider, path e provider_codeproviderCode são anexados e o original fica em cause.

An error that already is a StorageKernelError is never re-wrapped and never reclassified. It receives only the context fields it was missing, so a failure raised deep inside a client still reports where it came from.

Um erro que já é StorageKernelError nunca é reembrulhado nem reclassificado. Ele recebe apenas os campos de contexto que faltavam, então uma falha levantada lá no fundo de um client continua dizendo de onde veio.

handlingtratamento
from storage_kernel.factory import (
    StoragePathNotFoundError,
    StoragePermissionError,
    StorageProviderError,
)

try:
    data = await storage.read_binary("2026/summary.csv")
except StoragePathNotFoundError as error:
    error.code           # "strg_path_not_found"
    error.provider_code  # "NoSuchKey" on aws-s3
    raise
except StoragePermissionError:
    raise
except StorageProviderError as error:
    # the SDK's message, plus provider/path/provider_code
    logger.error("%s (%s)", error, error.provider_code)
    raise
import {
    StoragePathNotFoundError,
    StorageProviderError,
} from '@storage-kernel/factory';

try {
    const data = await storage.readBinary('2026/summary.csv');
} catch (error) {
    if (error instanceof StoragePathNotFoundError) {
        error.code; // 'strg_path_not_found'
        error.providerCode; // 'NoSuchKey' on aws-s3
    } else if (error instanceof StorageProviderError) {
        // the SDK's message, plus provider/path/providerCode
        logger.error(
            `${error.message} (${error.providerCode})`,
        );
    }
    throw error;
}
ClassClasse code Raised whenLevantado quando
StoragePathNotFoundErrorstrg_path_not_foundThe requested path does not exist. Normalized across providers.O caminho pedido não existe. Normalizado entre armazenamentos.
StoragePermissionErrorstrg_permission_deniedThe provider denied authentication or authorization. Normalized across providers.O armazenamento negou autenticação ou autorização. Normalizado entre armazenamentos.
StoragePathAlreadyExistsErrorstrg_path_already_existsOverwrite was off and the destination already exists.Overwrite estava desligado e o destino já existe.
StorageInvalidPathErrorstrg_invalid_pathAn empty, traversing or otherwise unsafe path.Um caminho vazio, com travessia ou de outra forma inseguro.
StorageCapabilityNotSupportedErrorstrg_capability_not_supportedThe provider cannot implement an optional capability, such as a signed URL on a local disk.O armazenamento não consegue implementar uma capacidade opcional, como uma URL assinada num disco local.
StorageKernelConfigurationErrorstrg_invalid_configurationRequired, invalid or mutually exclusive configuration — a missing bucket, two SFTP host policies at once.Configuração obrigatória, inválida ou mutuamente exclusiva — um bucket ausente, duas políticas de host SFTP ao mesmo tempo.
StorageProviderErrorstrg_unexpectedAny other provider or stream failure, with the provider's own message kept.Qualquer outra falha do armazenamento ou de stream, preservando a mensagem do próprio armazenamento.
09

Observability, and your own provider

Observabilidade, e seu próprio armazenamento

Silent unless you hand it a logger, and open to a provider the kernel has never heard of.

Silencioso a menos que você entregue um logger, e aberto a um armazenamento que o kernel nunca viu.

One line per operation

Uma linha por operação

Pass a logger and the client writes one line per operation. Without one it says nothing: the logger is the only sink, and there is no console fallback. The levels are fixed by the contract, not by each provider: starts and completions on debug, a missing path or an aborted upload on warningwarn, permission and provider failures on error.

Passe um logger e o client escreve uma linha por operação. Sem ele, não diz nada: o logger é o único destino, e não há console de reserva. Os níveis são fixados pelo contrato, não por cada armazenamento: início e conclusão em debug, caminho ausente ou upload abortado em warningwarn, falhas de permissão e do armazenamento em error.

Each line is one sentence naming the operation, the path, the provider and its bucket, directory or prefix — Reading 2026/summary.csv from aws-s3 bucket app-files under reports. Content, credentials and object metadata never appear in it.

Cada linha é uma frase que nomeia a operação, o caminho, o armazenamento e seu bucket, diretório ou prefix — Reading 2026/summary.csv from aws-s3 bucket app-files under reports. Conteúdo, credenciais e metadados de objeto nunca aparecem nela.

Completion is reported when it actually happened: a read stream after it has been fully consumed, an upload after close() succeeded — not when the call returned. A logger may implement any subset of the four levels; a line at a level it lacks is dropped.

A conclusão é reportada quando realmente aconteceu: um stream de leitura depois de consumido por inteiro, um upload depois de close() ter dado certo — não quando a chamada retornou. Um logger pode implementar qualquer subconjunto dos quatro níveis; uma linha num nível que ele não tem é descartada.

Bring your own provider

Traga seu próprio armazenamento

The factory takes a provider_classproviderClass — a class, never an instance — and then any provider name is allowed. If the class declares its own canonical name, it has to match the one you asked for.

A factory recebe um provider_classproviderClass — uma classe, nunca uma instância — e então qualquer nome de armazenamento é permitido. Se a classe declara o próprio nome canônico, ele precisa bater com o que você pediu.

The client is wrapped in the same observability boundary either way, so a provider you wrote logs the same lines as the four that ship here without inheriting anything.

O client é envolvido na mesma fronteira de observabilidade nos dois casos, então um armazenamento que você escreveu emite as mesmas linhas que os quatro que vêm aqui, sem herdar nada.

The testing package exports the portable contract suite these four are held to, so yours can be held to it too. It runs on your own vitest, which it takes as a peer dependency.

O pacote de testing exporta a suíte de contrato portável a que estes quatro são submetidos, então o seu pode ser submetido a ela também. Ela roda no seu próprio vitest, que ele recebe como peer dependency.

logginglogging
storage = await create_storage_client(
    CreateStorageClientConfig(
        provider_name="aws-s3",
        location=StorageLocation(bucket="app-files"),
        logger=logging.getLogger("my-app"),
    )
)
const storage = await createStorageClient({
    providerName: 'aws-s3',
    location: { bucket: 'app-files' },
    logger: console,
});
custom providerarmazenamento próprio
await create_storage_client(
    CreateStorageClientConfig(
        provider_name="azure-blob",
        provider_class=MyAzureBlobClient,
    )
)
await createStorageClient({
    providerName: 'azure-blob',
    providerClass: MyAzureBlobClient,
});
the contract suitea suíte de contrato
from storage_kernel.testing import assert_client_contract

await assert_client_contract(storage)
import {
    defineClientContractTests,
} from '@storage-kernel/testing';

defineClientContractTests('azure-blob', {
    createClient: () => new MyAzureBlobClient(),
});
10

One contract, and what it will not fake

Um contrato, e o que ele não vai fingir

Every provider implements the same seventeen asynchronous methods. Where a service genuinely cannot do something, the call says so instead of improvising.

Todo armazenamento implementa os mesmos dezessete métodos assíncronos. Onde um serviço realmente não consegue fazer algo, a chamada diz isso em vez de improvisar.

  • Nothing is emulated. create_signed_urlcreateSignedUrl on the local filesystem raises StorageCapabilityNotSupportedError rather than inventing a URL that would not work.
  • Streams are real streams. Reads and progressive uploads use the provider's own stream where it has one, so a multi-gigabyte file never has to fit in memory.
  • Provider options never leak. region belongs to AWS, project_idprojectId to GCP, host and key material to SFTP. The shared contract knows none of them.
  • Injected clients stay yours. Hand a provider an SDK client you built and close() leaves it alone — the kernel releases only what it created.
  • An empty path means the configured root, and only on S3, GCP Storage and SFTP. The local filesystem provider rejects it for every operation.
  • A per-call location replaces the client's, whole. Locations are never merged field by field, so a call that names another bucket cannot silently keep the old prefix.
  • Nada é emulado. create_signed_urlcreateSignedUrl no filesystem local levanta StorageCapabilityNotSupportedError em vez de inventar uma URL que não funcionaria.
  • Streams são streams de verdade. Leituras e uploads progressivos usam o stream do próprio armazenamento quando ele tem um, então um arquivo de vários gigabytes nunca precisa caber na memória.
  • Opções de armazenamento não vazam. region é da AWS, project_idprojectId é do GCP, host e material de chave são do SFTP. O contrato compartilhado não conhece nenhum deles.
  • Clients injetados continuam seus. Entregue a um armazenamento um client de SDK que você construiu e o close() não encosta nele — o kernel libera apenas o que ele mesmo criou.
  • Um caminho vazio significa a raiz configurada, e só no S3, no GCP Storage e no SFTP. O armazenamento de filesystem local o rejeita em toda operação.
  • Uma location por chamada substitui a do client, inteira. Locations nunca são mescladas campo a campo, então uma chamada que nomeia outro bucket não consegue manter o prefix antigo em silêncio.

Overwrite is on by default and can be turned off per call. The check always runs against the destination, and a blocked write is StoragePathAlreadyExistsError. On the local filesystem and over SFTP the file is opened exclusively, so one that appears between the check and the write is refused rather than replaced.

Sobrescrever é o padrão e pode ser desligado por chamada. A verificação sempre roda contra o destino, e uma escrita bloqueada é StoragePathAlreadyExistsError. No filesystem local e no SFTP o arquivo é aberto de forma exclusiva, então um que apareça entre a verificação e a escrita é recusado em vez de substituído.

11

Changelog

Changelog

Early, and honest about it. Each implementation releases on its own, so this follows the language you picked above. Within one release every distribution shares a version.

No começo, e honesto sobre isso. Cada implementação lança por conta própria, então isto acompanha a linguagem escolhida acima. Dentro de um release, toda distribuição compartilha a versão.

0.1.0a4 2026-09-25

Changed · breaking

Alterado · breaking

  • A path is a plain string. Every operation took StoragePath("a/b.txt") and now takes "a/b.txt". StoragePath is gone from the contract.
  • A writable stream finishes with close(), where it said end(). abort() still discards.
  • code changed owner. It held the vendor's identifier, which moved to provider_code. code is now the kernel's own, always present and frozen: strg_path_not_found, strg_path_already_exists, strg_permission_denied, strg_invalid_path, strg_capability_not_supported, strg_invalid_configuration, strg_unexpected. StorageErrorCode is a StrEnum of those seven.
  • provider_code is what the vendor's protocol called the failure: the errno name on a local disk, ENOENT, where it was a number; the SFTP status the server sent; the operating-system error, such as ECONNREFUSED, when the service never answered; a plain int on GCP Storage. TypeScript reports the same values.
  • StorageConfigurationError is now StorageKernelConfigurationError, with no alias. Its code is strg_invalid_configuration.
  • A not-found or permission failure names the provider and the path in its message, in the same words as TypeScript. Any other failure keeps the message the SDK produced, where it was replaced by one of the provider's own.
  • A log line is one string, and the logger is the only sink. The context argument and the debug option are gone, and each line says what happened and where. logging.getLogger("my-app") is a drop-in.
  • read_stream returns a stream that carries an idempotent close(), so a reader that stops early can release the socket, descriptor or channel it holds. A custom provider subclasses IterableStorageReadableStream.
  • send_stream takes text, bytes or a stream, with SendStreamOptions.
  • The S3 provider names its SDK configuration client_options, as TypeScript does. It was client_config.
  • The create_*_storage_client functions lose their third parameter: inject the SDK client through client in the provider options.
  • Errors are built from one StorageKernelErrorOptions, and write_text and write_text_line take a WriteTextOptions instead of an encoding keyword — the same call shape as TypeScript.
  • Helpers take the names TypeScript uses — join_storage_path, resolve_storage_location — and drop the storage prefix where the type they return already says it: normalize_provider_error, assert_client_contract and nine more. STORAGE_PROVIDER_TARGETS is STORAGE_PROVIDER_TARGET_BY_NAME. Breaking for a custom provider importing them.
  • Signed URLs use Signature Version 4 on both clouds, so they live seven days at most. A boto3 client you inject has to be built with signature_version="s3v4".
  • overwrite=False on SFTP is enforced by the server: the file is opened with an exclusive create, so one created between the check and the write is refused instead of replaced.
  • An SFTP failure carries your path in path, and names the remote one in its message.
  • Um caminho é uma string simples. Toda operação recebia StoragePath("a/b.txt") e agora recebe "a/b.txt". StoragePath saiu do contrato.
  • Um stream de escrita termina com close(), onde dizia end(). abort() continua descartando.
  • code mudou de dono. Ele guardava o identificador do fornecedor, que foi para provider_code. code agora é do próprio kernel, sempre presente e congelado: strg_path_not_found, strg_path_already_exists, strg_permission_denied, strg_invalid_path, strg_capability_not_supported, strg_invalid_configuration, strg_unexpected. StorageErrorCode é um StrEnum desses sete.
  • provider_code é o que o protocolo do fornecedor chamou a falha: o nome do errno num disco local, ENOENT, onde era um número; o status SFTP que o servidor mandou; o erro do sistema operacional, como ECONNREFUSED, quando o serviço nem respondeu; um int simples no GCP Storage. O TypeScript reporta os mesmos valores.
  • StorageConfigurationError agora é StorageKernelConfigurationError, sem alias. O code é strg_invalid_configuration.
  • Uma falha de não encontrado ou de permissão nomeia o armazenamento e o caminho na mensagem, com as mesmas palavras do TypeScript. Qualquer outra falha preserva a mensagem que o SDK produziu, onde antes era trocada por uma do próprio armazenamento.
  • Uma linha de log é uma string, e o logger é o único destino. O argumento de contexto e a opção debug saíram, e cada linha diz o que aconteceu e onde. logging.getLogger("my-app") serve direto.
  • read_stream devolve um stream que carrega um close() idempotente, então quem para de ler antes pode liberar o socket, descritor ou canal que ele segura. Um armazenamento próprio herda de IterableStorageReadableStream.
  • send_stream recebe texto, bytes ou um stream, com SendStreamOptions.
  • O armazenamento S3 chama sua configuração de SDK de client_options, como o TypeScript. Era client_config.
  • As funções create_*_storage_client perdem o terceiro parâmetro: injete o client do SDK por client nas opções do armazenamento.
  • Erros são construídos a partir de um único StorageKernelErrorOptions, e write_text e write_text_line recebem um WriteTextOptions em vez do keyword encoding — o mesmo formato de chamada do TypeScript.
  • Helpers adotam os nomes do TypeScript — join_storage_path, resolve_storage_location — e perdem o prefixo storage onde o tipo que devolvem já o diz: normalize_provider_error, assert_client_contract e mais nove. STORAGE_PROVIDER_TARGETS é STORAGE_PROVIDER_TARGET_BY_NAME. Breaking para um armazenamento próprio que os importa.
  • URLs assinadas usam a Signature Version 4 nas duas nuvens, então vivem no máximo sete dias. Um client boto3 que você injeta precisa ser construído com signature_version="s3v4".
  • overwrite=False no SFTP é garantido pelo servidor: o arquivo é aberto com criação exclusiva, então um criado entre a verificação e a escrita é recusado em vez de substituído.
  • Uma falha de SFTP carrega o seu caminho em path, e nomeia o remoto na mensagem.

Changed

Alterado

  • Install the factory and one provider. storage_kernel.factory re-exports the whole contract — errors, types, the provider registry — so everything is imported from it. Imports from storage_kernel.contracts keep working.
  • Every error carries its class name in name.
  • The content type of a file is read from an explicit table of 39 extensions, the same one TypeScript reads, instead of the host's MIME database, so the same file reports the same type on every machine. .wav reports audio/wav.
  • Dependency floors say what the code needs: boto3>=1.26.0, google-cloud-storage>=3.0.0, paramiko>=5.0.0, and botocore and google-auth, which the providers import, are now declared.
  • Instale a factory e um armazenamento. storage_kernel.factory reexporta o contrato inteiro — erros, tipos, o registro de armazenamentos —, então tudo é importado dela. Imports de storage_kernel.contracts continuam funcionando.
  • Todo erro carrega o nome da sua classe em name.
  • O content type de um arquivo é lido de uma tabela explícita de 39 extensões, a mesma que o TypeScript lê, em vez do banco MIME da máquina, então o mesmo arquivo reporta o mesmo tipo em toda máquina. .wav reporta audio/wav.
  • Os pisos de dependência dizem o que o código precisa: boto3>=1.26.0, google-cloud-storage>=3.0.0, paramiko>=5.0.0, e botocore e google-auth, que os armazenamentos importam, passam a ser declarados.

Fixed

Corrigido

  • A native copy given only a location lands beside its source. It used to land at the client's location.
  • close() on a GCP Storage client created from options closes the Google client it built.
  • A GCP Storage client no longer fetches bucket metadata behind your back, which needed a permission an object-only account does not have.
  • An SFTP stat the server refuses raises StoragePermissionError instead of answering that the path does not exist, and a refused SFTP delete no longer reports success.
  • An SFTP overwrite conflict raised while writing is StoragePathAlreadyExistsError, where it was a generic StorageProviderError.
  • Building an S3 client no longer rewrites DEFAULT_CLIENT_CONFIG.
  • Uma cópia nativa que só recebe uma location cai ao lado da origem. Antes ela caía na location do client.
  • close() num client GCP Storage criado a partir de opções fecha o client do Google que ele construiu.
  • Um client GCP Storage não busca mais metadados do bucket pelas suas costas, o que exigia uma permissão que uma conta restrita a objetos não tem.
  • Um stat SFTP que o servidor recusa levanta StoragePermissionError em vez de responder que o caminho não existe, e uma remoção SFTP recusada não reporta mais sucesso.
  • Um conflito de overwrite no SFTP levantado durante a escrita é StoragePathAlreadyExistsError, onde era um StorageProviderError genérico.
  • Construir um client S3 não reescreve mais o DEFAULT_CLIENT_CONFIG.
0.1.0a1 2026-08-27 first alpha primeiro alpha

The contract, the shared core, the factory, the four providers and the portable contract suite.

O contrato, o core compartilhado, a factory, os quatro armazenamentos e a suíte de contrato portável.

0.1.0-alpha.4 first alpha primeiro alpha

The contract, the shared core, the factory, the four providers and the portable contract suite.

O contrato, o core compartilhado, a factory, os quatro armazenamentos e a suíte de contrato portável.