Skip to content
Open
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
68 changes: 68 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Changelog

All notable changes to `arc-tsdb-client` are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-07-03

A large feature-parity release bringing the SDK up to date with the Arc server,
plus bug fixes and the first real HTTP-layer test coverage.

### Added

- **MessagePack query** (`query.query_msgpack`, experimental): decodes the
columnar MessagePack response with native timestamp handling and returns the
same `QueryResponse` shape as `query()` (with per-column Arrow `types`). Raises
`ArcNotSupportedError` when the server lacks the `duckdb_arrow` build tag (501).
- **Bulk import client** (`client.import_`): `import_csv`, `import_parquet`,
`import_lp`, `import_tle`, and `import_stats`. Accepts paths, file-like objects,
or bytes. New models: `ImportResult`, `LPImportResult`, `TLEImportResult`,
`ImportStats`.
- **TLE streaming write** (`write.write_tle`) and `write.tle_stats`.
- **Databases management** (`client.databases`): `list`, `get`,
`list_measurements`, `create`, `delete`. New models under `models.database`.
- **Enterprise sub-clients** (sync + async): `rbac`, `governance`, `audit`,
`queries` (query management), `cluster`, `compaction`, `tiering`, `backup`,
`mqtt`.
- **zstd write compression** (now the default; gzip and none still supported).
Compression is configurable via a `Compression` enum, a string, or a bool.
- **Automatic retries** with exponential backoff + jitter for connection errors,
429 (honoring `Retry-After`), and 503. Configurable via `max_retries` /
`retry_backoff`.
- New exceptions: `ArcNotSupportedError` (501) and `ArcLicenseError` (403 for a
missing enterprise-license feature; subclass of `ArcAuthenticationError`).
- Real HTTP round-trip test coverage (via `pytest-httpx`) for headers, error
mapping, compression on the wire, retries, msgpack decode, imports, databases,
and enterprise wrappers — sync and async. Opt-in live integration tests under
`tests/integration` (gated on `ARC_TEST_URL`).

### Changed

- Default write compression is now **zstd** (was gzip). Passing
`compression=True` still maps to gzip for backward compatibility.
- Writes no longer send a `Content-Encoding` header; Arc auto-detects the codec
from the payload's magic bytes.
- `User-Agent` is now derived from the package version instead of a hardcoded
string, and the package version is a single source of truth read by the build.

### Fixed

- Polars datetime → columnar conversion used a fragile private attribute that
raised at runtime; it now casts via the public `polars` API and normalizes the
series' `time_unit` to microseconds.
- Pandas `datetime64[us]` (and `ms`/`s`) timestamps were mis-scaled by assuming
nanoseconds; conversion now respects the column's resolution — including
tz-naive columns, whose numpy `datetime64` dtype exposes no `.unit` (the unit
is read via `numpy.datetime_data`). Previously these landed ~1000x too small.
- The buffered writer's time-based flush was passive (fired only on the next
write). It now runs a background timer (thread for sync, task for async) so
idle buffers still flush on `flush_interval`.
- `show_tables` interpolated the database name into SQL without validation; the
identifier is now validated against Arc's naming rules.
- Removed the dead, empty `dataframe` package stub.

## [0.1.2]

- Pad missing columns with `None` in the buffered columnar merge (#202).
139 changes: 126 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,27 @@ with ArcClient(host="localhost", token="your-token") as client:
## Features

- **High-performance ingestion**: MessagePack columnar format (9M+ records/sec)
- **Multiple formats**: MessagePack columnar/row, InfluxDB Line Protocol
- **Multiple formats**: MessagePack columnar/row, InfluxDB Line Protocol, TLE
- **Compression**: gzip **and zstd** (zstd is the default and recommended codec)
- **DataFrame integration**: Pandas, Polars, PyArrow
- **Sync and async APIs**: Full async support with httpx
- **Buffered writes**: Automatic batching with size and time thresholds
- **Query support**: SQL queries with JSON or Arrow IPC streaming
- **Buffered writes**: Automatic batching with a background flush timer
- **Automatic retries**: Exponential backoff for connection errors, 429, and 503
- **Query support**: SQL queries with JSON, Arrow IPC streaming, or MessagePack
- **Bulk import**: CSV, Parquet, line-protocol files, and TLE files
- **Databases**: List, create, delete databases and list measurements
- **Management**: Retention policies, continuous queries, delete operations
- **Enterprise**: RBAC, query governance, audit logs, query management, cluster,
compaction, storage tiering, backup/restore, and MQTT ingestion
- **Authentication**: Token management (create, rotate, revoke)

## Data Ingestion

### Columnar Format (Recommended)

The fastest way to write data. Uses MessagePack with gzip compression:
The fastest way to write data. Uses MessagePack with zstd compression by
default (the server recommends zstd; gzip and no compression are also supported —
see [Configuration](#configuration)):

```python
client.write.write_columnar(
Expand Down Expand Up @@ -134,6 +142,56 @@ lines = [
client.write.write_line_protocol(lines)
```

### TLE (Two-Line Element) Streaming

Stream satellite TLE text; the server parses it into orbital-element fields:

```python
tle = """ISS (ZARYA)
1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9994
2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.49309239432416"""

client.write.write_tle(tle, measurement="satellite_tle")
```

## Bulk Import

Import files directly (admin token required). Each returns a typed result with
the number of rows imported. Files may be gzip-compressed for LP/TLE.

```python
# CSV
result = client.import_.import_csv("metrics.csv", measurement="cpu",
time_column="time", time_format="epoch_ms")
print(result.rows_imported)

# Parquet
client.import_.import_parquet("data.parquet", measurement="cpu")

# InfluxDB line-protocol file
client.import_.import_lp("dump.lp", precision="ns")

# TLE file
client.import_.import_tle("catalog.tle", measurement="satellite_tle")
```

`import_csv`/`import_parquet`/`import_lp`/`import_tle` accept a path, a binary
file-like object, or raw `bytes`.

## Databases

```python
client.databases.create("metrics")
for db in client.databases.list().databases:
print(db.name, db.measurement_count)

info = client.databases.get("metrics")
measurements = client.databases.list_measurements("metrics")

# Destructive — requires confirm=True and server-side delete enabled
client.databases.delete("metrics", confirm=True)
```

## Querying Data

### JSON Response
Expand Down Expand Up @@ -163,6 +221,21 @@ pl_df = client.query.query_polars("SELECT * FROM default.cpu LIMIT 1000")
table = client.query.query_arrow("SELECT * FROM default.cpu LIMIT 1000")
```

### MessagePack Response (experimental)

A compact columnar MessagePack response with native timestamp decoding. Returns
the same `QueryResponse` shape as `query()` (row-major `data`), plus per-column
Arrow types in `.types`:

```python
result = client.query.query_msgpack("SELECT * FROM default.cpu LIMIT 1000")
print(result.columns, result.types)
```

> **Experimental.** This uses `/api/v1/query/msgpack`, which requires the Arc
> server to be built with the `duckdb_arrow` build tag. Against a server without
> it, the call raises `ArcNotSupportedError` (HTTP 501).

### Query Estimation

Preview query cost before execution:
Expand Down Expand Up @@ -313,33 +386,73 @@ print(f"New token: {rotated.new_token}")
client.auth.revoke_token(token_id=123)
```

## Enterprise Operations

These require an Arc server with the corresponding enterprise license feature.
Without it, calls raise `ArcLicenseError` (a subclass of `ArcAuthenticationError`).
Each sub-client is a thin wrapper that returns the server's JSON.

```python
# RBAC: organizations -> teams -> roles -> measurement permissions
org = client.rbac.create_organization("acme")
client.rbac.add_token_to_team(token_id=123, team_id=1)

# Query governance (per-token rate limits & quotas)
client.governance.create_policy(token_id=123, rate_limit_per_minute=600)
print(client.governance.usage(123))

# Audit logs
client.audit.logs(event_type="query", since="2024-01-01T00:00:00Z", limit=100)

# Query management
client.queries.active()
client.queries.kill("query-id")

# Cluster / compaction / tiering / backup / MQTT
client.cluster.status()
client.compaction.trigger(tier="hourly,daily")
client.tiering.set_policy("metrics", hot_only=True, hot_max_age_days=7)
client.backup.create()
client.mqtt.list_subscriptions()
```

## Configuration

```python
from arc_client import ArcClient, Compression

client = ArcClient(
host="localhost", # Arc server hostname
port=8000, # Arc server port (default: 8000)
token="your-token", # API token
database="default", # Default database
timeout=30.0, # Request timeout in seconds
compression=True, # Enable gzip compression for writes
ssl=False, # Use HTTPS
verify_ssl=True, # Verify SSL certificates
host="localhost", # Arc server hostname
port=8000, # Arc server port (default: 8000)
token="your-token", # API token
database="default", # Default database
timeout=30.0, # Request timeout in seconds
compression=Compression.ZSTD, # "zstd" (default), "gzip", "none", or a bool
ssl=False, # Use HTTPS
verify_ssl=True, # Verify SSL certificates
max_retries=3, # Retries for connection errors / 429 / 503
retry_backoff=0.5, # Base backoff (seconds) for exponential retry
)
```

`compression` accepts a `Compression` enum, a string (`"zstd"`/`"gzip"`/`"none"`),
or a bool for backward compatibility (`True` → gzip, `False` → none). Arc detects
the codec from the payload's magic bytes, so no `Content-Encoding` header is sent.

## Error Handling

```python
from arc_client.exceptions import (
ArcError, # Base exception
ArcConnectionError, # Connection failures
ArcAuthenticationError,# Auth failures (401)
ArcAuthenticationError,# Auth failures (401/403)
ArcLicenseError, # Enterprise feature not licensed (403)
ArcQueryError, # Query execution errors
ArcIngestionError, # Write failures
ArcValidationError, # Invalid input
ArcNotFoundError, # Resource not found (404)
ArcRateLimitError, # Rate limited (429)
ArcNotSupportedError, # Operation unsupported by server (501)
ArcServerError, # Server errors (5xx)
)

Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "arc-tsdb-client"
version = "0.1.2"
dynamic = ["version"]
description = "Python SDK for Arc time-series database"
readme = "README.md"
license = { text = "MIT" }
Expand All @@ -31,6 +31,7 @@ dependencies = [
"msgpack>=1.0.7",
"pyarrow>=15.0.0",
"pydantic>=2.6.0",
"zstandard>=0.22.0",
]

[project.urls]
Expand Down Expand Up @@ -60,6 +61,9 @@ docs = [
"nbsphinx>=0.9.0",
]

[tool.hatch.version]
path = "src/arc_client/__init__.py"

[tool.hatch.build.targets.sdist]
include = ["/src"]

Expand Down Expand Up @@ -88,4 +92,4 @@ warn_unused_ignores = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "-v --cov=arc_client --cov-report=term-missing"
addopts = "-v --cov=src/arc_client --cov-report=term-missing"
13 changes: 11 additions & 2 deletions src/arc_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
"""Arc Python SDK - High-performance client for Arc time-series database."""

# Defined before submodule imports so modules that read arc_client.__version__
# (e.g. the User-Agent header builder) can import it without a circular-import
# failure during package initialization.
__version__ = "0.2.0"

from arc_client.async_client import AsyncArcClient
from arc_client.client import ArcClient
from arc_client.compression import Compression
from arc_client.config import ClientConfig
from arc_client.exceptions import (
ArcAuthenticationError,
ArcConnectionError,
ArcError,
ArcIngestionError,
ArcLicenseError,
ArcNotFoundError,
ArcNotSupportedError,
ArcQueryError,
ArcRateLimitError,
ArcServerError,
ArcValidationError,
)

__version__ = "0.1.2"

__all__ = [
# Clients
"ArcClient",
"AsyncArcClient",
# Config
"ClientConfig",
"Compression",
# Exceptions
"ArcError",
"ArcConnectionError",
Expand All @@ -33,6 +40,8 @@
"ArcNotFoundError",
"ArcRateLimitError",
"ArcServerError",
"ArcNotSupportedError",
"ArcLicenseError",
# Version
"__version__",
]
Loading
Loading