Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.11.0] - 2026-07-23

### Added

- `accounts`: Added account details, active plan, and usage report clients.
- `datasets`: Added source JSON pointers, queryable metadata, JSON Schema references, semantic roles, and well-known protobuf message and enum fields to dataset creation and updates, including generated STAC types.
- `datasets`: Added fluent Boolean and numeric expressions for filtering datapoints by custom queryable fields.
- `datasets`: Added fluent Boolean, string, and numeric expressions for filtering datapoints by custom queryable fields.

### Changed

Expand Down Expand Up @@ -157,7 +160,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for Tilebox Observability, including logging and tracing helpers.
- Added examples for using the library.

[Unreleased]: https://github.com/tilebox/tilebox-go/compare/v0.10.0...HEAD
[Unreleased]: https://github.com/tilebox/tilebox-go/compare/v0.11.0...HEAD
[0.11.0]: https://github.com/tilebox/tilebox-go/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/tilebox/tilebox-go/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/tilebox/tilebox-go/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/tilebox/tilebox-go/compare/v0.7.1...v0.8.0
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ For examples on how to use the library, see the [examples](examples) directory.

### Filtering Dataset Queries

Fields marked queryable in a dataset schema can be filtered with fluent Boolean and numeric expressions. Multiple
Fields marked queryable in a dataset schema can be filtered with fluent Boolean, string, and numeric expressions. Multiple
expressions passed to `WithFilters` are combined with each other and with temporal and spatial filters using logical
AND.

Expand Down Expand Up @@ -77,6 +77,7 @@ func main() {
datasets.WithTemporalExtent(query.NewTimeInterval(start, end)),
datasets.WithFilters(
query.Field("eo_cloud_cover").LessThan(20.0),
query.Field("granule_name").Equal("S2A_GRANULE"),
query.Or(
query.Field("quality").GreaterThanOrEqual(80),
query.Field("quality").IsNull(),
Expand Down
38 changes: 38 additions & 0 deletions accounts/v1/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package accounts // import "github.com/tilebox/tilebox-go/accounts/v1"

import (
"context"
"fmt"

"connectrpc.com/connect"
"github.com/tilebox/tilebox-go/observability"
accountsv1alpha1 "github.com/tilebox/tilebox-go/protogen/accounts/v1alpha1"
"github.com/tilebox/tilebox-go/protogen/accounts/v1alpha1/accountsv1alpha1connect"
"go.opentelemetry.io/otel/trace"
)

// AccountClient provides access to account details for the authenticated credential.
type AccountClient interface {
// GetAccountDetails returns details about the account associated with the authenticated credential.
GetAccountDetails(ctx context.Context) (*accountsv1alpha1.AccountDetails, error)
}

var _ AccountClient = &accountClient{}

type accountClient struct {
connectClient accountsv1alpha1connect.AccountServiceClient
tracer trace.Tracer
}

func (c *accountClient) GetAccountDetails(ctx context.Context) (*accountsv1alpha1.AccountDetails, error) {
return observability.WithSpanResult(ctx, c.tracer, "accounts/details/get", func(ctx context.Context) (*accountsv1alpha1.AccountDetails, error) {
response, err := c.connectClient.GetAccountDetails(ctx, connect.NewRequest(
accountsv1alpha1.GetAccountDetailsRequest_builder{}.Build(),
))
if err != nil {
return nil, fmt.Errorf("failed to get account details: %w", err)
}

return response.Msg, nil
})
}
83 changes: 83 additions & 0 deletions accounts/v1/billing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package accounts // import "github.com/tilebox/tilebox-go/accounts/v1"

import (
"context"
"fmt"

"connectrpc.com/connect"
"github.com/tilebox/tilebox-go/observability"
accountsv1alpha1 "github.com/tilebox/tilebox-go/protogen/accounts/v1alpha1"
"github.com/tilebox/tilebox-go/protogen/accounts/v1alpha1/accountsv1alpha1connect"
"go.opentelemetry.io/otel/trace"
)

// BillingClient provides access to account billing information.
type BillingClient interface {
// GetActivePlan returns the active subscription plan for the authenticated account.
GetActivePlan(ctx context.Context) (*accountsv1alpha1.Plan, error)

// GetUsageReport returns the current usage report for the authenticated account.
//
// Options:
// - WithHistoryDays: includes historical values for the requested number of days.
GetUsageReport(ctx context.Context, options ...UsageReportOption) (*accountsv1alpha1.UsageReport, error)
}

var _ BillingClient = &billingClient{}

type billingClient struct {
connectClient accountsv1alpha1connect.BillingServiceClient
tracer trace.Tracer
}

func (c *billingClient) GetActivePlan(ctx context.Context) (*accountsv1alpha1.Plan, error) {
return observability.WithSpanResult(ctx, c.tracer, "accounts/billing/active_plan/get", func(ctx context.Context) (*accountsv1alpha1.Plan, error) {
response, err := c.connectClient.GetActivePlan(ctx, connect.NewRequest(
accountsv1alpha1.GetActivePlanRequest_builder{}.Build(),
))
if err != nil {
return nil, fmt.Errorf("failed to get active plan: %w", err)
}

return response.Msg, nil
})
}

func (c *billingClient) GetUsageReport(ctx context.Context, options ...UsageReportOption) (*accountsv1alpha1.UsageReport, error) {
usageReportOptions := newUsageReportOptions(options)
return observability.WithSpanResult(ctx, c.tracer, "accounts/billing/usage_report/get", func(ctx context.Context) (*accountsv1alpha1.UsageReport, error) {
response, err := c.connectClient.GetUsageReport(ctx, connect.NewRequest(
accountsv1alpha1.GetUsageReportRequest_builder{
HistoryDays: usageReportOptions.historyDays,
}.Build(),
))
if err != nil {
return nil, fmt.Errorf("failed to get usage report: %w", err)
}

return response.Msg, nil
})
}

type usageReportOptions struct {
historyDays uint64
}

// UsageReportOption configures a usage report request.
type UsageReportOption func(*usageReportOptions)

// WithHistoryDays includes historical usage values for the requested number of days.
// The API supports up to 365 days.
func WithHistoryDays(historyDays uint64) UsageReportOption {
return func(options *usageReportOptions) {
options.historyDays = historyDays
}
}

func newUsageReportOptions(options []UsageReportOption) usageReportOptions {
var result usageReportOptions
for _, option := range options {
option(&result)
}
return result
}
150 changes: 150 additions & 0 deletions accounts/v1/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Package accounts provides a client for interacting with Tilebox Accounts.
package accounts // import "github.com/tilebox/tilebox-go/accounts/v1"

import (
"context"
"net"
"net/http"
"os"
"strings"

"connectrpc.com/connect"
"github.com/tilebox/tilebox-go/internal/grpc"
"github.com/tilebox/tilebox-go/protogen/accounts/v1alpha1/accountsv1alpha1connect"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
)

const otelTracerName = "tilebox.com/observability"

// Client is a Tilebox Accounts client.
type Client struct {
Account AccountClient
Billing BillingClient
}

// NewClient creates a new Tilebox Accounts client.
//
// By default, the returned Client is configured with:
// - "https://api.tilebox.com" as the URL
// - environment variable TILEBOX_API_KEY as the API key
// - a grpc.RetryHTTPClient HTTP client
// - the global tracer provider
//
// The passed options are used to override these default values and configure the returned Client appropriately.
func NewClient(options ...ClientOption) *Client {
cfg := newClientConfig(options)
accountConnectClient := newConnectClient(accountsv1alpha1connect.NewAccountServiceClient, cfg)
billingConnectClient := newConnectClient(accountsv1alpha1connect.NewBillingServiceClient, cfg)
tracer := cfg.tracerProvider.Tracer(otelTracerName)

return &Client{
Account: &accountClient{
connectClient: accountConnectClient,
tracer: tracer,
},
Billing: &billingClient{
connectClient: billingConnectClient,
tracer: tracer,
},
}
}

// clientConfig contains the configuration for a Tilebox Accounts client.
type clientConfig struct {
httpClient connect.HTTPClient
url string
apiKey string
connectOptions []connect.ClientOption

tracerProvider trace.TracerProvider
}

// ClientOption configures a client.
type ClientOption func(*clientConfig)

// WithHTTPClient sets the connect.HTTPClient to use for the client.
//
// Defaults to grpc.RetryHTTPClient.
func WithHTTPClient(httpClient connect.HTTPClient) ClientOption {
return func(cfg *clientConfig) {
cfg.httpClient = httpClient
}
}

// WithURL sets the URL of the Tilebox Accounts service.
//
// Defaults to "https://api.tilebox.com".
func WithURL(url string) ClientOption {
return func(cfg *clientConfig) {
cfg.url = url
}
}

// WithAPIKey sets the API key to use for the client.
//
// Defaults to the TILEBOX_API_KEY environment variable.
func WithAPIKey(apiKey string) ClientOption {
return func(cfg *clientConfig) {
cfg.apiKey = apiKey
}
}

// WithConnectClientOptions sets additional options for the connect.HTTPClient.
func WithConnectClientOptions(options ...connect.ClientOption) ClientOption {
return func(cfg *clientConfig) {
cfg.connectOptions = append(cfg.connectOptions, options...)
}
}

// WithDisableTracing disables OpenTelemetry tracing for the client.
func WithDisableTracing() ClientOption {
return func(cfg *clientConfig) {
cfg.tracerProvider = noop.NewTracerProvider()
}
}

func newClientConfig(options []ClientOption) *clientConfig {
cfg := &clientConfig{
url: "https://api.tilebox.com",
apiKey: os.Getenv("TILEBOX_API_KEY"),
tracerProvider: otel.GetTracerProvider(),
}
for _, option := range options {
option(cfg)
}

if cfg.httpClient == nil {
if strings.HasPrefix(cfg.url, "https://") || strings.HasPrefix(cfg.url, "http://") {
cfg.httpClient = grpc.RetryHTTPClient()
} else {
address := cfg.url
dial := func(ctx context.Context, _ string, _ string) (net.Conn, error) {
var dialer net.Dialer
return dialer.DialContext(ctx, "unix", address)
}
transport := &http.Transport{DialContext: dial}
cfg.httpClient = &http.Client{Transport: transport}
cfg.url = "http://localhost"
}
}

return cfg
}

func newConnectClient[T any](newClientFunc func(httpClient connect.HTTPClient, baseURL string, options ...connect.ClientOption) T, cfg *clientConfig) T {
interceptors := make([]connect.Interceptor, 0)
if cfg.apiKey != "" {
interceptors = append(interceptors, grpc.NewAddAuthTokenInterceptor(func() string {
return cfg.apiKey
}))
}

return newClientFunc(
cfg.httpClient,
cfg.url,
connect.WithClientOptions(cfg.connectOptions...),
connect.WithInterceptors(interceptors...),
)
}
Loading