From 3de32415dd2d97ad6b79d004fd334e126df6629c Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Wed, 10 Sep 2025 19:08:13 -0500 Subject: [PATCH 1/4] add example --- examples/README.md | 49 ++++++++++++++++++++++++ examples/basic_example.py | 78 +++++++++++++++++++++++++++++++++++++++ replicated/http_client.py | 5 +++ replicated/services.py | 44 ++++++++++++++++++---- replicated/state.py | 11 ++++++ 5 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/basic_example.py diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c8958b7 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,49 @@ +# Replicated Python SDK Examples + +This directory contains examples demonstrating how to use the Replicated Python SDK. + +## Basic Example + +The `basic_example.py` script demonstrates the most fundamental usage of the SDK: +- Initializing the Replicated client +- Creating or retrieving a customer +- Creating or retrieving an instance + +### Usage + +```bash +python3 basic_example.py \ + --publishable-key "your_publishable_key_here" \ + --app-slug "your-app-slug" \ + --customer-email "customer@example.com" \ + --customer-name "John Doe" \ + --channel "stable" +``` + +### Options + +- `--publishable-key`: Your Replicated publishable key (required) +- `--app-slug`: Your application slug (required) +- `--customer-email`: Customer email address (default: user@example.com) +- `--channel`: Channel for the customer (optional) +- `--customer-name`: Customer name (optional) +- `--base-url`: Base URL for the Replicated API (default: https://replicated.app) + +### Using Different Environments + +To use the SDK against a different environment (not production), use the `--base-url` flag: + +```bash +python3 basic_example.py \ + --publishable-key "your_publishable_key_here" \ + --app-slug "your-app-slug" \ + --base-url "https://staging.replicated.app" \ + --customer-email "customer@example.com" \ + --customer-name "Jane Smith" \ + --channel "beta" +``` + +## Other Examples + +- `sync_example.py`: More comprehensive synchronous example with metrics and status updates +- `async_example.py`: Asynchronous version of the SDK usage \ No newline at end of file diff --git a/examples/basic_example.py b/examples/basic_example.py new file mode 100644 index 0000000..3251b52 --- /dev/null +++ b/examples/basic_example.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Basic example of using the Replicated Python SDK. +This script initializes the replicated package, creates a customer and instance. +""" + +import argparse +from replicated import ReplicatedClient + + +def main(): + parser = argparse.ArgumentParser(description="Basic Replicated SDK example") + parser.add_argument( + "--base-url", + default="https://replicated.app", + help="Base URL for the Replicated API (default: https://replicated.app)" + ) + parser.add_argument( + "--publishable-key", + required=True, + help="Your Replicated publishable key (required)" + ) + parser.add_argument( + "--app-slug", + required=True, + help="Your application slug (required)" + ) + parser.add_argument( + "--customer-email", + default="user@example.com", + help="Customer email address (default: user@example.com)" + ) + parser.add_argument( + "--channel", + help="Channel for the customer (optional)" + ) + parser.add_argument( + "--customer-name", + help="Customer name (optional)" + ) + + args = parser.parse_args() + + print(f"Initializing Replicated client...") + print(f"Base URL: {args.base_url}") + print(f"App Slug: {args.app_slug}") + + # Initialize the client + with ReplicatedClient( + publishable_key=args.publishable_key, + app_slug=args.app_slug, + base_url=args.base_url, + ) as client: + print("āœ“ Replicated client initialized successfully") + + # Create or get customer + channel_info = f" (channel: {args.channel})" if args.channel else "" + name_info = f" (name: {args.customer_name})" if args.customer_name else "" + print(f"\nCreating/getting customer with email: {args.customer_email}{channel_info}{name_info}") + customer = client.customer.get_or_create( + email_address=args.customer_email, + channel=args.channel, + name=args.customer_name + ) + print(f"āœ“ Customer created/retrieved - ID: {customer.customer_id}") + + # Create or get instance + print(f"\nCreating/getting instance for customer...") + instance = customer.get_or_create_instance() + print(f"āœ“ Instance created/retrieved - ID: {instance.instance_id}") + + print("\nšŸŽ‰ Basic example completed successfully!") + print(f"Customer ID: {customer.customer_id}") + print(f"Instance ID: {instance.instance_id}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/replicated/http_client.py b/replicated/http_client.py index 00849b5..ef317a7 100644 --- a/replicated/http_client.py +++ b/replicated/http_client.py @@ -50,6 +50,11 @@ def _handle_response(self, response: httpx.Response) -> Dict[str, Any]: default_msg = f"HTTP {response.status_code}" error_message = json_body.get("message", default_msg) error_code = json_body.get("code") + + # Debug: print the full error response + print(f"DEBUG: HTTP {response.status_code} Error Response:") + print(f"DEBUG: Response body: {response.text}") + print(f"DEBUG: JSON body: {json_body}") if response.status_code == 401: raise ReplicatedAuthError( diff --git a/replicated/services.py b/replicated/services.py index 5255e2c..0eb0c7f 100644 --- a/replicated/services.py +++ b/replicated/services.py @@ -17,37 +17,51 @@ def get_or_create( self, email_address: str, channel: Optional[str] = None, + name: Optional[str] = None, ) -> Customer: """Get or create a customer.""" - # Check if customer ID is cached + # Check if customer ID is cached and email matches cached_customer_id = self._client.state_manager.get_customer_id() - if cached_customer_id: + cached_email = self._client.state_manager.get_customer_email() + + if cached_customer_id and cached_email == email_address: + print(f"DEBUG: Using cached customer ID {cached_customer_id} for email {email_address}") return Customer( self._client, cached_customer_id, email_address, channel, ) + elif cached_customer_id and cached_email != email_address: + print(f"DEBUG: Email changed from {cached_email} to {email_address}, clearing cache") + self._client.state_manager.clear_state() # Create or fetch customer response = self._client.http_client._make_request( "POST", - "/api/v1/customers", + "/v3/customer", json_data={ "email_address": email_address, "channel": channel, + "name": name, "app_slug": self._client.app_slug, }, headers=self._client._get_auth_headers(), ) - customer_id = response["id"] + print(f"DEBUG: API Response: {response}") + customer_id = response["customer"]["id"] self._client.state_manager.set_customer_id(customer_id) + self._client.state_manager.set_customer_email(email_address) # Store dynamic token if provided if "dynamic_token" in response: dynamic_token = response["dynamic_token"] self._client.state_manager.set_dynamic_token(dynamic_token) + elif "customer" in response and "serviceToken" in response["customer"]: + service_token = response["customer"]["serviceToken"] + self._client.state_manager.set_dynamic_token(service_token) + print(f"DEBUG: Stored service token: {service_token[:20]}...") response_data = response.copy() response_data.pop("email_address", None) @@ -71,37 +85,51 @@ async def get_or_create( self, email_address: str, channel: Optional[str] = None, + name: Optional[str] = None, ) -> AsyncCustomer: """Get or create a customer.""" - # Check if customer ID is cached + # Check if customer ID is cached and email matches cached_customer_id = self._client.state_manager.get_customer_id() - if cached_customer_id: + cached_email = self._client.state_manager.get_customer_email() + + if cached_customer_id and cached_email == email_address: + print(f"DEBUG: Using cached customer ID {cached_customer_id} for email {email_address}") return AsyncCustomer( self._client, cached_customer_id, email_address, channel, ) + elif cached_customer_id and cached_email != email_address: + print(f"DEBUG: Email changed from {cached_email} to {email_address}, clearing cache") + self._client.state_manager.clear_state() # Create or fetch customer response = await self._client.http_client._make_request_async( "POST", - "/api/v1/customers", + "/v3/customer", json_data={ "email_address": email_address, "channel": channel, + "name": name, "app_slug": self._client.app_slug, }, headers=self._client._get_auth_headers(), ) - customer_id = response["id"] + print(f"DEBUG: API Response: {response}") + customer_id = response["customer"]["id"] self._client.state_manager.set_customer_id(customer_id) + self._client.state_manager.set_customer_email(email_address) # Store dynamic token if provided if "dynamic_token" in response: dynamic_token = response["dynamic_token"] self._client.state_manager.set_dynamic_token(dynamic_token) + elif "customer" in response and "serviceToken" in response["customer"]: + service_token = response["customer"]["serviceToken"] + self._client.state_manager.set_dynamic_token(service_token) + print(f"DEBUG: Stored service token: {service_token[:20]}...") response_data = response.copy() response_data.pop("email_address", None) diff --git a/replicated/state.py b/replicated/state.py index a16ea8c..84dfe9b 100644 --- a/replicated/state.py +++ b/replicated/state.py @@ -92,6 +92,17 @@ def set_dynamic_token(self, token: str) -> None: state["dynamic_token"] = token self.save_state(state) + def get_customer_email(self) -> Optional[str]: + """Get the cached customer email.""" + state = self.get_state() + return state.get("customer_email") + + def set_customer_email(self, email: str) -> None: + """Set the customer email in state.""" + state = self.get_state() + state["customer_email"] = email + self.save_state(state) + def clear_state(self) -> None: """Clear all cached state.""" if self._state_file.exists(): From b9c8039df72a2e16b36af3d085c35ac57400d2a7 Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Wed, 10 Sep 2025 19:10:10 -0500 Subject: [PATCH 2/4] f --- tests/test_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 8f312a4..a402798 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,8 +23,11 @@ def test_customer_creation(self, mock_httpx): mock_response = Mock() mock_response.is_success = True mock_response.json.return_value = { - "id": "customer_123", - "email_address": "test@example.com", + "customer": { + "id": "customer_123", + "email": "test@example.com", + "name": "test user" + } } mock_client = Mock() From bdb21c6e7ba477aa9f13a35d178396bfea73ed04 Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Wed, 10 Sep 2025 19:13:23 -0500 Subject: [PATCH 3/4] f --- examples/basic_example.py | 23 +++++++++++++---------- replicated/http_client.py | 2 +- replicated/services.py | 24 ++++++++++++++++++------ 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/examples/basic_example.py b/examples/basic_example.py index 3251b52..1af0fed 100644 --- a/examples/basic_example.py +++ b/examples/basic_example.py @@ -38,13 +38,13 @@ def main(): "--customer-name", help="Customer name (optional)" ) - + args = parser.parse_args() - - print(f"Initializing Replicated client...") + + print("Initializing Replicated client...") print(f"Base URL: {args.base_url}") print(f"App Slug: {args.app_slug}") - + # Initialize the client with ReplicatedClient( publishable_key=args.publishable_key, @@ -52,27 +52,30 @@ def main(): base_url=args.base_url, ) as client: print("āœ“ Replicated client initialized successfully") - + # Create or get customer channel_info = f" (channel: {args.channel})" if args.channel else "" name_info = f" (name: {args.customer_name})" if args.customer_name else "" - print(f"\nCreating/getting customer with email: {args.customer_email}{channel_info}{name_info}") + print( + f"\nCreating/getting customer with email: " + f"{args.customer_email}{channel_info}{name_info}" + ) customer = client.customer.get_or_create( email_address=args.customer_email, channel=args.channel, name=args.customer_name ) print(f"āœ“ Customer created/retrieved - ID: {customer.customer_id}") - + # Create or get instance - print(f"\nCreating/getting instance for customer...") + print("\nCreating/getting instance for customer...") instance = customer.get_or_create_instance() print(f"āœ“ Instance created/retrieved - ID: {instance.instance_id}") - + print("\nšŸŽ‰ Basic example completed successfully!") print(f"Customer ID: {customer.customer_id}") print(f"Instance ID: {instance.instance_id}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/replicated/http_client.py b/replicated/http_client.py index ef317a7..9d2e37b 100644 --- a/replicated/http_client.py +++ b/replicated/http_client.py @@ -50,7 +50,7 @@ def _handle_response(self, response: httpx.Response) -> Dict[str, Any]: default_msg = f"HTTP {response.status_code}" error_message = json_body.get("message", default_msg) error_code = json_body.get("code") - + # Debug: print the full error response print(f"DEBUG: HTTP {response.status_code} Error Response:") print(f"DEBUG: Response body: {response.text}") diff --git a/replicated/services.py b/replicated/services.py index 0eb0c7f..ed7f94a 100644 --- a/replicated/services.py +++ b/replicated/services.py @@ -23,9 +23,12 @@ def get_or_create( # Check if customer ID is cached and email matches cached_customer_id = self._client.state_manager.get_customer_id() cached_email = self._client.state_manager.get_customer_email() - + if cached_customer_id and cached_email == email_address: - print(f"DEBUG: Using cached customer ID {cached_customer_id} for email {email_address}") + print( + f"DEBUG: Using cached customer ID {cached_customer_id} " + f"for email {email_address}" + ) return Customer( self._client, cached_customer_id, @@ -33,7 +36,10 @@ def get_or_create( channel, ) elif cached_customer_id and cached_email != email_address: - print(f"DEBUG: Email changed from {cached_email} to {email_address}, clearing cache") + print( + f"DEBUG: Email changed from {cached_email} to " + f"{email_address}, clearing cache" + ) self._client.state_manager.clear_state() # Create or fetch customer @@ -91,9 +97,12 @@ async def get_or_create( # Check if customer ID is cached and email matches cached_customer_id = self._client.state_manager.get_customer_id() cached_email = self._client.state_manager.get_customer_email() - + if cached_customer_id and cached_email == email_address: - print(f"DEBUG: Using cached customer ID {cached_customer_id} for email {email_address}") + print( + f"DEBUG: Using cached customer ID {cached_customer_id} " + f"for email {email_address}" + ) return AsyncCustomer( self._client, cached_customer_id, @@ -101,7 +110,10 @@ async def get_or_create( channel, ) elif cached_customer_id and cached_email != email_address: - print(f"DEBUG: Email changed from {cached_email} to {email_address}, clearing cache") + print( + f"DEBUG: Email changed from {cached_email} to " + f"{email_address}, clearing cache" + ) self._client.state_manager.clear_state() # Create or fetch customer From 97e12acab99e70890ccd691b77c5ffde09cfef42 Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Wed, 10 Sep 2025 19:17:31 -0500 Subject: [PATCH 4/4] f --- examples/basic_example.py | 23 ++++++++--------------- tests/test_client.py | 2 +- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/examples/basic_example.py b/examples/basic_example.py index 1af0fed..2cdec62 100644 --- a/examples/basic_example.py +++ b/examples/basic_example.py @@ -5,6 +5,7 @@ """ import argparse + from replicated import ReplicatedClient @@ -13,31 +14,23 @@ def main(): parser.add_argument( "--base-url", default="https://replicated.app", - help="Base URL for the Replicated API (default: https://replicated.app)" + help="Base URL for the Replicated API (default: https://replicated.app)", ) parser.add_argument( "--publishable-key", required=True, - help="Your Replicated publishable key (required)" + help="Your Replicated publishable key (required)", ) parser.add_argument( - "--app-slug", - required=True, - help="Your application slug (required)" + "--app-slug", required=True, help="Your application slug (required)" ) parser.add_argument( "--customer-email", default="user@example.com", - help="Customer email address (default: user@example.com)" - ) - parser.add_argument( - "--channel", - help="Channel for the customer (optional)" - ) - parser.add_argument( - "--customer-name", - help="Customer name (optional)" + help="Customer email address (default: user@example.com)", ) + parser.add_argument("--channel", help="Channel for the customer (optional)") + parser.add_argument("--customer-name", help="Customer name (optional)") args = parser.parse_args() @@ -63,7 +56,7 @@ def main(): customer = client.customer.get_or_create( email_address=args.customer_email, channel=args.channel, - name=args.customer_name + name=args.customer_name, ) print(f"āœ“ Customer created/retrieved - ID: {customer.customer_id}") diff --git a/tests/test_client.py b/tests/test_client.py index a402798..b103831 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -26,7 +26,7 @@ def test_customer_creation(self, mock_httpx): "customer": { "id": "customer_123", "email": "test@example.com", - "name": "test user" + "name": "test user", } }