Merge pull request #1 from katariina-jarvenmaki/adding-functionality

Adding functionality
This commit is contained in:
Katariina Järvenmäki
2026-03-02 16:29:18 +02:00
committed by GitHub
7 changed files with 135 additions and 35 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
app/__pycache__ app/__pycache__
venv/ venv/
test.txt
+3 -3
View File
@@ -12,11 +12,11 @@ source venv/bin/activate
```bash ```bash
uvicorn app.main:app --reload uvicorn app.main:app --reload
``` ```
Then check: Then check:<br>
http://127.0.0.1:8000 http://127.0.0.1:8000<br>
http://127.0.0.1:8000/docs http://127.0.0.1:8000/docs
Test Short URL Endpoint Test Short URL Endpoint:<br>
http://127.0.0.1:8000/shorten http://127.0.0.1:8000/shorten
## Running a test ## Running a test
View File
+72 -24
View File
@@ -1,13 +1,18 @@
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pydantic import BaseModel
from app.utils import generate_short_code from app.utils import generate_short_code
from dotenv import load_dotenv from pydantic import BaseModel, HttpUrl
import os from pydantic_settings import BaseSettings
import sqlite3 import sqlite3
import logging 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 # Set up logging configuration
logging.basicConfig(level=logging.INFO, logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s") format="%(asctime)s - %(levelname)s - %(message)s")
@@ -16,9 +21,18 @@ logger = logging.getLogger(__name__)
# Database connection setup # Database connection setup
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
conn = sqlite3.connect('urls.db') conn = sqlite3.connect(settings.database_url)
conn.execute('''CREATE TABLE IF NOT EXISTS urls conn.execute("""
(id INTEGER PRIMARY KEY, short_code TEXT, original_url TEXT, clicks INTEGER)''') 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() conn.commit()
logger.info("Database initialized or already exists.") logger.info("Database initialized or already exists.")
yield yield
@@ -28,11 +42,10 @@ async def lifespan(app: FastAPI):
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
def get_db_connection(): def get_db_connection():
conn = sqlite3.connect('urls.db') return sqlite3.connect(settings.database_url, check_same_thread=False)
return conn
class URLRequest(BaseModel): class URLRequest(BaseModel):
url: str url: HttpUrl
@app.get("/") @app.get("/")
def home(): def home():
@@ -41,36 +54,71 @@ def home():
@app.post("/shorten") @app.post("/shorten")
def shorten_url(request: URLRequest): def shorten_url(request: URLRequest):
logger.info(f"Request received to shorten URL: {request.url}")
short_code = generate_short_code()
conn = get_db_connection() 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() short_code = generate_short_code()
conn.execute("INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, ?)", conn.execute(
(short_code, request.url, 0)) "INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, 0)",
(short_code, str(request.url))
)
conn.commit() conn.commit()
conn.close() conn.close()
short_url = f"{os.getenv('BASE_URL', 'http://localhost:8000')}/{short_code}" return {"short_url": f"{settings.base_url}/{short_code}"}
logger.info(f"Shortened URL created: {short_url}")
return {"short_url": short_url}
@app.get("/{short_code}") @app.get("/{short_code}")
def redirect_to_url(short_code: str): def redirect_to_url(short_code: str):
logger.info(f"Redirect request received for short_code: {short_code}")
conn = get_db_connection() 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: 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") raise HTTPException(status_code=404, detail="Shortened URL not found")
original_url, clicks = url_data original_url = url_data[0]
conn.execute("UPDATE urls SET clicks = ? WHERE short_code = ?", (clicks + 1, short_code))
conn.execute(
"UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?",
(short_code,)
)
conn.commit() conn.commit()
conn.close() conn.close()
logger.info(f"Redirecting to {original_url}. Total clicks: {clicks + 1}") return RedirectResponse(url=original_url)
return RedirectResponse(url=original_url)
@app.get("/stats/{short_code}")
def get_url_stats(short_code: str):
logger.info(f"Stats 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()
if url_data is None:
logger.warning(f"Stats not found for {short_code}.")
conn.close()
raise HTTPException(status_code=404, detail="URL stats not found")
original_url, clicks = url_data
conn.close()
logger.info(f"Returning stats for {original_url}. Total clicks: {clicks}")
return {"original_url": original_url, "clicks": clicks}
+7
View File
@@ -0,0 +1,7 @@
# app/settings.py
import os
# Example configuration
database_url = os.getenv("DATABASE_URL", "sqlite:///default.db")
debug = os.getenv("DEBUG", "True") == "True"
+51 -7
View File
@@ -3,20 +3,64 @@ import sys
import os import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from app.main import app from app.main import app
from app import settings
import tempfile
import pytest
client = TestClient(app) @pytest.fixture
def test_client():
with tempfile.NamedTemporaryFile() as tmp:
settings.database_url = tmp.name
with TestClient(app) as c:
yield c
def test_home(): @pytest.fixture
response = client.get("/") def use_temp_db():
with tempfile.NamedTemporaryFile() as tmp:
settings.database_url = tmp.name
yield
def test_home(test_client):
response = test_client.get("/")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"message": "URL Shortener API"} assert response.json() == {"message": "URL Shortener API"}
def test_shorten_url(): def test_shorten_url(test_client):
response = client.post("/shorten", json={"url": "https://google.com"}) response = test_client.post("/shorten", json={"url": "https://google.com"})
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert "short_url" in data assert "short_url" in data
short_url = data["short_url"] short_url = data["short_url"]
assert short_url.startswith("http://localhost:8000/") assert short_url.startswith("http://localhost:8000/")
def test_redirect(test_client):
response = test_client.post("/shorten", json={"url": "https://google.com"})
short_url = response.json()["short_url"]
code = short_url.split("/")[-1]
redirect = test_client.get(f"/{code}", follow_redirects=False)
assert redirect.status_code == 307
def test_duplicate_url_returns_same_code(test_client):
r1 = test_client.post("/shorten", json={"url": "https://example.com"})
r2 = test_client.post("/shorten", json={"url": "https://example.com"})
assert r1.status_code == 200
assert r2.status_code == 200
assert r1.json()["short_url"] == r2.json()["short_url"]
def test_stats_endpoint(test_client):
response = test_client.post("/shorten", json={"url": "https://stats-test.com"})
code = response.json()["short_url"].split("/")[-1]
test_client.get(f"/{code}", follow_redirects=False)
stats = test_client.get(f"/stats/{code}")
assert stats.status_code == 200
data = stats.json()
assert data["clicks"] == 1
def test_redirect_404(test_client):
response = test_client.get("/nonexistent", follow_redirects=False)
assert response.status_code == 404
BIN
View File
Binary file not shown.