Python & TypeScript · files and objects
Python & TypeScript · arquivos e objetos
One contract.
Four backends.
Um contrato.
Quatro backends.
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.
from storage_kernel.contracts import StorageLocation, StoragePath from storage_kernel.factory import ( CreateStorageClientConfig, create_storage_client, ) storage = await create_storage_client( CreateStorageClientConfig( provider_name="aws-s3", location=StorageLocation( bucket="app-files", prefix="reports" ), ) ) await storage.send_content( StoragePath("2026/summary.csv"), "id,total\n1,42\n" ) content = await storage.read_content( StoragePath("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( { path: '2026/summary.csv' }, 'id,total\n1,42\n', ); const content = await storage.readContent({ path: '2026/summary.csv', }); // app-files/reports/2026/summary.csv await storage.close();
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 provider 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_urlcreateSignedUrlon the local filesystem raisesStorageCapabilityNotSupportedErrorrather 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.
regionbelongs to AWS,project_idprojectIdto 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_urlcreateSignedUrlno filesystem local levantaStorageCapabilityNotSupportedErrorem 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 provider quando ele tem um, então um arquivo de vários gigabytes nunca precisa caber na memória.
- Opções de provider 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 provider um client de SDK que você construiu e o
close()não encosta nele — o kernel libera apenas o que ele mesmo criou. - Um path vazio significa a raiz configurada, e só no S3, no GCP Storage e no SFTP. O provider 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.
Sobrescrever é o padrão e pode ser desligado por chamada. A verificação sempre roda contra o destino, e uma escrita bloqueada é StoragePathAlreadyExistsError.
Install and first write
Instalação e primeira escrita
Nothing is bundled. Install the contract, the shared core, the factory and the one provider you need.
Nada vem embutido. Instale o contrato, o core compartilhado, a factory e o único provider de que você precisa.
Step 1 · install the base
Passo 1 · instale a base
The contract, the shared core and the factory. 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 a paramiko.
O contrato, o core compartilhado e a factory. Nenhum provider 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 um paramiko.
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.
$ uv add storage-kernel-contracts storage-kernel-core \
storage-kernel-factory
$ npm install @storage-kernel/contracts @storage-kernel/core \
@storage-kernel/factory
Step 2 · add a provider and write
Passo 2 · adicione um provider e escreva
Pick where the file lives. The calls are identical either way — only the provider name and its typed options change, which is the whole point of the contract.
Escolha onde o arquivo mora. As chamadas são idênticas nos dois casos — mudam apenas o nome do provider e suas opções tipadas, que é justamente o propósito do contrato.
local-filesystem
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 path 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 provider.
$ uv add storage-kernel-provider-local-filesystem
$ npm install @storage-kernel/provider-local-filesystem
from storage_kernel.contracts import StorageLocation, StoragePath from storage_kernel.factory import ( CreateStorageClientConfig, create_storage_client, ) storage = await create_storage_client( CreateStorageClientConfig( provider_name="local-filesystem", location=StorageLocation(directory="./var/storage"), ) ) await storage.send_content( StoragePath("notes/today.txt"), "hello" ) text = await storage.read_content( StoragePath("notes/today.txt") ) # ./var/storage/notes/today.txt
import { createStorageClient } from '@storage-kernel/factory'; const storage = await createStorageClient({ providerName: 'local-filesystem', location: { directory: './var/storage' }, }); await storage.sendContent( { path: 'notes/today.txt' }, 'hello', ); const text = await storage.readContent({ path: 'notes/today.txt', }); // ./var/storage/notes/today.txt
aws-s3
For objects in S3 and S3-compatible services. location.bucket is mandatory; location.prefix is optional and may be empty.
Para objetos no S3 e em serviços compatíveis com 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 provider chain applies.
Passe credentials para ser explícito, ou omita e a própria cadeia de credenciais do SDK da AWS se aplica.
Only the provider name and its options differ from the block above. send_binarysendBinary is the same call it would be anywhere else.
Só o nome do provider e suas opções diferem do bloco acima. send_binarysendBinary é a mesma chamada que seria em qualquer outro lugar.
$ uv add storage-kernel-provider-aws-s3
$ npm install @storage-kernel/provider-aws-s3
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", ), ) ) await storage.send_binary( StoragePath("2026/summary.csv"), payload ) # app-files/reports/2026/summary.csv
const storage = await createStorageClient({ providerName: 'aws-s3', location: { bucket: 'app-files', prefix: 'reports' }, options: { region: 'us-east-1' }, }); await storage.sendBinary( { path: '2026/summary.csv' }, payload, ); // app-files/reports/2026/summary.csv
What the registries serve today
O que os registries entregam hoje
The library is mid-release, and the install lines above are ahead of it. Checked against the registries, not the repositories:
A biblioteca está no meio de um release, e as linhas de instalação acima estão à frente dele. Verificado contra os registries, não contra os repositórios:
- PyPI serves
storage-kernel-contracts,storage-kernel-coreandstorage-kernel-factoryat0.1.0a1. The four provider distributions andstorage-kernel-testingare not there yet, so a provideruv addwill not resolve until they are. - npm has nothing under
@storage-kernel. The TypeScript side is tagged at0.1.0-alpha.1and the samples on this page match that tag; thenpm installlines do not work yet.
- O PyPI entrega
storage-kernel-contracts,storage-kernel-coreestorage-kernel-factoryna0.1.0a1. As quatro distribuições de provider e ostorage-kernel-testingainda não estão lá, então umuv addde provider não resolve enquanto não estiverem. - O npm não tem nada sob
@storage-kernel. O lado TypeScript está tagueado na0.1.0-alpha.1e os exemplos desta página conferem com essa tag; as linhasnpm installainda não funcionam.
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 path que cada chamada pede é resolvido abaixo dela, e o provider mapeia o resultado para o que o serviço dele chama de lugar.
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.
| Field | Campo | Used by | Usado por | Rule | Regra |
|---|---|---|---|---|---|
location.directory |
local-filesystemsftp |
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 provider local. No SFTP ela seleciona a raiz remota, caindo para root_directoryrootDirectory e depois para .. |
||
location.bucket |
aws-s3gcp-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 providers de nuvem, e o único lugar onde um nome de bucket é escrito. | ||
location.prefix |
all four | os 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 four | os quatro | What the call asks for. Always / separated, whatever the platform underneath. |
O que a chamada pede. 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 provider 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 provider local acrescenta mais uma checagem depois de resolver: o resultado absoluto precisa continuar abaixo do diretório configurado, então um symlink ou um path 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.
from storage_kernel.contracts import StorageOperationOptions # archive/reports/2026/summary.csv — another bucket, # and the prefix comes from this location, not the client's await storage.read_binary( StoragePath("2026/summary.csv"), StorageOperationOptions( location=StorageLocation( bucket="archive", prefix="reports" ) ), ) # rejected: StorageInvalidPathError await storage.read_binary(StoragePath("../etc/passwd"))
// archive/reports/2026/summary.csv — another bucket, // and the prefix comes from this location, not the client's await storage.readBinary( { path: '2026/summary.csv' }, { location: { bucket: 'archive', prefix: 'reports' } }, ); // rejected: StorageInvalidPathError await storage.readBinary({ path: '../etc/passwd' });
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 provider.
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.
from storage_kernel.contracts import SendContentOptions await storage.send_content( StoragePath("docs/readme.md"), "# Hello", SendContentOptions( content_type="text/markdown", overwrite=False, ), ) text = await storage.read_content( StoragePath("docs/readme.md") ) data = await storage.read_binary( StoragePath("images/avatar.png") )
await storage.sendContent( { path: 'docs/readme.md' }, '# Hello', { contentType: 'text/markdown', overwrite: false }, ); const text = await storage.readContent({ path: 'docs/readme.md', }); const data = await storage.readBinary({ path: 'images/avatar.png', });
Streams
Streams
read_streamreadStream hands back an async byte iterator. create_upload_streamcreateUploadStream hands back a writable handle with five methods: write, write_textwriteText, write_text_linewriteTextLine, then end() to finish or abort() to discard.
read_streamreadStream devolve um iterador assíncrono de bytes. create_upload_streamcreateUploadStream devolve um handle de escrita com cinco métodos: write, write_textwriteText, write_text_linewriteTextLine, e então end() para concluir ou abort() para descartar.
end() waits for the provider to actually finish. After either end() or abort(), another write fails with a normalized error rather than quietly doing nothing.
end() espera o provider realmente concluir. Depois de end() ou abort(), outra escrita falha com um erro normalizado em vez de não fazer nada em silêncio.
On S3 this is the SDK's managed multipart upload. MultipartUploadOptions tunes part_sizepartSize, queue_sizequeueSize and leave_parts_on_errorleavePartsOnError; disabling it buffers to a single PutObject. On GCP Storage, enabled maps to a resumable upload.
No S3 isso é o upload multipart gerenciado do SDK. MultipartUploadOptions ajusta part_sizepartSize, queue_sizequeueSize e leave_parts_on_errorleavePartsOnError; desligá-lo acumula tudo num único PutObject. No GCP Storage, enabled vira um upload retomável.
# read without holding the file in memory async for chunk in await storage.read_stream( StoragePath("archive/2026.zip") ): process(chunk) # write progressively; end() commits, abort() discards upload = await storage.create_upload_stream( StoragePath("logs/app.ndjson") ) try: for event in events: await upload.write_text_line(json.dumps(event)) await upload.end() except BaseException: await upload.abort() raise
// read without holding the file in memory for await (const chunk of await storage.readStream({ path: 'archive/2026.zip', })) { process(chunk); } // write progressively; end() commits, abort() discards const upload = await storage.createUploadStream({ path: 'logs/app.ndjson', }); try { for (const event of events) { await upload.writeTextLine(JSON.stringify(event)); } await upload.end(); } catch (error) { await upload.abort(error); throw error; }
| Method | Método | Contract | Contrato |
|---|---|---|---|
| Read · 3Leitura · 3 | |||
read_binaryreadBinary | Loads the complete file into memory. | Carrega o arquivo inteiro na memória. | |
read_contentreadContent | Loads and decodes complete text; UTF-8 by default. | Carrega e decodifica o texto inteiro; UTF-8 por padrão. | |
read_streamreadStream | Returns the provider stream as an async byte iterator. | Devolve o stream do provider como iterador assíncrono de bytes. | |
| Write · 4Escrita · 4 | |||
send_contentsendContent | Encodes and writes complete text. | Codifica e escreve o texto inteiro. | |
send_binarysendBinary | Writes complete bytes. | Escreve os bytes inteiros. | |
send_streamsendStream | Consumes an async byte stream without full buffering where the backend allows it. | Consome um stream assíncrono de bytes sem acumular tudo, onde o backend permite. | |
create_upload_streamcreateUploadStream | Returns a progressive upload handle. | Devolve um handle de upload progressivo. | |
| Delete · 2Remoção · 2 | |||
delete_filedeleteFile | Idempotently deletes one file. Deleting what is already gone succeeds. | Remove um arquivo de forma idempotente. Remover o que já sumiu dá certo. | |
delete_directorydeleteDirectory | Deletes 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_directoryreadDirectory | Returns sorted, normalized entries. | Devolve entradas normalizadas e ordenadas. | |
get_directory_content_lengthgetDirectoryContentLength | Sums 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_existscheckPathExists | Checks a file, or the provider's notion of a directory. | Checa um arquivo, ou a noção de diretório do provider. | |
get_file_infogetFileInfo | Returns 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_filenativeCopyFile | Copies within one provider using its own mechanism. | Copia dentro de um provider usando o mecanismo dele. | |
create_signed_urlcreateSignedUrl | Creates 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_urlgetPublicUrl | Resolves a provider or configured public URL. | Resolve uma URL pública do provider ou configurada. | |
| Lifecycle · 1Ciclo de vida · 1 | |||
close | Releases only what the kernel created. An injected client is left alone. | Libera apenas o que o kernel criou. Um client injetado fica intocado. | |
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 backends.
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 backends.
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 path, 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 path que não está lá. Devolve o path 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 provider de nuvem recusa enquanto ainda houver objetos aninhados, em vez de apagar parte da árvore.
from storage_kernel.contracts import ReadDirectoryOptions # everything below reports/, files and directories entries = await storage.read_directory( StoragePath("reports") ) # only the files directly inside reports/ files = await storage.read_directory( StoragePath("reports"), ReadDirectoryOptions( recursive=False, include_directories=False ), ) total = await storage.get_directory_content_length( StoragePath("reports") ) info = await storage.get_file_info( StoragePath("reports/2026/summary.csv") ) # info.exists, info.content_length, info.updated_at
// everything below reports/, files and directories const entries = await storage.readDirectory({ path: 'reports', }); // only the files directly inside reports/ const files = await storage.readDirectory( { path: 'reports' }, { recursive: false, includeDirectories: false }, ); const total = await storage.getDirectoryContentLength({ path: 'reports', }); const info = await storage.getFileInfo({ path: 'reports/2026/summary.csv', }); // info.exists, info.contentLength, info.updatedAt
recursive |
include_directoriesincludeDirectories |
Returns | Devolve |
|---|---|---|---|
| ✓ | ✓ | Everything below the path, files and directories. The default. | Tudo abaixo do path, arquivos e diretórios. O padrão. |
| ✓ | — | Every file below the path, at any depth. | Todo arquivo abaixo do path, 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. |
What each backend can actually do
O que cada backend realmente faz
The differences sit in the contract rather than behind it. A capability a service does not have raises StorageCapabilityNotSupportedError, at the call, with the provider named.
As diferenças ficam no contrato, não atrás dele. Uma capacidade que um serviço não tem levanta StorageCapabilityNotSupportedError, na chamada, com o provider nomeado.
| Capability | Capacidade | local | aws-s3 | gcp-storage | sftp | |
|---|---|---|---|---|---|---|
| Empty path addresses the root | Path vazio endereça a raiz | — | ✓ | ✓ | ✓ | |
| Real read stream | Stream de leitura real | ✓ | ✓ | ✓ | ✓ | |
| Progressive upload stream | Stream de upload progressivo | ✓ | ✓ | ✓ | ✓ | |
| Native copy | Cópia nativa | ✓ | ✓ | ✓ | streamed | via stream |
| Signed URL — read, write, delete | URL assinada — leitura, escrita, remoção | — | ✓ | ✓ | — | |
| Public URL without configuration | URL pública sem configuração | — | ✓ | ✓ | — | |
| Public URL from a configured base | URL pública a partir de uma base configurada | ✓ | ✓ | ✓ | ✓ | |
| Caller-owned injected client | Client injetado, do chamador | — | ✓ | ✓ | ✓ |
✓ supported · — not supported, and the call raises rather than pretending. The local filesystem provider has no SDK client to inject.
✓ suportado · — não suportado, e a chamada levanta erro em vez de fingir. O provider 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.
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, a remote stream piped into a remote stream over SFTP. destination_locationdestinationLocation addresses the destination when it differs, and the overwrite check always runs there.
native_copy_filenativeCopyFile copia dentro de um provider usando o mecanismo dele — CopyObject no S3, uma cópia de filesystem localmente, um stream remoto canalizado para outro stream remoto no SFTP. destination_locationdestinationLocation endereça o destino quando ele difere, e a verificação de overwrite sempre roda lá.
from storage_kernel.contracts import ( CreateSignedUrlOptions, NativeCopyFileOptions, StorageSignedUrlAction, ) signed = await storage.create_signed_url( StoragePath("2026/summary.csv"), CreateSignedUrlOptions( action=StorageSignedUrlAction.READ, expires_in_seconds=900, ), ) # signed.url, signed.expires_at, signed.method await storage.native_copy_file( StoragePath("2026/summary.csv"), StoragePath("2026/summary.csv"), NativeCopyFileOptions( destination_location=StorageLocation(bucket="archive"), ), )
const signed = await storage.createSignedUrl( { path: '2026/summary.csv' }, { action: 'read', expiresInSeconds: 900 }, ); // signed.url, signed.expiresAt, signed.method await storage.nativeCopyFile( { path: '2026/summary.csv' }, { path: '2026/summary.csv' }, { destinationLocation: { bucket: 'archive' } }, );
Four providers, one call
Quatro providers, 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 provider são tipadas por provider e nunca chegam ao contrato compartilhado. Cada um é uma distribuição própria, então você instala exatamente os SDKs que usa.
| Provider | Distribution | Distribuição | Roots on | Enraíza em | Use for | Usar 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 S3 and S3-compatible services. Also aws and s3. |
Objetos no S3 e em serviços compatíveis. Também aws e s3. |
||
gcp-storage |
storage-kernel-provider-gcp-storage@storage-kernel/provider-gcp-storage |
bucket |
Objects in Google Cloud Storage. Also gcp and gcs. |
Objetos no Google Cloud Storage. Também gcp e gcs. |
||
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 providers da AWS, ou as Application Default Credentials no GCP.
Injecting a client keeps that SDK client under your lifecycle. close() destroys only an S3 client the kernel built itself; 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() destrói apenas um client S3 que o próprio kernel 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.
from storage_kernel.contracts import StorageClientOptions 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 provider 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 OpenSSHknown_hostsfile.host_key_sha256hostKeySha256— a fingerprint inSHA256:<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 arquivoknown_hostsdo OpenSSH.host_key_sha256hostKeySha256— uma fingerprint no formatoSHA256:<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 StorageConfigurationError rather than a quiet fallback to something weaker.
Duas políticas ao mesmo tempo, ou uma fingerprint malformada, é um StorageConfigurationError 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.
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 provider 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.
from storage_kernel.provider_sftp import ( SftpStorageProviderOptions, ) storage = await create_storage_client( CreateStorageClientConfig( provider_name="sftp", location=StorageLocation(directory="uploads"), options=SftpStorageProviderOptions( host="sftp.example.com", username="app", private_key_path="/run/secrets/id_ed25519", known_hosts_path="/home/app/.ssh/known_hosts", ), ) )
const storage = await createStorageClient({ providerName: 'sftp', location: { directory: 'uploads' }, options: { host: 'sftp.example.com', username: 'app', privateKeyPath: '/run/secrets/id_ed25519', knownHostsPath: '/home/app/.ssh/known_hosts', }, });
Errors are part of the contract
Erros fazem parte do contrato
Every error carries provider, path, code and cause when the failure exposes them.
Todo erro carrega provider, path, code e cause quando a falha os expõe.
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.
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.
Everything else becomes StorageProviderError and keeps the message the provider produced. Wrapping adds context, it never translates: provider, path and code are attached and the original stays in cause. Structured loggers read the message and the traceback and do not walk the cause chain on their own.
Todo o resto vira StorageProviderError e preserva a mensagem que o provider produziu. O wrap acrescenta contexto, nunca traduz: provider, path e code são anexados e o original fica em cause. Loggers estruturados leem a mensagem e o traceback e não percorrem a cadeia de causas sozinhos.
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.
code is the provider's own failure code, not the name of the error class. The class already discriminates the type; repeating it as a code would report something meaningless on every ordinary failure.
code é o código de falha do próprio provider, não o nome da classe de erro. A classe já discrimina o tipo; repeti-la como código reportaria algo sem sentido em toda falha comum.
from storage_kernel.contracts import ( StoragePathNotFoundError, StoragePermissionError, StorageProviderError, ) try: data = await storage.read_binary( StoragePath("2026/summary.csv") ) except StoragePathNotFoundError as error: logger.warning("missing %s on %s", error.path, error.provider) raise except StoragePermissionError: raise except StorageProviderError as error: # the SDK's own message, plus provider/path/code logger.error("%s (%s)", error, error.code) raise
import { StoragePathNotFoundError, StoragePermissionError, StorageProviderError, } from '@storage-kernel/contracts'; try { const data = await storage.readBinary({ path: '2026/summary.csv', }); } catch (error) { if (error instanceof StoragePathNotFoundError) { logger.warn(`missing ${error.path} on ${error.provider}`); } else if (error instanceof StorageProviderError) { // the SDK's own message, plus provider/path/code logger.error(error.message, { code: error.code }); } throw error; }
| Class | Classe | Raised when | Levantado quando |
|---|---|---|---|
StoragePathNotFoundError | The requested path does not exist. Normalized across providers. | O path pedido não existe. Normalizado entre providers. | |
StoragePermissionError | The provider denied authentication or authorization. Normalized across providers. | O provider negou autenticação ou autorização. Normalizado entre providers. | |
StoragePathAlreadyExistsError | Overwrite was off and the destination already exists. | Overwrite estava desligado e o destino já existe. | |
StorageInvalidPathError | An empty, traversing or otherwise unsafe path. | Um path vazio, com travessia ou de outra forma inseguro. | |
StorageCapabilityNotSupportedError | The provider cannot implement an optional capability, such as a signed URL on a local disk. | O provider não consegue implementar uma capacidade opcional, como uma URL assinada num disco local. | |
StorageConfigurationError | Required, 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. | |
StorageProviderError | Any other provider or stream failure, with the provider's own message kept. | Qualquer outra falha de provider ou de stream, preservando a mensagem do próprio provider. |
Observability, and your own backend
Observabilidade, e seu próprio backend
Silent unless you ask, and open to a store the kernel has never heard of.
Silencioso a menos que você peça, e aberto a um destino que o kernel nunca viu.
Lifecycle events
Eventos de ciclo de vida
Pass a logger to receive events, or debug to send them to the console. Without either the client says nothing. 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 para receber eventos, ou debug para mandá-los ao console. Sem nenhum dos dois o client não diz nada. Os níveis são fixados pelo contrato, não por cada provider: início e conclusão em debug, path ausente ou upload abortado em warningwarn, falhas de permissão e de provider em error.
Completion is reported when it actually happened: a read stream after it has been fully consumed, an upload after end() succeeded — not when the call returned.
A conclusão é reportada quando realmente aconteceu: um stream de leitura depois de consumido por inteiro, um upload depois de end() ter dado certo — não quando a chamada retornou.
The context is provider, operation, path, an optional destination path, and a location trimmed to directory, bucket and prefix. Content, credentials and object metadata never appear in it.
O contexto é provider, operação, path, um path de destino opcional, e uma location reduzida a directory, bucket e prefix. Conteúdo, credenciais e metadados de objeto nunca aparecem nele.
A logger may implement any subset of the four levels; one it does not implement is silent unless debug is on.
Um logger pode implementar qualquer subconjunto dos quatro níveis; um que ele não implementa fica silencioso a menos que debug esteja ligado.
Bring your own provider
Traga seu próprio provider
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 provider é 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 events as the four that ship here without inheriting anything.
O client é envolvido na mesma fronteira de observabilidade nos dois casos, então um provider que você escreveu emite os mesmos eventos que os quatro que vêm aqui, sem herdar nada.
The testing package publishes the portable contract suite these four are held to, so yours can be held to it too.
O pacote de testing publica 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.
storage = await create_storage_client( CreateStorageClientConfig( provider_name="aws-s3", location=StorageLocation(bucket="app-files"), logger=my_logger, # .debug(message, context) ) )
const storage = await createStorageClient({ providerName: 'aws-s3', location: { bucket: 'app-files' }, logger: { debug: (message, context) => myLogger.debug(message, context), }, });
await create_storage_client( CreateStorageClientConfig( provider_name="azure-blob", provider_class=MyAzureBlobClient, ) )
await createStorageClient({ providerName: 'azure-blob', providerClass: MyAzureBlobClient, });
from storage_kernel.testing import ( assert_storage_client_contract, ) await assert_storage_client_contract(storage)
import { defineStorageClientContractTests, } from '@storage-kernel/testing'; defineStorageClientContractTests('azure-blob', context);
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.
The contract, the shared core, the factory, the four providers and the portable contract suite. Tagged before changelog fragments were being kept, which is why there is nothing itemised here.
O contrato, o core compartilhado, a factory, os quatro providers e a suíte de contrato portável. Tagueado antes de os fragmentos de changelog começarem a ser mantidos, e é por isso que não há itens detalhados aqui.
On PyPI so far: storage-kernel-contracts, storage-kernel-core and storage-kernel-factory. The provider distributions and storage-kernel-testing are still to come.
No PyPI até aqui: storage-kernel-contracts, storage-kernel-core e storage-kernel-factory. As distribuições de provider e o storage-kernel-testing ainda vêm.
The contract, the shared core, the factory, the four providers and the portable contract suite. Tagged before changelog fragments were being kept, which is why there is nothing itemised here.
O contrato, o core compartilhado, a factory, os quatro providers e a suíte de contrato portável. Tagueado antes de os fragmentos de changelog começarem a ser mantidos, e é por isso que não há itens detalhados aqui.
The tag exists; the packages are not published. Everything on this page matches that tag.
A tag existe; os pacotes não estão publicados. Tudo nesta página confere com essa tag.