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.
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();
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, codestrg_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_contentreadContentcall stays as written. - Tests without the cloud. The
local-filesystemprovider 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
StorageCapabilityNotSupportedErrorinstead 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ódigostrg_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_contentreadContentcontinua como foi escrita. - Testes sem nuvem. O armazenamento
local-filesystemsegue 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
StorageCapabilityNotSupportedErrorem 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 } }
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.
$ 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.
$ uv add storage-kernel-provider-local-filesystem
$ npm install @storage-kernel/provider-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.
$ uv add storage-kernel-provider-aws-s3
$ npm install @storage-kernel/provider-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.
$ uv add storage-kernel-provider-gcp-storage
$ npm install @storage-kernel/provider-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.
$ uv add storage-kernel-provider-sftp
$ npm install @storage-kernel/provider-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 } }
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.
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 armazenamento 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 armazenamentos 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, 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.
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');
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.
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.
# 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; }
| 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 an async byte iterable that carries close(). | Devolve um iterável assíncrono de bytes que carrega close(). | |
| 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 | Sends 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_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 armazenamento. | |
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 armazenamento 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 armazenamento 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 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.
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 |
Returns | Devolve |
|---|---|---|---|
| ✓ | ✓ | 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. |
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.
| Capability | Capacidade | local | aws-s3 | gcp-storage | sftp |
|---|---|---|---|---|---|
| Empty path addresses the root | Caminho vazio endereça a raiz | — | ✓ | ✓ | ✓ |
| Real read stream | Stream de leitura real | ✓ | ✓ | ✓ | ✓ |
| Progressive upload stream | Stream de upload progressivo | ✓ | ✓ | ✓ | ✓ |
| Native copy | Cópia nativa | ✓ | ✓ | ✓ | streamedvia streamin memoryem memória |
| 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 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.
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' } }, );
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 | 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 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.
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 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 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.
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, };
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.
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; }
| Class | Classe | code |
Raised when | Levantado quando |
|---|---|---|---|---|
StoragePathNotFoundError | strg_path_not_found | The requested path does not exist. Normalized across providers. | O caminho pedido não existe. Normalizado entre armazenamentos. | |
StoragePermissionError | strg_permission_denied | The provider denied authentication or authorization. Normalized across providers. | O armazenamento negou autenticação ou autorização. Normalizado entre armazenamentos. | |
StoragePathAlreadyExistsError | strg_path_already_exists | Overwrite was off and the destination already exists. | Overwrite estava desligado e o destino já existe. | |
StorageInvalidPathError | strg_invalid_path | An empty, traversing or otherwise unsafe path. | Um caminho vazio, com travessia ou de outra forma inseguro. | |
StorageCapabilityNotSupportedError | strg_capability_not_supported | The 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. | |
StorageKernelConfigurationError | strg_invalid_configuration | 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 | strg_unexpected | Any 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. |
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.
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, });
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_client_contract await assert_client_contract(storage)
import { defineClientContractTests, } from '@storage-kernel/testing'; defineClientContractTests('azure-blob', { createClient: () => new MyAzureBlobClient(), });
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_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 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.
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.
Changed · breaking
Alterado · breaking
- A path is a plain string. Every operation took
StoragePath("a/b.txt")and now takes"a/b.txt".StoragePathis gone from the contract. - A writable stream finishes with
close(), where it saidend().abort()still discards. codechanged owner. It held the vendor's identifier, which moved toprovider_code.codeis 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.StorageErrorCodeis aStrEnumof those seven.provider_codeis 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 asECONNREFUSED, when the service never answered; a plaininton GCP Storage. TypeScript reports the same values.StorageConfigurationErroris nowStorageKernelConfigurationError, with no alias. Itscodeisstrg_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
debugoption are gone, and each line says what happened and where.logging.getLogger("my-app")is a drop-in. read_streamreturns a stream that carries an idempotentclose(), so a reader that stops early can release the socket, descriptor or channel it holds. A custom provider subclassesIterableStorageReadableStream.send_streamtakes text, bytes or a stream, withSendStreamOptions.- The S3 provider names its SDK configuration
client_options, as TypeScript does. It wasclient_config. - The
create_*_storage_clientfunctions lose their third parameter: inject the SDK client throughclientin the provider options. - Errors are built from one
StorageKernelErrorOptions, andwrite_textandwrite_text_linetake aWriteTextOptionsinstead of anencodingkeyword — the same call shape as TypeScript. - Helpers take the names TypeScript uses —
join_storage_path,resolve_storage_location— and drop thestorageprefix where the type they return already says it:normalize_provider_error,assert_client_contractand nine more.STORAGE_PROVIDER_TARGETSisSTORAGE_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=Falseon 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".StoragePathsaiu do contrato. - Um stream de escrita termina com
close(), onde diziaend().abort()continua descartando. codemudou de dono. Ele guardava o identificador do fornecedor, que foi paraprovider_code.codeagora é 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é umStrEnumdesses 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, comoECONNREFUSED, quando o serviço nem respondeu; umintsimples no GCP Storage. O TypeScript reporta os mesmos valores.StorageConfigurationErroragora éStorageKernelConfigurationError, sem alias. Ocodeé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
debugsaíram, e cada linha diz o que aconteceu e onde.logging.getLogger("my-app")serve direto. read_streamdevolve um stream que carrega umclose()idempotente, então quem para de ler antes pode liberar o socket, descritor ou canal que ele segura. Um armazenamento próprio herda deIterableStorageReadableStream.send_streamrecebe texto, bytes ou um stream, comSendStreamOptions.- O armazenamento S3 chama sua configuração de SDK de
client_options, como o TypeScript. Eraclient_config. - As funções
create_*_storage_clientperdem o terceiro parâmetro: injete o client do SDK porclientnas opções do armazenamento. - Erros são construídos a partir de um único
StorageKernelErrorOptions, ewrite_textewrite_text_linerecebem umWriteTextOptionsem vez do keywordencoding— o mesmo formato de chamada do TypeScript. - Helpers adotam os nomes do TypeScript —
join_storage_path,resolve_storage_location— e perdem o prefixostorageonde o tipo que devolvem já o diz:normalize_provider_error,assert_client_contracte 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=Falseno 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.factoryre-exports the whole contract — errors, types, the provider registry — so everything is imported from it. Imports fromstorage_kernel.contractskeep 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.
.wavreportsaudio/wav. - Dependency floors say what the code needs:
boto3>=1.26.0,google-cloud-storage>=3.0.0,paramiko>=5.0.0, andbotocoreandgoogle-auth, which the providers import, are now declared.
- Instale a factory e um armazenamento.
storage_kernel.factoryreexporta o contrato inteiro — erros, tipos, o registro de armazenamentos —, então tudo é importado dela. Imports destorage_kernel.contractscontinuam 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.
.wavreportaaudio/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, ebotocoreegoogle-auth, que os armazenamentos importam, passam a ser declarados.
Fixed
Corrigido
- A native copy given only a
locationlands 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
statthe server refuses raisesStoragePermissionErrorinstead 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 genericStorageProviderError. - Building an S3 client no longer rewrites
DEFAULT_CLIENT_CONFIG.
- Uma cópia nativa que só recebe uma
locationcai 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
statSFTP que o servidor recusa levantaStoragePermissionErrorem 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 umStorageProviderErrorgenérico. - Construir um client S3 não reescreve mais o
DEFAULT_CLIENT_CONFIG.
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.
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.