diff --git a/python/url-shortener/README.md b/python/url-shortener/README.md new file mode 100644 index 00000000..431fde4c --- /dev/null +++ b/python/url-shortener/README.md @@ -0,0 +1,39 @@ +# 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, size 2048) + +2. **Deploy the Function:** + * Package the function: `tar -czvf code.tar.gz .` + * 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 a `POST` request and a JSON body: + +```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..41e0bfc8 --- /dev/null +++ b/python/url-shortener/src/main.py @@ -0,0 +1,93 @@ +import os +import json +import nanoid +from appwrite.exceptions import AppwriteException +import utils + +# This is your Appwrite function +# It's executed each time we get a request +def main(context): + database = utils.get_database() + + database_id = os.environ.get("DATABASE_ID") + collection_id = os.environ.get("COLLECTION_ID") + + 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 + ) + + 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 short_id: + 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) + + 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) + + 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) + + if 'url' in payload: + original_url = payload['url'].strip() if isinstance(payload.get('url'), str) else None + + 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) + + short_id = None + # --- (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) + + 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.'}, 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..a523a7a6 --- /dev/null +++ b/python/url-shortener/src/utils.py @@ -0,0 +1,23 @@ +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"] + 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"]) + 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