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 ¶
- Variables
- func Apply(r io.Reader, transforms ...Transform) (io.ReadCloser, error)
- func CSV(r io.Reader, opts ...CSVOption) iter.Seq2[[]string, error]
- func CSVDicts(r io.Reader, opts ...CSVOption) iter.Seq2[map[string]string, error]
- func JSONValues[T any](r io.Reader, opts ...json.Options) iter.Seq2[T, error]
- func Lines(r io.Reader) iter.Seq2[[]byte, error]
- type CSVOption
- type Option
- func WithClientOptions(fns ...func(*s3.Options)) Option
- func WithExpectedBucketOwner(accountID string) Option
- func WithGetObjectInput(fn func(*s3.GetObjectInput)) Option
- func WithHeadObjectInput(fn func(*s3.HeadObjectInput)) Option
- func WithRequestPayer(payer types.RequestPayer) Option
- func WithSSECustomerKey(key []byte) Option
- func WithVersionID(id string) Option
- type S3API
- type S3Object
- func (o *S3Object) Bucket() string
- func (o *S3Object) Close() error
- func (o *S3Object) Key() string
- func (o *S3Object) Read(p []byte) (int, error)
- func (o *S3Object) ReadAt(p []byte, off int64) (int, error)
- func (o *S3Object) Seek(offset int64, whence int) (int64, error)
- func (o *S3Object) Size() (int64, error)
- func (o *S3Object) Transform(transforms ...Transform) (io.ReadCloser, error)
- func (o *S3Object) VersionID() string
- func (o *S3Object) WriteTo(w io.Writer) (int64, error)
- func (o *S3Object) Zip() (*zip.Reader, error)
- type Transform
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
Types ¶
type CSVOption ¶
type CSVOption func(*csvOptions)
CSVOption configures CSV and CSVDicts, as the dialect options of Python's csv module do.
func WithComment ¶
WithComment skips the lines that start with r.
func WithDelimiter ¶
WithDelimiter separates fields with r instead of a comma: '\t' for TSV.
func WithFieldnames ¶
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 ¶
WithRestKey keeps the values of a row longer than the header under key, joined by the delimiter. Without it they are dropped.
func WithRestValue ¶
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 ¶
WithClientOptions applies fns to the client options of every request.
func WithExpectedBucketOwner ¶
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 ¶
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 ¶
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 ¶
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) Close ¶
Close releases the open response bodies. Reading afterwards fails; closing again does nothing.
func (*S3Object) Read ¶
Read reads from the current position, opening a ranged GetObject the first time and after a Seek that moves.
func (*S3Object) ReadAt ¶
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 ¶
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 ¶
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) WriteTo ¶
WriteTo writes the object from the current position to its end into w, with a single request: io.Copy uses it.
func (*S3Object) Zip ¶
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 ¶
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.