Skip to content

Commit 6d4f170

Browse files
authored
Merge pull request #176 from axiomhq/feat/edge-based-ingestion
2 parents 6b57887 + f854ec5 commit 6d4f170

14 files changed

Lines changed: 1330 additions & 111 deletions

.github/workflows/ci.yml

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ jobs:
1818
- uses: actions/checkout@v4
1919
- uses: actions/setup-python@v5
2020
with:
21-
python-version: ${{ matrix.python }}
21+
python-version: "3.12"
2222
- name: Install uv
23-
uses: astral-sh/setup-uv@v3
23+
uses: astral-sh/setup-uv@v5
2424
- run: uv run ruff check
2525
- run: uv run ruff format --check
2626

@@ -34,25 +34,31 @@ jobs:
3434
fail-fast: true
3535
matrix:
3636
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
37+
environment:
38+
- development
39+
- staging
40+
include:
41+
- environment: development
42+
slug: DEV
43+
- environment: staging
44+
slug: STAGING
3745
steps:
3846
- uses: actions/checkout@v4
3947
- uses: actions/setup-python@v5
4048
with:
4149
python-version: ${{ matrix.python }}
4250
- name: Install uv
4351
uses: astral-sh/setup-uv@v5
44-
- name: Test against development
45-
run: uv run pytest
46-
env:
47-
AXIOM_URL: ${{ secrets.TESTING_DEV_API_URL }}
48-
AXIOM_TOKEN: ${{ secrets.TESTING_DEV_TOKEN }}
49-
AXIOM_ORG_ID: ${{ secrets.TESTING_DEV_ORG_ID }}
50-
- name: Test against staging
52+
- name: Run tests
5153
run: uv run pytest
5254
env:
53-
AXIOM_URL: ${{ secrets.TESTING_STAGING_API_URL }}
54-
AXIOM_TOKEN: ${{ secrets.TESTING_STAGING_TOKEN }}
55-
AXIOM_ORG_ID: ${{ secrets.TESTING_STAGING_ORG_ID }}
55+
AXIOM_URL: ${{ secrets[format('TESTING_{0}_API_URL', matrix.slug)] }}
56+
AXIOM_TOKEN: ${{ secrets[format('TESTING_{0}_TOKEN', matrix.slug)] }}
57+
AXIOM_ORG_ID: ${{ secrets[format('TESTING_{0}_ORG_ID', matrix.slug)] }}
58+
AXIOM_EDGE_URL: ${{ vars[format('TESTING_{0}_EDGE_URL', matrix.slug)] }}
59+
AXIOM_EDGE_TOKEN: ${{ secrets[format('TESTING_{0}_EDGE_TOKEN', matrix.slug)] }}
60+
AXIOM_EDGE_DATASET_REGION: ${{ vars[format('TESTING_{0}_EDGE_DATASET_REGION', matrix.slug)] }}
61+
AXIOM_DATASET_SUFFIX: ${{ github.run_id }}-${{ matrix.python }}
5662

5763
publish:
5864
name: Publish on PyPi

CLAUDE.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
axiom-py is the official Python client library for Axiom, a serverless log management and analytics platform. The library provides bindings for ingesting events, querying data, and managing datasets via the Axiom REST API.
8+
9+
## Development Commands
10+
11+
### Running Tests
12+
```bash
13+
# Run all tests (requires AXIOM_TOKEN, AXIOM_ORG_ID, and optionally AXIOM_URL env vars)
14+
uv run pytest
15+
16+
# Run a specific test file
17+
uv run pytest tests/test_client.py
18+
19+
# Run a specific test
20+
uv run pytest tests/test_client.py::TestClient::test_step001_ingest
21+
```
22+
23+
### Linting and Formatting
24+
```bash
25+
# Check code with ruff
26+
uv run ruff check
27+
28+
# Auto-fix issues
29+
uv run ruff check --fix
30+
31+
# Check formatting
32+
uv run ruff format --check
33+
34+
# Format code
35+
uv run ruff format
36+
```
37+
38+
### Pre-commit Hooks
39+
```bash
40+
# Install pre-commit hooks
41+
uv run pre-commit install
42+
43+
# Run hooks manually
44+
uv run pre-commit run --all-files
45+
```
46+
47+
### Building
48+
```bash
49+
# Build the package for distribution
50+
uv build
51+
```
52+
53+
## Architecture
54+
55+
### Core Components
56+
57+
**Client (`src/axiom_py/client.py`)**: The main `Client` class is the entry point for all Axiom operations. It manages:
58+
- HTTP session with automatic retries (3 retries with exponential backoff for 5xx errors)
59+
- Authentication via Bearer token (from constructor or `AXIOM_TOKEN` env var)
60+
- Organization ID handling (from constructor or `AXIOM_ORG_ID` env var)
61+
- Shutdown hooks registered with `atexit` to flush buffered logs
62+
63+
The Client exposes specialized sub-clients as properties:
64+
- `client.datasets` - Dataset CRUD operations
65+
- `client.annotations` - Annotation management
66+
- `client.users` - User information
67+
- `client.tokens` - API token management
68+
69+
**DatasetsClient (`src/axiom_py/datasets.py`)**: Handles dataset operations (create, get, list, update, delete, trim). Takes a requests.Session object and makes API calls to `/v1/datasets` endpoints.
70+
71+
**Query System (`src/axiom_py/query/`)**: Supports two query types:
72+
- **APL queries** (recommended): Uses Axiom Processing Language via `client.query()` or `client.apl_query()`. Supports both "legacy" and "tabular" result formats.
73+
- **Legacy structured queries**: Uses `QueryLegacy` dataclass with filters, aggregations, groupBy, etc. via `client.query_legacy()`.
74+
75+
**Logging Integration (`src/axiom_py/logging.py`, `src/axiom_py/structlog.py`)**:
76+
- `AxiomHandler`: Standard Python logging handler that buffers log records and flushes to Axiom every 1 second or when buffer reaches 1000 events.
77+
- `AxiomProcessor`: Structlog processor with similar buffering behavior.
78+
- Both use threading.Timer (AxiomHandler) or time checks to periodically flush.
79+
80+
### Key Design Patterns
81+
82+
**Ingestion**: Events are encoded as NDJSON and gzip-compressed before sending. The `ingest_events()` method is a convenience wrapper around the lower-level `ingest()` method that handles this automatically.
83+
84+
**Error Handling**: All HTTP responses are checked via a response hook that raises `AxiomError` for status codes >= 400. This exception includes the status code and error message from the API.
85+
86+
**Serialization**: Uses `dacite` for deserializing API responses to dataclasses, and custom JSON handling (`handle_json_serialization` in `util.py`) for datetime objects during serialization.
87+
88+
**Type Safety**: The codebase heavily uses dataclasses and type hints (Python 3.8+). Field names use snake_case in Python but are automatically converted to/from camelCase for API communication using the `pyhumps` library.
89+
90+
## Testing Guidelines
91+
92+
- Tests are integration tests that run against live Axiom environments (dev and staging)
93+
- Test methods should be prefixed with `test_step` and numbered (e.g., `test_step001_ingest`) to control execution order when needed
94+
- Use `get_random_name()` helper from `tests/helpers.py` to generate unique dataset names
95+
- Clean up resources in `tearDownClass` even on test failures to avoid zombie datasets
96+
- Tests use the `responses` library for mocking HTTP requests when testing client behavior (retries, error handling)
97+
98+
## Code Style
99+
100+
- Line length: 79 characters (PEP 8)
101+
- Uses Ruff for both linting and formatting
102+
- Python 3.8+ compatible (check `classifiers` in pyproject.toml before using newer syntax)
103+
104+
## Important Notes
105+
106+
- Version 0.9.0 removed the aggregation operation enum (see #158). Use string literals instead.
107+
- When using APL queries with `limit`, pass it via `AplOptions.limit` parameter (not in the APL query string).
108+
- Personal tokens don't require an org_id, but organization tokens do. The `is_personal_token()` utility in `tokens.py` detects token type.

README.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
44
# axiom-py [![CI][ci_badge]][ci] [![PyPI version][pypi_badge]][pypi] [![Python version][version_badge]][pypi]
55

6+
## Install
7+
8+
```sh
9+
pip install axiom-py
10+
```
11+
612
## Synchronous Client
713

814
```py
@@ -14,6 +20,30 @@ client.ingest_events(dataset="DATASET_NAME", events=[{"foo": "bar"}, {"bar": "ba
1420
client.query(r"['DATASET_NAME'] | where foo == 'bar' | limit 100")
1521
```
1622

23+
## Edge Ingestion
24+
25+
For improved data locality, you can configure the client to use regional edge
26+
endpoints for ingest and query operations. All other API operations continue to
27+
use the main Axiom API endpoint.
28+
29+
```python
30+
import axiom_py
31+
32+
# Using a regional edge domain
33+
edge_client = axiom_py.Client(
34+
token="xaat-your-api-token",
35+
edge="eu-central-1.aws.edge.axiom.co"
36+
)
37+
38+
# Or using an explicit edge URL
39+
edge_client = axiom_py.Client(
40+
token="xaat-your-api-token",
41+
edge_url="https://custom-edge.example.com"
42+
)
43+
```
44+
45+
**Note:** Edge endpoints require API tokens (`xaat-`), not personal tokens.
46+
Edge configuration must be passed explicitly when creating the client.
1747
## Asynchronous Client
1848

1949
The library also provides an async client for use with asyncio:
@@ -57,12 +87,6 @@ async def main():
5787
asyncio.run(main())
5888
```
5989

60-
## Install
61-
62-
```sh
63-
pip install axiom-py
64-
```
65-
6690
## Documentation
6791

6892
Read documentation on [axiom.co/docs/guides/python](https://axiom.co/docs/guides/python).

src/axiom_py/__init__.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
ContentType,
1313
ContentEncoding,
1414
WrongQueryKindException,
15+
PersonalTokenNotSupportedForEdgeError,
1516
AplOptions,
1617
Client,
18+
AXIOM_URL,
1719
)
1820
from .datasets import (
1921
Dataset,
@@ -38,34 +40,35 @@
3840
from .logging_async import AsyncAxiomHandler
3941
from .structlog_async import AsyncAxiomProcessor
4042

41-
__all__ = [
42-
# Exceptions & Models (shared between sync and async)
43-
"AxiomError",
44-
"IngestFailure",
45-
"IngestStatus",
46-
"IngestOptions",
47-
"AplResultFormat",
48-
"ContentType",
49-
"ContentEncoding",
50-
"WrongQueryKindException",
51-
"AplOptions",
52-
"Dataset",
53-
"TrimRequest",
54-
"Annotation",
55-
"AnnotationCreateRequest",
56-
"AnnotationUpdateRequest",
43+
_all_ = [
44+
AxiomError,
45+
IngestFailure,
46+
IngestStatus,
47+
IngestOptions,
48+
AplResultFormat,
49+
ContentType,
50+
ContentEncoding,
51+
WrongQueryKindException,
52+
PersonalTokenNotSupportedForEdgeError,
53+
AplOptions,
54+
AXIOM_URL,
55+
Dataset,
56+
TrimRequest,
57+
Annotation,
58+
AnnotationCreateRequest,
59+
AnnotationUpdateRequest,
5760
# Sync API
58-
"Client",
59-
"DatasetsClient",
60-
"AnnotationsClient",
61-
"AxiomHandler",
62-
"AxiomProcessor",
61+
Client,
62+
DatasetsClient,
63+
AnnotationsClient,
64+
AxiomHandler,
65+
AxiomProcessor,
6366
# Async API
64-
"AsyncClient",
65-
"AsyncDatasetsClient",
66-
"AsyncAnnotationsClient",
67-
"AsyncTokensClient",
68-
"AsyncUsersClient",
69-
"AsyncAxiomHandler",
70-
"AsyncAxiomProcessor",
67+
AsyncClient,
68+
AsyncDatasetsClient,
69+
AsyncAnnotationsClient,
70+
AsyncTokensClient,
71+
AsyncUsersClient,
72+
AsyncAxiomHandler,
73+
AsyncAxiomProcessor,
7174
]

0 commit comments

Comments
 (0)