From e29a0be085dca523060b13a98297a7aa422b38e3 Mon Sep 17 00:00:00 2001 From: Kavya Trivedi Date: Thu, 16 Oct 2025 23:53:42 +0530 Subject: [PATCH 1/4] feat: Add Python URL shortener template --- python/url-shortener/README.md | 33 +++++++++++++++ python/url-shortener/requirements.txt | 2 + python/url-shortener/src/main.py | 60 +++++++++++++++++++++++++++ python/url-shortener/src/utils.py | 15 +++++++ 4 files changed, 110 insertions(+) create mode 100644 python/url-shortener/README.md create mode 100644 python/url-shortener/requirements.txt create mode 100644 python/url-shortener/src/main.py create mode 100644 python/url-shortener/src/utils.py diff --git a/python/url-shortener/README.md b/python/url-shortener/README.md new file mode 100644 index 00000000..571adf77 --- /dev/null +++ b/python/url-shortener/README.md @@ -0,0 +1,33 @@ +# URL Shortener in Python + +A Python function to create and manage short URLs. + +## 📝 Environment Variables + +- `APPWRITE_ENDPOINT`: Your Appwrite endpoint. +- `APPWRITE_API_KEY`: Your Appwrite API key. +- `APPWRITE_PROJECT`: Your Appwrite project ID. +- `DATABASE_ID`: The ID of the database to store URLs. +- `COLLECTION_ID`: The ID of the collection to store URLs. + +## 🚀 Building and Deployment + +1. **Create an Appwrite Database and Collection:** + * Create a database with a unique ID. + * Create a collection with the following attribute: + * `original_url` (string, required) + +2. **Deploy the Function:** + * Package the function: `tar -czvf code.tar.gz .` + * Upload and deploy the packaged function to your Appwrite project. + +## 🛠️ Usage + +### Create a Short URL + +Execute the function with the following JSON data to create a short URL: + +```json +{ + "url": "[https://www.google.com](https://www.google.com)" +} \ No newline at end of file diff --git a/python/url-shortener/requirements.txt b/python/url-shortener/requirements.txt new file mode 100644 index 00000000..5afba639 --- /dev/null +++ b/python/url-shortener/requirements.txt @@ -0,0 +1,2 @@ +appwrite==3.1.0 +nanoid==2.0.0 \ No newline at end of file diff --git a/python/url-shortener/src/main.py b/python/url-shortener/src/main.py new file mode 100644 index 00000000..511baf29 --- /dev/null +++ b/python/url-shortener/src/main.py @@ -0,0 +1,60 @@ +import os +import json +import nanoid +from appwrite.services.databases import Databases +from appwrite.query import Query +from . import utils + +# This is your Appwrite function +# It's executed each time we get a request +def main(context): + # Get Appwrite's database client + database = utils.get_database() + + # Database and collection IDs + database_id = os.environ.get("DATABASE_ID") + collection_id = os.environ.get("COLLECTION_ID") + + # The payload will contain 'url' to shorten or 'short_id' to redirect + payload = json.loads(context.req.body) + + # Action: Create a short URL + if 'url' in payload: + original_url = payload['url'] + short_id = nanoid.generate(size=7) + + try: + database.create_document( + database_id, + collection_id, + short_id, + {'original_url': original_url} + ) + short_url = f"{context.req.scheme}://{context.req.host}/v1/databases/{database_id}/collections/{collection_id}/documents/{short_id}" + return context.res.json({'short_url': short_url}) + except Exception as e: + return context.res.json({'error': str(e)}, status_code=500) + + # Action: Redirect to the original URL + if 'short_id' in payload: + short_id = payload['short_id'] + + try: + # Find the document with the given short_id + result = database.list_documents( + database_id, + collection_id, + [Query.equal("$id", short_id)] + ) + + if result['total'] > 0: + original_url = result['documents'][0]['original_url'] + return context.res.redirect(original_url, 301) + else: + return context.res.json({'error': 'URL not found'}, status_code=404) + + except Exception as e: + return context.res.json({'error': str(e)}, status_code=500) + + # If no valid action is found + return context.res.json({'error': 'Invalid request. Provide either a "url" or a "short_id".'}, status_code=400) \ No newline at end of file diff --git a/python/url-shortener/src/utils.py b/python/url-shortener/src/utils.py new file mode 100644 index 00000000..74cbac0a --- /dev/null +++ b/python/url-shortener/src/utils.py @@ -0,0 +1,15 @@ +import os +from appwrite.client import Client +from appwrite.services.databases import Databases + +# This is your Appwrite function +# It's executed each time we get a request +def get_database(): + # Initialize the Appwrite client + client = Client() + client.set_endpoint(os.environ["APPWRITE_ENDPOINT"]) + client.set_project(os.environ["APPWRITE_PROJECT"]) + client.set_key(os.environ["APPWRITE_API_KEY"]) + + # Initialize the database service + return Databases(client) \ No newline at end of file From 8ebb5c15bd0320e15610462dbc4c629932c82a60 Mon Sep 17 00:00:00 2001 From: Kavya Trivedi Date: Fri, 17 Oct 2025 11:30:06 +0530 Subject: [PATCH 2/4] fix: Apply suggestions from code review --- python/url-shortener/README.md | 2 +- python/url-shortener/src/main.py | 80 ++++++++++++++++++++----------- python/url-shortener/src/utils.py | 6 +++ 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/python/url-shortener/README.md b/python/url-shortener/README.md index 571adf77..bc0442dc 100644 --- a/python/url-shortener/README.md +++ b/python/url-shortener/README.md @@ -29,5 +29,5 @@ Execute the function with the following JSON data to create a short URL: ```json { - "url": "[https://www.google.com](https://www.google.com)" + "url": "https://www.google.com" } \ No newline at end of file diff --git a/python/url-shortener/src/main.py b/python/url-shortener/src/main.py index 511baf29..7d0e9516 100644 --- a/python/url-shortener/src/main.py +++ b/python/url-shortener/src/main.py @@ -3,6 +3,7 @@ import nanoid from appwrite.services.databases import Databases from appwrite.query import Query +from appwrite.exceptions import AppwriteException from . import utils # This is your Appwrite function @@ -11,50 +12,71 @@ def main(context): # Get Appwrite's database client database = utils.get_database() - # Database and collection IDs + # --- (Suggestion 2) Validate Database Environment Variables --- database_id = os.environ.get("DATABASE_ID") collection_id = os.environ.get("COLLECTION_ID") - # The payload will contain 'url' to shorten or 'short_id' to redirect - payload = json.loads(context.req.body) + if not database_id or not collection_id: + return context.res.json( + {'error': 'Missing required environment variables: DATABASE_ID or COLLECTION_ID'}, + status_code=500 + ) - # Action: Create a short URL - if 'url' in payload: - original_url = payload['url'] - short_id = nanoid.generate(size=7) - - try: - database.create_document( - database_id, - collection_id, - short_id, - {'original_url': original_url} - ) - short_url = f"{context.req.scheme}://{context.req.host}/v1/databases/{database_id}/collections/{collection_id}/documents/{short_id}" - return context.res.json({'short_url': short_url}) - except Exception as e: - return context.res.json({'error': str(e)}, status_code=500) - - # Action: Redirect to the original URL - if 'short_id' in payload: - short_id = payload['short_id'] + # --- (Suggestion 4, Part 1) Handle Redirection from URL Path --- + # Check for short_id in the path, e.g. /v1/functions/.../executions/.../6aT8bC1 + path_parts = context.req.path.split('/') + short_id_from_path = path_parts[-1] if len(path_parts) > 1 and len(path_parts[-1]) == 7 else None + if context.req.method == 'GET' and short_id_from_path: try: - # Find the document with the given short_id result = database.list_documents( database_id, collection_id, - [Query.equal("$id", short_id)] + [Query.equal("$id", short_id_from_path)] ) - if result['total'] > 0: original_url = result['documents'][0]['original_url'] return context.res.redirect(original_url, 301) else: return context.res.json({'error': 'URL not found'}, status_code=404) + # --- (Suggestion 5) Specific Exception Handling --- + except AppwriteException as e: + return context.res.json({'error': str(e)}, status_code=500) - except Exception as e: + # --- (Suggestion 1) Handle JSON Parsing Errors --- + try: + payload = json.loads(context.req.body) if context.req.body else {} + except json.JSONDecodeError: + return context.res.json({'error': 'Invalid JSON payload'}, status_code=400) + + # --- Action: Create a short URL --- + if 'url' in payload: + original_url = payload['url'] + + # --- (Suggestion 3) Add URL Validation --- + if not original_url or not isinstance(original_url, str): + return context.res.json({'error': 'Invalid URL format'}, status_code=400) + if len(original_url) > 2048: + return context.res.json({'error': 'URL too long (max 2048 characters)'}, status_code=400) + if not original_url.startswith(('http://', 'https://')): + return context.res.json({'error': 'URL must start with http:// or https://'}, status_code=400) + + short_id = nanoid.generate(size=7) + + try: + database.create_document( + database_id, + collection_id, + short_id, + {'original_url': original_url} + ) + # --- (Suggestion 4, Part 2) Construct Correct short_url --- + # It should point to the function itself + execution_path = context.req.path + short_url = f"{context.req.scheme}://{context.req.host}{execution_path}/{short_id}" + return context.res.json({'short_url': short_url}) + # --- (Suggestion 5) Specific Exception Handling --- + except AppwriteException as e: return context.res.json({'error': str(e)}, status_code=500) - # If no valid action is found - return context.res.json({'error': 'Invalid request. Provide either a "url" or a "short_id".'}, status_code=400) \ No newline at end of file + return context.res.json({'error': 'Invalid request. Provide a "url" in the JSON body to shorten.'}, status_code=400) \ No newline at end of file diff --git a/python/url-shortener/src/utils.py b/python/url-shortener/src/utils.py index 74cbac0a..1035d393 100644 --- a/python/url-shortener/src/utils.py +++ b/python/url-shortener/src/utils.py @@ -5,6 +5,12 @@ # This is your Appwrite function # It's executed each time we get a request def get_database(): + # Validate environment variables + required_vars = ["APPWRITE_ENDPOINT", "APPWRITE_PROJECT", "APPWRITE_API_KEY"] + missing_vars = [var for var in required_vars if var not in os.environ] + if missing_vars: + raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}") + # Initialize the Appwrite client client = Client() client.set_endpoint(os.environ["APPWRITE_ENDPOINT"]) From 547490fc3a00c4d89fcdfd96abfc1bd969a1ad00 Mon Sep 17 00:00:00 2001 From: Kavya Trivedi Date: Fri, 17 Oct 2025 12:01:01 +0530 Subject: [PATCH 3/4] feat: Address all review feedback from coderabbitai --- python/url-shortener/README.md | 16 +++-- python/url-shortener/src/main.py | 104 +++++++++++++++++------------- python/url-shortener/src/utils.py | 2 + 3 files changed, 73 insertions(+), 49 deletions(-) diff --git a/python/url-shortener/README.md b/python/url-shortener/README.md index bc0442dc..431fde4c 100644 --- a/python/url-shortener/README.md +++ b/python/url-shortener/README.md @@ -15,19 +15,25 @@ A Python function to create and manage short URLs. 1. **Create an Appwrite Database and Collection:** * Create a database with a unique ID. * Create a collection with the following attribute: - * `original_url` (string, required) + * `original_url` (string, required, size 2048) 2. **Deploy the Function:** * Package the function: `tar -czvf code.tar.gz .` - * Upload and deploy the packaged function to your Appwrite project. + * In the Appwrite Console, go to **Functions** and click **Create Function**. + * Select the **Python 3.9** runtime. + * Upload the `code.tar.gz` file. + * In the **Settings** tab, set the **Entrypoint** to `src/main.py`. + * Add the required environment variables. + * Activate the function. ## 🛠️ Usage ### Create a Short URL -Execute the function with the following JSON data to create a short URL: +Execute the function with a `POST` request and a JSON body: ```json { - "url": "https://www.google.com" -} \ No newline at end of file + "url": "[https://www.google.com](https://www.google.com)" +} +``` \ No newline at end of file diff --git a/python/url-shortener/src/main.py b/python/url-shortener/src/main.py index 7d0e9516..f6f72f46 100644 --- a/python/url-shortener/src/main.py +++ b/python/url-shortener/src/main.py @@ -1,10 +1,8 @@ import os import json import nanoid -from appwrite.services.databases import Databases -from appwrite.query import Query from appwrite.exceptions import AppwriteException -from . import utils +import utils # CRITICAL: Changed from 'from . import utils' # This is your Appwrite function # It's executed each time we get a request @@ -12,7 +10,7 @@ def main(context): # Get Appwrite's database client database = utils.get_database() - # --- (Suggestion 2) Validate Database Environment Variables --- + # Validate Database Environment Variables database_id = os.environ.get("DATABASE_ID") collection_id = os.environ.get("COLLECTION_ID") @@ -22,61 +20,79 @@ def main(context): status_code=500 ) - # --- (Suggestion 4, Part 1) Handle Redirection from URL Path --- - # Check for short_id in the path, e.g. /v1/functions/.../executions/.../6aT8bC1 - path_parts = context.req.path.split('/') - short_id_from_path = path_parts[-1] if len(path_parts) > 1 and len(path_parts[-1]) == 7 else None + # --- (MAJOR) Handle Redirection from URL Path or Query --- + short_id = None + if context.req.method in ('GET', 'HEAD'): + path_parts = context.req.path.split('/') + if path_parts and len(path_parts[-1]) == 7: + short_id = path_parts[-1] + + query = getattr(context.req, 'query', {}) + if not short_id and query and 'id' in query: + short_id = query['id'] - if context.req.method == 'GET' and short_id_from_path: + # --- Handle Redirection Logic --- + if short_id: try: - result = database.list_documents( - database_id, - collection_id, - [Query.equal("$id", short_id_from_path)] - ) - if result['total'] > 0: - original_url = result['documents'][0]['original_url'] - return context.res.redirect(original_url, 301) - else: - return context.res.json({'error': 'URL not found'}, status_code=404) - # --- (Suggestion 5) Specific Exception Handling --- + doc = database.get_document(database_id, collection_id, short_id) + return context.res.redirect(doc['original_url'], 301) except AppwriteException as e: + if e.code == 404: + return context.res.json({'error': 'URL not found'}, status_code=404) return context.res.json({'error': str(e)}, status_code=500) - # --- (Suggestion 1) Handle JSON Parsing Errors --- + # Handle POST requests for creation or JSON-based redirection try: payload = json.loads(context.req.body) if context.req.body else {} except json.JSONDecodeError: return context.res.json({'error': 'Invalid JSON payload'}, status_code=400) + # --- (MAJOR) Support JSON payload redirect fallback --- + if 'short_id' in payload: + short_id = payload['short_id'] + if not isinstance(short_id, str) or len(short_id) != 7: + return context.res.json({'error': 'Invalid short_id format'}, status_code=400) + try: + doc = database.get_document(database_id, collection_id, short_id) + return context.res.redirect(doc['original_url'], 301) + except AppwriteException as e: + if e.code == 404: + return context.res.json({'error': 'URL not found'}, status_code=404) + return context.res.json({'error': str(e)}, status_code=500) + # --- Action: Create a short URL --- if 'url' in payload: - original_url = payload['url'] + original_url = payload['url'].strip() if isinstance(payload.get('url'), str) else None - # --- (Suggestion 3) Add URL Validation --- - if not original_url or not isinstance(original_url, str): - return context.res.json({'error': 'Invalid URL format'}, status_code=400) + # Add stricter URL Validation + if not original_url or not original_url.startswith(('http://', 'https://')): + return context.res.json({'error': 'Invalid URL format. Must start with http:// or https://'}, status_code=400) if len(original_url) > 2048: return context.res.json({'error': 'URL too long (max 2048 characters)'}, status_code=400) - if not original_url.startswith(('http://', 'https://')): - return context.res.json({'error': 'URL must start with http:// or https://'}, status_code=400) + + # --- (NITPICK) Generate a unique short ID with retries --- + short_id = None + for _ in range(5): # Try up to 5 times + candidate_id = nanoid.generate(size=7) + try: + database.create_document( + database_id, + collection_id, + candidate_id, + {'original_url': original_url} + ) + short_id = candidate_id + break # Success + except AppwriteException as e: + if e.code == 409: # Collision, try again + continue + raise # Re-raise other errors - short_id = nanoid.generate(size=7) + if not short_id: + return context.res.json({'error': 'Failed to generate a unique short ID after multiple attempts.'}, status_code=500) - try: - database.create_document( - database_id, - collection_id, - short_id, - {'original_url': original_url} - ) - # --- (Suggestion 4, Part 2) Construct Correct short_url --- - # It should point to the function itself - execution_path = context.req.path - short_url = f"{context.req.scheme}://{context.req.host}{execution_path}/{short_id}" - return context.res.json({'short_url': short_url}) - # --- (Suggestion 5) Specific Exception Handling --- - except AppwriteException as e: - return context.res.json({'error': str(e)}, status_code=500) + execution_path = context.req.path + short_url = f"{context.req.scheme}://{context.req.host}{execution_path}/{short_id}" + return context.res.json({'short_url': short_url}) - return context.res.json({'error': 'Invalid request. Provide a "url" in the JSON body to shorten.'}, status_code=400) \ No newline at end of file + return context.res.json({'error': 'Invalid request.'}, status_code=400) \ No newline at end of file diff --git a/python/url-shortener/src/utils.py b/python/url-shortener/src/utils.py index 1035d393..a523a7a6 100644 --- a/python/url-shortener/src/utils.py +++ b/python/url-shortener/src/utils.py @@ -1,9 +1,11 @@ import os from appwrite.client import Client from appwrite.services.databases import Databases +from functools import lru_cache # This is your Appwrite function # It's executed each time we get a request +@lru_cache(maxsize=1) def get_database(): # Validate environment variables required_vars = ["APPWRITE_ENDPOINT", "APPWRITE_PROJECT", "APPWRITE_API_KEY"] From 3727d00e72e19a28947782937afbe701dc9d6054 Mon Sep 17 00:00:00 2001 From: Kavya Trivedi Date: Fri, 17 Oct 2025 22:39:44 +0530 Subject: [PATCH 4/4] fix: Handle non-collision errors during ID generation --- python/url-shortener/src/main.py | 45 ++++++++++++++------------------ 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/python/url-shortener/src/main.py b/python/url-shortener/src/main.py index f6f72f46..41e0bfc8 100644 --- a/python/url-shortener/src/main.py +++ b/python/url-shortener/src/main.py @@ -2,15 +2,13 @@ import json import nanoid from appwrite.exceptions import AppwriteException -import utils # CRITICAL: Changed from 'from . import utils' +import utils # This is your Appwrite function # It's executed each time we get a request def main(context): - # Get Appwrite's database client database = utils.get_database() - # Validate Database Environment Variables database_id = os.environ.get("DATABASE_ID") collection_id = os.environ.get("COLLECTION_ID") @@ -20,7 +18,6 @@ def main(context): status_code=500 ) - # --- (MAJOR) Handle Redirection from URL Path or Query --- short_id = None if context.req.method in ('GET', 'HEAD'): path_parts = context.req.path.split('/') @@ -31,7 +28,6 @@ def main(context): if not short_id and query and 'id' in query: short_id = query['id'] - # --- Handle Redirection Logic --- if short_id: try: doc = database.get_document(database_id, collection_id, short_id) @@ -41,13 +37,11 @@ def main(context): return context.res.json({'error': 'URL not found'}, status_code=404) return context.res.json({'error': str(e)}, status_code=500) - # Handle POST requests for creation or JSON-based redirection try: payload = json.loads(context.req.body) if context.req.body else {} except json.JSONDecodeError: return context.res.json({'error': 'Invalid JSON payload'}, status_code=400) - # --- (MAJOR) Support JSON payload redirect fallback --- if 'short_id' in payload: short_id = payload['short_id'] if not isinstance(short_id, str) or len(short_id) != 7: @@ -60,33 +54,34 @@ def main(context): return context.res.json({'error': 'URL not found'}, status_code=404) return context.res.json({'error': str(e)}, status_code=500) - # --- Action: Create a short URL --- if 'url' in payload: original_url = payload['url'].strip() if isinstance(payload.get('url'), str) else None - # Add stricter URL Validation if not original_url or not original_url.startswith(('http://', 'https://')): return context.res.json({'error': 'Invalid URL format. Must start with http:// or https://'}, status_code=400) if len(original_url) > 2048: return context.res.json({'error': 'URL too long (max 2048 characters)'}, status_code=400) - # --- (NITPICK) Generate a unique short ID with retries --- short_id = None - for _ in range(5): # Try up to 5 times - candidate_id = nanoid.generate(size=7) - try: - database.create_document( - database_id, - collection_id, - candidate_id, - {'original_url': original_url} - ) - short_id = candidate_id - break # Success - except AppwriteException as e: - if e.code == 409: # Collision, try again - continue - raise # Re-raise other errors + # --- (FINAL FIX) Wrap the loop in a try...except block --- + try: + for _ in range(5): + candidate_id = nanoid.generate(size=7) + try: + database.create_document( + database_id, + collection_id, + candidate_id, + {'original_url': original_url} + ) + short_id = candidate_id + break + except AppwriteException as e: + if e.code == 409: + continue + raise + except AppwriteException as e: + return context.res.json({'error': f'Database error: {str(e)}'}, status_code=500) if not short_id: return context.res.json({'error': 'Failed to generate a unique short ID after multiple attempts.'}, status_code=500)