From d3778d1171f9ebc14b3a9afd5a1048432fc7d1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katariina=20J=C3=A4rvenm=C3=A4ki?= Date: Mon, 2 Mar 2026 09:35:34 +0200 Subject: [PATCH] Add URL shortening functionality with FastAPI --- .gitignore | 3 ++- README.md | 6 ++--- app/main.py | 74 +++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 8750bee..9aaeae7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ app/__pycache__ -venv/ \ No newline at end of file +venv/ +test.txt \ No newline at end of file diff --git a/README.md b/README.md index 2bde86f..dfe6b60 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ source venv/bin/activate ```bash uvicorn app.main:app --reload ``` -Then check: -http://127.0.0.1:8000 +Then check:
+http://127.0.0.1:8000
http://127.0.0.1:8000/docs -Test Short URL Endpoint +Test Short URL Endpoint:
http://127.0.0.1:8000/shorten ## Running a test diff --git a/app/main.py b/app/main.py index 079aec0..f7a96db 100644 --- a/app/main.py +++ b/app/main.py @@ -1,13 +1,18 @@ from fastapi import FastAPI, HTTPException from fastapi.responses import RedirectResponse from contextlib import asynccontextmanager -from pydantic import BaseModel from app.utils import generate_short_code -from dotenv import load_dotenv -import os +from pydantic import BaseModel, HttpUrl +from pydantic_settings import BaseSettings import sqlite3 import logging +class AppSettings(BaseSettings): + base_url: str = "http://localhost:8000" + database_url: str = "urls.db" + +settings = AppSettings() # Load environment variables + # Set up logging configuration logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") @@ -17,8 +22,18 @@ logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): conn = sqlite3.connect('urls.db') - conn.execute('''CREATE TABLE IF NOT EXISTS urls - (id INTEGER PRIMARY KEY, short_code TEXT, original_url TEXT, clicks INTEGER)''') + conn = sqlite3.connect(settings.database_url) + conn.execute(""" + CREATE TABLE IF NOT EXISTS urls ( + id INTEGER PRIMARY KEY, + short_code TEXT UNIQUE, + original_url TEXT NOT NULL, + clicks INTEGER DEFAULT 0 + ); + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_short_code ON urls(short_code); + """) conn.commit() logger.info("Database initialized or already exists.") yield @@ -28,11 +43,10 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) def get_db_connection(): - conn = sqlite3.connect('urls.db') - return conn + return sqlite3.connect(settings.database_url, check_same_thread=False) class URLRequest(BaseModel): - url: str + url: HttpUrl @app.get("/") def home(): @@ -41,36 +55,52 @@ def home(): @app.post("/shorten") def shorten_url(request: URLRequest): - logger.info(f"Request received to shorten URL: {request.url}") - short_code = generate_short_code() conn = get_db_connection() - while conn.execute("SELECT 1 FROM urls WHERE short_code = ?", (short_code,)).fetchone(): + existing = conn.execute( + "SELECT short_code FROM urls WHERE original_url = ?", + (str(request.url),) + ).fetchone() + + if existing: + conn.close() + return {"short_url": f"{settings.base_url}/{existing[0]}"} + + short_code = generate_short_code() + while conn.execute( + "SELECT 1 FROM urls WHERE short_code = ?", + (short_code,) + ).fetchone(): short_code = generate_short_code() - conn.execute("INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, ?)", - (short_code, request.url, 0)) + conn.execute( + "INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, 0)", + (short_code, str(request.url)) + ) conn.commit() conn.close() - short_url = f"{os.getenv('BASE_URL', 'http://localhost:8000')}/{short_code}" - logger.info(f"Shortened URL created: {short_url}") - return {"short_url": short_url} + return {"short_url": f"{settings.base_url}/{short_code}"} @app.get("/{short_code}") def redirect_to_url(short_code: str): - logger.info(f"Redirect request received for short_code: {short_code}") conn = get_db_connection() - url_data = conn.execute("SELECT original_url, clicks FROM urls WHERE short_code = ?", (short_code,)).fetchone() + url_data = conn.execute( + "SELECT original_url FROM urls WHERE short_code = ?", + (short_code,) + ).fetchone() if url_data is None: - logger.warning(f"Shortened URL for {short_code} not found.") + conn.close() raise HTTPException(status_code=404, detail="Shortened URL not found") - original_url, clicks = url_data - conn.execute("UPDATE urls SET clicks = ? WHERE short_code = ?", (clicks + 1, short_code)) + original_url = url_data[0] + + conn.execute( + "UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?", + (short_code,) + ) conn.commit() conn.close() - logger.info(f"Redirecting to {original_url}. Total clicks: {clicks + 1}") return RedirectResponse(url=original_url) \ No newline at end of file