Skip to content

Commit b604bcd

Browse files
crdantOpen Handsclaude
authored
Aligns SDK endpoints with Vandoor telemetry architecture (#5)
TL;DR ----- Aligns Python SDK with available or soon to be deployed endpoints Details -------- The Python SDK previously called several endpoints that don't exist in Vandoor, causing instance tracking and metrics reporting to fail. This change either This change adopts the telemetry-based architecture used by the Go SDK. Instance metadata flows through telemetry headers to `/kots_metrics/license_instance/info` rather than explicit CRUD operations. Authentication now correctly distinguishes between publishable keys (which use Bearer prefix) and service tokens (raw token without prefix), matching Vandoor's middleware expectations. Custom metrics use the proper format with `{"data": {name: value}}` structure and correct `/application/custom-metrics` endpoint. Co-Authored-By: @jpshackelford Co-Authored-By: Open Hands <noreply@all-hands.dev> Co-Authored-by: Claude Code <noreply@anthropic.com>
1 parent d5466ff commit b604bcd

12 files changed

Lines changed: 270 additions & 126 deletions

File tree

.envrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
layout python python3.12
2+
dotenv_if_exists

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ share/python-wheels/
2323
*.egg
2424
MANIFEST
2525

26-
# Virtual environments
26+
# Virtual environments
2727
.env
2828
.venv
2929
env/
3030
venv/
3131
ENV/
3232
env.bak/
3333
venv.bak/
34+
.direnv
3435

3536
# IDE
3637
.vscode/
@@ -61,4 +62,4 @@ Thumbs.db
6162
# Replicated SDK state
6263
Library/
6364
.local/
64-
AppData/
65+
AppData/

examples/async_example.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import asyncio
77

8-
from replicated import AsyncReplicatedClient, InstanceStatus
8+
from replicated import AsyncReplicatedClient
99

1010

1111
async def main():
@@ -31,13 +31,6 @@ async def main():
3131
)
3232
print("Metrics sent successfully")
3333

34-
# Set the instance status and version concurrently
35-
await asyncio.gather(
36-
instance.set_status(InstanceStatus.RUNNING),
37-
instance.set_version("1.2.0"),
38-
)
39-
print("Instance status set to RUNNING and version set to 1.2.0")
40-
4134
print("Example completed successfully!")
4235

4336

examples/metrics_example.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Basic example of using the Replicated Python SDK.
4+
This script initializes the replicated package, creates a customer and instance.
5+
"""
6+
7+
import argparse
8+
import asyncio
9+
10+
from replicated import AsyncReplicatedClient
11+
12+
13+
async def main():
14+
parser = argparse.ArgumentParser(description="Basic Replicated SDK example")
15+
parser.add_argument(
16+
"--base-url",
17+
default="https://replicated.app",
18+
help="Base URL for the Replicated API (default: https://replicated.app)",
19+
)
20+
parser.add_argument(
21+
"--publishable-key",
22+
required=True,
23+
help="Your Replicated publishable key (required)",
24+
)
25+
parser.add_argument(
26+
"--app-slug", required=True, help="Your application slug (required)"
27+
)
28+
parser.add_argument(
29+
"--customer-email",
30+
default="user@example.com",
31+
help="Customer email address (default: user@example.com)",
32+
)
33+
parser.add_argument("--channel", help="Channel for the customer (optional)")
34+
parser.add_argument("--customer-name", help="Customer name (optional)")
35+
parser.add_argument(
36+
"--status",
37+
choices=["missing", "unavailable", "ready", "updating", "degraded"],
38+
default="ready",
39+
help="Instance status (default: ready)",
40+
)
41+
42+
args = parser.parse_args()
43+
44+
print("Initializing Replicated client...")
45+
print(f"Base URL: {args.base_url}")
46+
print(f"App Slug: {args.app_slug}")
47+
48+
# Initialize the client
49+
async with AsyncReplicatedClient(
50+
publishable_key=args.publishable_key,
51+
app_slug=args.app_slug,
52+
base_url=args.base_url,
53+
) as client:
54+
print("✓ Replicated client initialized successfully")
55+
56+
# Create or get customer
57+
channel_info = f" (channel: {args.channel})" if args.channel else ""
58+
name_info = f" (name: {args.customer_name})" if args.customer_name else ""
59+
print(
60+
f"\nCreating/getting customer with email: "
61+
f"{args.customer_email}{channel_info}{name_info}"
62+
)
63+
customer = await client.customer.get_or_create(
64+
email_address=args.customer_email,
65+
channel=args.channel,
66+
name=args.customer_name,
67+
)
68+
print(f"✓ Customer created/retrieved - ID: {customer.customer_id}")
69+
70+
# Get or create the associated instance
71+
instance = await customer.get_or_create_instance()
72+
print(f"Instance ID: {instance.instance_id}")
73+
print(f"✓ Instance created/retrieved - ID: {instance.instance_id}")
74+
75+
# Get or create the associated instance
76+
instance = await customer.get_or_create_instance()
77+
print(f"Instance ID: {instance.instance_id}")
78+
79+
# Set instance status
80+
await instance.set_status(args.status)
81+
print(f"✓ Instance status set to: {args.status}")
82+
83+
# Send some metrics concurrently
84+
await asyncio.gather(
85+
instance.send_metric("cpu_usage", 0.83),
86+
instance.send_metric("memory_usage", 0.67),
87+
instance.send_metric("disk_usage", 0.45),
88+
)
89+
print("Metrics sent successfully")
90+
91+
print(f"Instance ID: {instance.instance_id}")
92+
93+
94+
if __name__ == "__main__":
95+
asyncio.run(main())

examples/sync_example.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Synchronous example of using the Replicated Python SDK.
44
"""
55

6-
from replicated import InstanceStatus, ReplicatedClient
6+
from replicated import ReplicatedClient
77

88

99
def main():
@@ -27,14 +27,6 @@ def main():
2727
instance.send_metric("disk_usage", 0.45)
2828
print("Metrics sent successfully")
2929

30-
# Set the instance status
31-
instance.set_status(InstanceStatus.RUNNING)
32-
print("Instance status set to RUNNING")
33-
34-
# Set the application version
35-
instance.set_version("1.2.0")
36-
print("Instance version set to 1.2.0")
37-
3830
print("Example completed successfully!")
3931

4032

replicated/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
ReplicatedRateLimitError,
1010
)
1111

12-
__version__ = "1.0.0"
12+
# Try to get version from package metadata, fall back to hardcoded version
13+
try:
14+
from importlib.metadata import version as _get_version
15+
16+
__version__ = _get_version("replicated") + "+python"
17+
except Exception:
18+
# Fallback for development or if package isn't installed
19+
__version__ = "1.0.0+python"
1320
__all__ = [
1421
"ReplicatedClient",
1522
"AsyncReplicatedClient",

replicated/async_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def _get_auth_headers(self) -> Dict[str, str]:
4141
# Try to use dynamic token first, fall back to publishable key
4242
dynamic_token = self.state_manager.get_dynamic_token()
4343
if dynamic_token:
44-
return {"Authorization": f"Bearer {dynamic_token}"}
44+
# Service tokens are sent without Bearer prefix
45+
return {"Authorization": dynamic_token}
4546
else:
47+
# Publishable keys use Bearer prefix
4648
return {"Authorization": f"Bearer {self.publishable_key}"}

replicated/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def _get_auth_headers(self) -> Dict[str, str]:
4141
# Try to use dynamic token first, fall back to publishable key
4242
dynamic_token = self.state_manager.get_dynamic_token()
4343
if dynamic_token:
44-
return {"Authorization": f"Bearer {dynamic_token}"}
44+
# Service tokens are sent without Bearer prefix
45+
return {"Authorization": dynamic_token}
4546
else:
47+
# Publishable keys use Bearer prefix
4648
return {"Authorization": f"Bearer {self.publishable_key}"}

replicated/http_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ def _build_headers(
2828
self, headers: Optional[Dict[str, str]] = None
2929
) -> Dict[str, str]:
3030
"""Build request headers."""
31+
# Import here to avoid circular import
32+
from . import __version__
33+
34+
# Format: "Replicated-SDK/{version}" as expected by Vandoor
35+
user_agent = f"Replicated-SDK/{__version__}"
3136
request_headers = {
3237
"Content-Type": "application/json",
33-
"User-Agent": "replicated-python/1.0.0",
38+
"User-Agent": user_agent,
3439
**self.default_headers,
3540
}
3641
if headers:
@@ -51,11 +56,6 @@ def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
5156
error_message = json_body.get("message", default_msg)
5257
error_code = json_body.get("code")
5358

54-
# Debug: print the full error response
55-
print(f"DEBUG: HTTP {response.status_code} Error Response:")
56-
print(f"DEBUG: Response body: {response.text}")
57-
print(f"DEBUG: JSON body: {json_body}")
58-
5959
if response.status_code == 401:
6060
raise ReplicatedAuthError(
6161
message=error_message,

0 commit comments

Comments
 (0)