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
1,541 changes: 771 additions & 770 deletions api/api.gen.go

Large diffs are not rendered by default.

865 changes: 433 additions & 432 deletions api/client/go/client.gen.go

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion api/client/javascript/src/client/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10770,7 +10770,11 @@ export interface components {
* Price
* @description The price of the rate card.
* When null, the feature or service is free.
* @example {}
* @example {
* "type": "flat",
* "amount": "100",
* "paymentTerm": "in_arrears"
* }
*/
price: components['schemas']['RateCardUsageBasedPrice'] | null
/**
Expand Down
2 changes: 1 addition & 1 deletion api/client/python/.gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
src/**/* linguist-generated=true
openmeter/**/* linguist-generated=true
22 changes: 15 additions & 7 deletions api/client/python/.gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# python
.python-version
# Packaging
*.egg-info/
*.egg
*.eggs/
dist/
build/
CHANGELOG.md

# Python cache
__pycache__/
*.pyc
build
dist
pydo.egg-info
.venv
*.pyo
*.pyd

# generate
openapi.yaml
# Virtual envs
.python-version
.venv/
venv/
5 changes: 5 additions & 0 deletions api/client/python/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include *.md
include LICENSE
include openmeter/py.typed
recursive-include tests *.py
recursive-include samples *.py *.md
Comment on lines +1 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that referenced paths and packaging approach are correct.

The MANIFEST.in configuration includes several path dependencies that should be verified:

  1. Line 3: openmeter/py.typed — Ensure this PEP 561 marker file exists at the specified path for type hint support.
  2. Line 4: Including test files in the distribution may increase package size significantly. Confirm this aligns with your packaging strategy for the SDK.
  3. Lines 1–5: Verify that the directory structure (tests, samples, openmeter) exists and that these rules capture all necessary non-code assets.

🏁 Script executed:

#!/bin/bash
# Description: Verify file structure and packaging compliance for MANIFEST.in

# Check for referenced files and directories
echo "=== Checking referenced paths ==="
[ -f "api/client/python/openmeter/py.typed" ] && echo "✓ py.typed found" || echo "✗ py.typed NOT found"
[ -d "api/client/python/tests" ] && echo "✓ tests directory found" || echo "✗ tests directory NOT found"
[ -d "api/client/python/samples" ] && echo "✓ samples directory found" || echo "✗ samples directory NOT found"

# Check for markdown files in root that will be included
echo "=== Markdown files to be included ==="
find api/client/python -maxdepth 1 -name "*.md" -type f

# Check if LICENSE exists
echo "=== LICENSE file ==="
[ -f "api/client/python/LICENSE" ] && echo "✓ LICENSE found" || echo "✗ LICENSE NOT found"

# Show structure of tests and samples to assess size impact
echo "=== File counts ==="
echo "Python files in tests: $(find api/client/python/tests -name "*.py" -type f 2>/dev/null | wc -l)"
echo "Python files in samples: $(find api/client/python/samples -name "*.py" -type f 2>/dev/null | wc -l)"

Length of output: 287


🏁 Script executed:

#!/bin/bash
# Find test and sample files that may be in different locations
echo "=== Searching for test-related files ==="
find api/client/python -type f -name "*test*.py" | head -20

echo "=== Searching for sample-related files ==="
find api/client/python -type f -name "*sample*.py" | head -20

echo "=== Checking for LICENSE files ==="
find . -maxdepth 2 -name "LICENSE*" -type f

echo "=== Checking setup.py or pyproject.toml ==="
ls -la api/client/python/setup.py api/client/python/pyproject.toml 2>/dev/null || echo "No setup.py or pyproject.toml found in api/client/python/"

echo "=== Full directory structure of api/client/python ==="
ls -la api/client/python/ | head -30

Length of output: 984


🏁 Script executed:

#!/bin/bash
# Check if setup.py or pyproject.toml exists at repo root or provides packaging context
find . -maxdepth 2 -name "setup.py" -o -name "pyproject.toml" | grep -E "(setup\.py|pyproject\.toml)$" | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Show actual structure of openmeter module to understand package layout
echo "=== openmeter module structure ==="
find api/client/python/openmeter -type f | head -30

Length of output: 1142


🏁 Script executed:

#!/bin/bash
# Check if tests/samples exist elsewhere in repo (not just in api/client/python)
echo "=== test files anywhere in repo ==="
find . -path ./node_modules -prune -o -type d -name "tests" -print 2>/dev/null

echo "=== samples directories anywhere in repo ==="
find . -path ./node_modules -prune -o -type d -name "samples" -print 2>/dev/null

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Check if there's a .gitignore that might hide these directories
echo "=== .gitignore files ==="
find api/client/python -name ".gitignore" -exec echo "File: {}" \; -exec cat {} \;

Length of output: 224


🏁 Script executed:

#!/bin/bash
# Look at git status to see if tests/samples are untracked or ignored
cd api/client/python 2>/dev/null && git status --short 2>/dev/null | head -20

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Final check: does MANIFEST.in reference paths that should be created?
echo "=== Directory listing of api/client/python ==="
find api/client/python -maxdepth 2 -type d

Length of output: 325


🏁 Script executed:

#!/bin/bash
# Check if there are any commented or alternative packaging configs
echo "=== Checking for other packaging configs ==="
ls -la api/client/python/ | grep -E "\.(cfg|ini|toml|txt)$"

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Final sanity check: verify openmeter package contents
echo "=== Contents of openmeter module ==="
ls -R api/client/python/openmeter/ 2>/dev/null | head -50

Length of output: 593


🏁 Script executed:

#!/bin/bash
# Check official MANIFEST.in documentation to confirm syntax/behavior
echo "Checking MANIFEST.in best practices..."
# Note: This would typically require web search for setuptools documentation

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Read pyproject.toml to understand packaging configuration
echo "=== pyproject.toml content ==="
cat api/client/python/pyproject.toml

Length of output: 902


🏁 Script executed:

#!/bin/bash
# Check if there's a parent pyproject.toml at repo root
echo "=== Repo root pyproject.toml (if exists) ==="
cat ./pyproject.toml 2>/dev/null || echo "No pyproject.toml at repo root"

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Verify where LICENSE should be copied/referenced from
echo "=== License file verification ==="
ls -la ./LICENSE
echo ""
echo "=== Can LICENSE be referenced from parent via MANIFEST.in? ==="
echo "Testing if '../LICENSE' would work from api/client/python/ context..."
# In MANIFEST.in, paths are relative to the distribution root which is api/client/python/
# So '../LICENSE' would need to be used, or LICENSE needs to be copied

Length of output: 227


🏁 Script executed:

#!/bin/bash
# Check what files exist in examples directory
echo "=== Examples directory contents ==="
find api/client/python/examples -type f

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Double-check if tests/samples might be created/ignored in build process
echo "=== Checking pyproject.toml for build-time directory creation ==="
grep -n "tests\|samples\|build" api/client/python/pyproject.toml || echo "No references found"

Length of output: 128


Fix MANIFEST.in: Remove non-existent directory references and correct LICENSE path.

The file references several paths that do not exist in the codebase:

  1. Line 2 — include LICENSE: The LICENSE file is at repository root (./LICENSE), not in the package directory. Change to include ../LICENSE or remove if Poetry metadata already handles licensing.
  2. Lines 4–5 — recursive-include tests *.py and recursive-include samples *.py *.md: The tests/ and samples/ directories do not exist. Remove these lines to prevent packaging errors.
  3. Note: The examples/ directory exists with sample code but is not included; verify if this is intentional.

Lines 1 and 3 are correct.

🤖 Prompt for AI Agents
In api/client/python/MANIFEST.in around lines 1 to 5, the manifest references
non-existent directories and an incorrect LICENSE path: remove the two lines
referencing non-existent directories ("recursive-include tests *.py" and
"recursive-include samples *.py *.md"), and either change "include LICENSE" to
"include ../LICENSE" so it points to the repository root or remove that line if
your Poetry/packaging metadata already supplies the license; also verify whether
the existing examples/ directory should be included (and add a recursive-include
for examples if needed).

137 changes: 106 additions & 31 deletions api/client/python/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# OpenMeter Python SDK

[https://pypi.org/project/openmeter](On PyPI)
[On PyPI](https://pypi.org/project/openmeter)

This package is generated by `@typespec/http-client-python` with Typespec.

## Prerequisites

Python version: >= 3.9
- Python 3.9 or later is required to use this package.

## Install

Expand All @@ -14,54 +16,127 @@ Python version: >= 3.9
pip install openmeter
```

## Quickstart
## Examples

### Setup

The client can be initialized with `openmeter.Client()`:
#### Synchronous Client

```python
from os import environ
from openmeter import Client

ENDPOINT = environ.get("OPENMETER_ENDPOINT") or "http://localhost:8888"
client = Client(
endpoint="https://openmeter.cloud",
token="your-api-token",
)
```

#### Async Client

```python
from openmeter.aio import Client

# it's recommended to also set the Accept header at the client level
client = Client(
endpoint=ENDPOINT,
headers={"Accept": "application/json"},
endpoint="https://openmeter.cloud",
token="your-api-token",
)
```

**Async** client can be initialized by importing the `Client` from `openmeter.aio`.
### Ingest an Event

Ingest events:
#### Synchronous

```python
from cloudevents.http import CloudEvent
from cloudevents.conversion import to_dict

event = CloudEvent(
attributes={
"type": "tokens",
"source": "openmeter-python",
"subject": "user-id",
},
import datetime
import uuid

from openmeter.models import Event

# Create an Event instance (following CloudEvents specification)
event = Event(
id=str(uuid.uuid4()),
source="my-app",
specversion="1.0",
type="prompt",
subject="customer-1",
time=datetime.datetime.now(datetime.timezone.utc),
data={
"prompt_tokens": 5,
"completion_tokens": 10,
"total_tokens": 15,
"model": "gpt-3.5-turbo",
"tokens": 100,
"model": "gpt-4o",
"type": "input",
},
)

resp = client.ingest_events(to_dict(event))
# Ingest the event
client.events.ingest_event(event)
```

#### Async

```python
import datetime
import uuid
import asyncio

from openmeter.aio import Client
from openmeter.models import Event

async def main():
async with Client(
endpoint="https://openmeter.cloud",
token="your-api-token",
) as client:
# Create an Event instance (following CloudEvents specification)
event = Event(
id=str(uuid.uuid4()),
source="my-app",
specversion="1.0",
type="prompt",
subject="customer-1",
time=datetime.datetime.now(datetime.timezone.utc),
data={
"tokens": 100,
"model": "gpt-4o",
"type": "input",
},
)

# Ingest the event
await client.events.ingest_event(event)

asyncio.run(main())
```

## Publish
### Query Meter

Update version number in `pyproject.toml`.
Run the following commands:
#### Synchronous

```sh
poetry config pypi-token.pypi {your_pypi_api_token}
poetry publish --build
```python
from openmeter.models import MeterQueryResult

# Query total values
r: MeterQueryResult = client.meters.query_json(meter_id_or_slug="tokens_total")
print("Query total values:", r.data[0].value)
```

#### Async

```python
import asyncio

from openmeter.aio import Client
from openmeter.models import MeterQueryResult

async def main():
async with Client(
endpoint="https://openmeter.cloud",
token="your-api-token",
) as client:
# Query total values
r: MeterQueryResult = await client.meters.query_json(
meter_id_or_slug="tokens_total"
)
print("Query total values:", r.data[0].value)

asyncio.run(main())
```
58 changes: 0 additions & 58 deletions api/client/python/config.yaml

This file was deleted.

15 changes: 15 additions & 0 deletions api/client/python/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Examples

Install dependencies

```sh
poetry install
```

Run examples

```sh
OPENMETER_ENDPOINT=https://openmeter.cloud \
OPENMETER_TOKEN=om_xxx \
poetry run python ./sync/ingest.py
```
72 changes: 72 additions & 0 deletions api/client/python/examples/async/customer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from os import environ
from typing import Optional
import asyncio

from openmeter.aio import Client
from openmeter.models import (
CustomerCreate,
CustomerReplaceUpdate,
CustomerUsageAttribution,
)
from corehttp.exceptions import HttpResponseError

ENDPOINT: str = environ.get("OPENMETER_ENDPOINT") or "https://openmeter.cloud"
token: Optional[str] = environ.get("OPENMETER_TOKEN")
customer_key: str = environ.get("OPENMETER_CUSTOMER_KEY") or "acme-corp-1"
subject_key: str = environ.get("OPENMETER_SUBJECT_KEY") or "acme-user-1"


async def main() -> None:
async with Client(
endpoint=ENDPOINT,
token=token,
) as client:
try:
# Create a customer
customer_create = CustomerCreate(
name="Acme Corporation",
key=customer_key,
description="A demo customer for testing",
primary_email="contact@acme-corp.example.com",
currency="EUR",
usage_attribution=CustomerUsageAttribution(subject_keys=[subject_key]),
metadata={
"industry": "technology",
},
)

created_customer = await client.customer.customers.create(customer_create)
print(f"Customer created successfully with ID: {created_customer.id}")
print(f"Customer name: {created_customer.name}")
print(f"Customer key: {created_customer.key}")

# Get the customer by ID or key
customer = await client.customer.customers.get(created_customer.id)
print(f"\nRetrieved customer: {customer.name}")
print(f"Primary email: {customer.primary_email}")
print(f"Currency: {customer.currency}")

# Update the customer
customer_update = CustomerReplaceUpdate(
name="Acme Corporation Ltd.",
key=customer_key,
description="Updated demo customer",
primary_email="info@acme-corp.example.com",
currency="USD",
usage_attribution=CustomerUsageAttribution(subject_keys=[subject_key]),
metadata={
"industry": "technology",
},
)

updated_customer = await client.customer.customers.update(created_customer.id, customer_update)
print(f"\nCustomer updated successfully")
print(f"Updated name: {updated_customer.name}")
print(f"Updated email: {updated_customer.primary_email}")
print(f"Updated currency: {updated_customer.currency}")

except HttpResponseError as e:
print(f"Error: {e}")


asyncio.run(main())
Loading
Loading