streaming

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package streaming processes S3 objects larger than the function's memory as streams: the Go side of Powertools Python's streaming utility.

Reading an object

S3Object fetches the bytes of an object with ranged GetObject requests, as they are read, and seeks without downloading what it skips:

obj := streaming.NewS3Object(ctx, client, bucket, key)
defer obj.Close()
for line, err := range streaming.Lines(obj) {
	...
}

It is an io.Reader, io.Seeker, io.ReaderAt, io.WriterTo and io.Closer, so it plugs into anything that reads files. Nothing is requested until the first read; a sequential read takes one request, and a Seek opens a new one only when the position moves. Reads at or past the end return io.EOF, as for a file. Size reports the size, from the first response or from a HeadObject request.

Options pass what S3 needs to serve the object: WithVersionID for a versioned bucket, WithSSECustomerKey for SSE-C, WithRequestPayer, WithExpectedBucketOwner, and WithGetObjectInput / WithHeadObjectInput for any other request field.

Consistency

The first response pins the object's ETag, and every later request carries it in If-Match: an object overwritten while it is read fails with ErrObjectChanged instead of returning the bytes of two versions. Powertools Python does not check; WithVersionID reads a version that cannot change.

Transformations

A Transform turns a byte stream into another one: Gzip decompresses, and any func(io.Reader) (io.Reader, error) is a custom transformation (decryption, a character set decoder from golang.org/x/text). Apply, or S3Object.Transform, chains them in order:

r, err := obj.Transform(streaming.Gzip())
defer r.Close()
for row, err := range streaming.CSVDicts(r) {
	fmt.Println(row["id"])
}

The decoders turn a stream into an iterator: CSVDicts (Python's CsvTransform, a csv.DictReader) and CSV, Lines, and JSONValues for JSON Lines. They work on any io.Reader, so transformations are tested on a strings.Reader, without S3.

S3Object.Zip opens a zip archive with random access: the central directory at the end, then only the members opened, each streamed (Python's ZipTransform). As in Python, a zip archive cannot feed another transformation, but each member can.

Differences from Powertools Python

  • S3Object is the raw object. Python's constructor flags (is_gzip, is_csv) and transform(in_place=True) make the object read transformed data while it still seeks in raw bytes; Go composes instead: obj.Transform returns a new reader, and Seek stays on the raw object. Seek before transforming to start somewhere else, as the Python recipes do.
  • CSV uses encoding/csv: the delimiter, comment character, lazy quotes and leading space trimming are options; the quote character is always '"'. Text must be UTF-8: decode other encodings with a Transform. A missing column is "" (WithRestValue), not None, and the extra values of a long row are dropped unless WithRestKey keeps them, joined.
  • The reading context is the one given to NewS3Object: io.Reader has no context parameter.
  • Reads are checked against the first ETag (see Consistency).
  • The file methods Python must raise NotImplementedError for (write, fileno, truncate) do not exist; readline and 1024-byte chunk iteration become Lines and plain Read calls.
  • Powertools Python tags its S3 requests with a Powertools user agent; this port does not pose as the official library.

Tracing

Every range request is an S3 call. Instrumenting the S3 client with OpenTelemetry records each one as a span, which adds up on large objects: as Powertools Python advises for X-Ray, keep tracing away from the reading loop.

The package reads no environment variable of its own; a client built from the default configuration follows the AWS SDK's.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrObjectChanged = errors.New("streaming: the S3 object changed while it was read")

ErrObjectChanged is what reading an object matches when it was overwritten after the reading began: the next range request no longer matches the ETag of the first response.

Functions

func Apply

func Apply(r io.Reader, transforms ...Transform) (io.ReadCloser, error)

Apply returns r through the transforms, applied in order: the first one reads r, the next one reads what the first one returns, and so on. Closing the result closes every layer that is an io.Closer, the outermost first, r included.

Example
package main

import (
	"fmt"
	"io"
	"strings"

	"github.com/alessandrolattao/powertools-lambda-go/streaming"
)

func main() {
	// A custom transformation: any func(io.Reader) (io.Reader, error).
	upper := func(r io.Reader) (io.Reader, error) {
		data, err := io.ReadAll(r)
		if err != nil {
			return nil, err
		}
		return strings.NewReader(strings.ToUpper(string(data))), nil
	}

	// Transformations are tested on any reader, without S3.
	r, err := streaming.Apply(strings.NewReader("hello\n"), upper)
	if err != nil {
		panic(err)
	}
	data, err := io.ReadAll(r)
	if err != nil {
		panic(err)
	}
	fmt.Print(string(data))
	if err := r.Close(); err != nil {
		panic(err)
	}
}
Output:
HELLO

func CSV

func CSV(r io.Reader, opts ...CSVOption) iter.Seq2[[]string, error]

CSV returns the rows of the CSV stream r. A UTF-8 byte order mark at the start is skipped. Rows may have different lengths, as in Python's csv module. A malformed row comes with its *csv.ParseError and the iteration goes on; a read error ends it.

func CSVDicts

func CSVDicts(r io.Reader, opts ...CSVOption) iter.Seq2[map[string]string, error]

CSVDicts returns the rows of the CSV stream r as maps from column name to value: Powertools Python's CsvTransform, a csv.DictReader. The first row names the columns, unless WithFieldnames does. A short row has the columns it lacks set to "" (WithRestValue); a long row loses the extra values (WithRestKey keeps them).

Example
package main

import (
	"fmt"
	"strings"

	"github.com/alessandrolattao/powertools-lambda-go/streaming"
)

func main() {
	tsv := "id\tname\n1\tann\n2\tbob\n"
	for row, err := range streaming.CSVDicts(strings.NewReader(tsv), streaming.WithDelimiter('\t')) {
		if err != nil {
			panic(err)
		}
		fmt.Println(row["id"], row["name"])
	}
}
Output:
1 ann
2 bob

func JSONValues

func JSONValues[T any](r io.Reader, opts ...json.Options) iter.Seq2[T, error]

JSONValues returns the JSON values of r decoded into T, one at a time: newline-delimited JSON (JSON Lines) or values simply concatenated. It is the JSON transform of the Powertools Python documentation, with encoding/json/v2 and the options given. A malformed value ends the iteration with its error.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/alessandrolattao/powertools-lambda-go/streaming"
)

func main() {
	type event struct {
		Type string `json:"type"`
	}
	lines := "{\"type\":\"created\"}\n{\"type\":\"deleted\"}\n"
	for e, err := range streaming.JSONValues[event](strings.NewReader(lines)) {
		if err != nil {
			panic(err)
		}
		fmt.Println(e.Type)
	}
}
Output:
created
deleted

func Lines

func Lines(r io.Reader) iter.Seq2[[]byte, error]

Lines returns the lines of r without their line ending ("\n" or "\r\n"), whatever their length: what iterating a gunzipped stream gives in Python. The last line needs no line ending. A read error ends the iteration.

Types

type CSVOption

type CSVOption func(*csvOptions)

CSVOption configures CSV and CSVDicts, as the dialect options of Python's csv module do.

func WithComment

func WithComment(r rune) CSVOption

WithComment skips the lines that start with r.

func WithDelimiter

func WithDelimiter(r rune) CSVOption

WithDelimiter separates fields with r instead of a comma: '\t' for TSV.

func WithFieldnames

func WithFieldnames(names ...string) CSVOption

WithFieldnames names the columns for CSVDicts, which then reads the first row as data rather than as the header.

func WithLazyQuotes

func WithLazyQuotes() CSVOption

WithLazyQuotes accepts quotes in unquoted fields and unescaped quotes in quoted ones.

func WithRestKey

func WithRestKey(key string) CSVOption

WithRestKey keeps the values of a row longer than the header under key, joined by the delimiter. Without it they are dropped.

func WithRestValue

func WithRestValue(value string) CSVOption

WithRestValue fills the columns a short row lacks with value instead of "".

func WithTrimLeadingSpace

func WithTrimLeadingSpace() CSVOption

WithTrimLeadingSpace ignores the spaces after a delimiter: Python's skipinitialspace.

type Option

type Option func(*options)

Option configures an S3Object.

func WithClientOptions

func WithClientOptions(fns ...func(*s3.Options)) Option

WithClientOptions applies fns to the client options of every request.

func WithExpectedBucketOwner

func WithExpectedBucketOwner(accountID string) Option

WithExpectedBucketOwner fails the requests if the bucket does not belong to the given account.

func WithGetObjectInput

func WithGetObjectInput(fn func(*s3.GetObjectInput)) Option

WithGetObjectInput sets any other field of the GetObject requests (response header overrides, checksum mode, conditions), after the options above. Bucket, Key and Range always are the object's.

func WithHeadObjectInput

func WithHeadObjectInput(fn func(*s3.HeadObjectInput)) Option

WithHeadObjectInput sets any other field of the HeadObject request that reads the size, after the options above. Bucket and Key always are the object's.

func WithRequestPayer

func WithRequestPayer(payer types.RequestPayer) Option

WithRequestPayer acknowledges the charges of a Requester Pays bucket (types.RequestPayerRequester).

func WithSSECustomerKey

func WithSSECustomerKey(key []byte) Option

WithSSECustomerKey reads an object encrypted with a customer-provided key (SSE-C, AES256): the raw 256-bit key, which the option encodes and checksums as S3 requires, as boto3 does.

func WithVersionID

func WithVersionID(id string) Option

WithVersionID reads the given version of the object, in a versioned bucket.

type S3API

type S3API interface {
	GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
	HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
}

S3API is the part of the S3 client an S3Object calls; *s3.Client satisfies it.

type S3Object

type S3Object struct {
	// contains filtered or unexported fields
}

S3Object reads an S3 object as a stream, fetching its bytes with ranged GetObject requests as they are read, so that objects larger than the function's memory can be processed. It implements io.Reader, io.Seeker, io.ReaderAt, io.WriterTo and io.Closer: Powertools Python's S3Object.

The first response pins the object's ETag: every later request carries it in If-Match, so an object overwritten while it is read fails with ErrObjectChanged instead of mixing the bytes of two versions.

Read, Seek and WriteTo share a position; ReadAt has its own, as io.ReaderAt requires. An S3Object is safe for concurrent use, though concurrent readers take turns.

func NewS3Object

func NewS3Object(ctx context.Context, client S3API, bucket, key string, opts ...Option) *S3Object

NewS3Object returns a stream over the object key of bucket. Nothing is requested until the first read. ctx bounds every request the object makes: pass the invocation's context. A nil client is replaced by one built from the default AWS configuration, once per process, as boto3 does.

Example
ctx := context.Background()
obj := streaming.NewS3Object(ctx, sampleBucket(), "my-bucket", "orders.csv")
for line, err := range streaming.Lines(obj) {
	if err != nil {
		panic(err)
	}
	fmt.Println(string(line))
}
if err := obj.Close(); err != nil {
	panic(err)
}
Output:
id,customer,total
1,ann,12.50
2,bob,7.00
3,eve,30.25

func (*S3Object) Bucket

func (o *S3Object) Bucket() string

Bucket returns the bucket of the object.

func (*S3Object) Close

func (o *S3Object) Close() error

Close releases the open response bodies. Reading afterwards fails; closing again does nothing.

func (*S3Object) Key

func (o *S3Object) Key() string

Key returns the key of the object.

func (*S3Object) Read

func (o *S3Object) Read(p []byte) (int, error)

Read reads from the current position, opening a ranged GetObject the first time and after a Seek that moves.

func (*S3Object) ReadAt

func (o *S3Object) ReadAt(p []byte, off int64) (int, error)

ReadAt reads len(p) bytes at off, without moving the position of Read. Reads that continue where the previous one ended share one response body, so that reading a zip member takes a single request.

func (*S3Object) Seek

func (o *S3Object) Seek(offset int64, whence int) (int64, error)

Seek sets the position of the next Read: io.SeekStart, io.SeekCurrent or io.SeekEnd (which needs the size). The response body open at the old position is discarded only when the position moves, because some readers, such as zip ones, seek to where they already are.

Example
obj := streaming.NewS3Object(context.Background(), sampleBucket(), "my-bucket", "orders.csv")

// Read only the last row, which is 12 bytes long: the header is not
// there, so name the columns.
if _, err := obj.Seek(-12, io.SeekEnd); err != nil {
	panic(err)
}
for row, err := range streaming.CSVDicts(obj, streaming.WithFieldnames("id", "customer", "total")) {
	if err != nil {
		panic(err)
	}
	fmt.Println(row["id"], row["customer"])
}
if err := obj.Close(); err != nil {
	panic(err)
}
Output:
3 eve

func (*S3Object) Size

func (o *S3Object) Size() (int64, error)

Size returns the size of the object, from the first response if a read already happened, from a HeadObject request otherwise.

func (*S3Object) Transform

func (o *S3Object) Transform(transforms ...Transform) (io.ReadCloser, error)

Transform returns the object, from the current position, through the transforms: Powertools Python's S3Object.transform. Closing the result closes the object too.

Example
obj := streaming.NewS3Object(context.Background(), sampleBucket(), "my-bucket", "orders.csv.gz")

// Gunzip, then parse: Python's S3Object(..., is_gzip=True, is_csv=True).
r, err := obj.Transform(streaming.Gzip())
if err != nil {
	panic(err)
}
for row, err := range streaming.CSVDicts(r) {
	if err != nil {
		panic(err)
	}
	fmt.Println(row["customer"], row["total"])
}
if err := r.Close(); err != nil { // closes obj too
	panic(err)
}
Output:
ann 12.50
bob 7.00

func (*S3Object) VersionID

func (o *S3Object) VersionID() string

VersionID returns the version WithVersionID asked for, if any.

func (*S3Object) WriteTo

func (o *S3Object) WriteTo(w io.Writer) (int64, error)

WriteTo writes the object from the current position to its end into w, with a single request: io.Copy uses it.

func (*S3Object) Zip

func (o *S3Object) Zip() (*zip.Reader, error)

Zip opens the object as a zip archive, reading the central directory at its end and then only the members that are opened: Powertools Python's ZipTransform. Store and Deflate are supported; register other methods (LZMA, Zstandard, bzip2) on the returned reader with RegisterDecompressor.

Example
obj := streaming.NewS3Object(context.Background(), sampleBucket(), "my-bucket", "reports.zip")
archive, err := obj.Zip()
if err != nil {
	panic(err)
}
for _, f := range archive.File {
	fmt.Println(f.Name)
}
member, err := archive.Open("reports/today.txt")
if err != nil {
	panic(err)
}
content, err := io.ReadAll(member)
if err != nil {
	panic(err)
}
fmt.Print(string(content))
if err := errors.Join(member.Close(), obj.Close()); err != nil {
	panic(err)
}
Output:
reports/today.txt
all good

type Transform

type Transform func(r io.Reader) (io.Reader, error)

Transform turns a byte stream into another one, as the bytes are read: decompression, decryption, character set conversion. Powertools Python's BaseTransform; Gzip is the built-in one, and any function of this shape is a custom one.

A transform that returns an io.Closer is closed with the pipeline.

func Gzip

func Gzip() Transform

Gzip decompresses a gzip stream, member after member: Powertools Python's GzipTransform. An empty stream reads as empty, as Python's GzipFile does.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL