33import traceback
44from datetime import datetime , timezone
55
6- from fastapi import APIRouter , FastAPI , HTTPException , Request
7- from fastapi .responses import HTMLResponse , JSONResponse , RedirectResponse
6+ from fastapi import APIRouter , FastAPI , Request
7+ from fastapi .responses import HTMLResponse , JSONResponse
88from pydantic import BaseModel , Field
9- from pymongo .errors import PyMongoError
109
10+ try :
11+ from pymongo .errors import PyMongoError
12+ except ImportError :
13+ PyMongoError = Exception
1114from app import __version__
1215from app .db import data as db_data
13- from app .utils .config import load_env
16+ from app .utils .cache import get_short_from_cache , set_cache_pair
1417from app .utils .helper import generate_code , is_valid_url , sanitize_url
1518
16- load_env () # explicit call
17-
18- # Decide which env file to load
19-
2019SHORT_CODE_PATTERN = re .compile (r"^[A-Za-z0-9]{6}$" )
21-
22-
23- DOMAIN = os .getenv ("DOMAIN" , "http://127.0.0.1" )
24- PORT = os .getenv ("PORT" , "8000" )
25-
26- print (f"Starting Tiny API on { DOMAIN } :{ PORT } " )
27-
2820MAX_URL_LENGTH = 2048
2921
30- # -------------------------------------------------
31- # App
32- # -------------------------------------------------
3322app = FastAPI (
3423 title = "Tiny API" ,
3524 version = __version__ ,
3625 description = "Tiny URL Shortener API built with FastAPI" ,
3726)
3827
28+ api_v1 = APIRouter (prefix = os .getenv ("API_VERSION" , "/api/v1" ), tags = ["v1" ])
29+
3930
40- # -------------------------------------------------
41- # Global error handler
42- # -------------------------------------------------
4331@app .exception_handler (Exception )
4432async def global_exception_handler (request : Request , exc : Exception ):
4533 traceback .print_exc ()
4634 return JSONResponse (
4735 status_code = 500 ,
48- content = {
49- "success" : False ,
50- "error" : "INTERNAL_SERVER_ERROR" ,
51- "message" : "Something went wrong on the server" ,
52- },
36+ content = {"success" : False , "error" : "INTERNAL_SERVER_ERROR" },
5337 )
5438
5539
56- # -------------------------------------------------
57- # Router
58- # -------------------------------------------------
59- api_v1 = APIRouter (prefix = os .getenv ("API_VERSION" , "/api/v1" ), tags = ["v1" ])
60-
61-
62- # -------------------------------------------------
63- # Models
64- # -------------------------------------------------
6540class ShortenRequest (BaseModel ):
6641 url : str = Field (..., examples = ["https://abcdkbd.com" ])
6742
@@ -148,39 +123,16 @@ async def read_root(_: Request):
148123 """
149124
150125
151- @api_v1 .post (
152- "/shorten" ,
153- response_model = ShortenResponse ,
154- responses = {400 : {"model" : ErrorResponse }, 413 : {"model" : ErrorResponse }},
155- status_code = 201 ,
156- )
126+ @api_v1 .post ("/shorten" , response_model = ShortenResponse , status_code = 201 )
157127def shorten_url (payload : ShortenRequest ):
158128 print (" SHORTEN ENDPOINT HIT " , payload .url )
159129 raw_url = payload .url .strip ()
160130
161131 if len (raw_url ) > MAX_URL_LENGTH :
162132 return JSONResponse (
163- status_code = 413 ,
164- content = {
165- "success" : False ,
166- "input_url" : payload .url ,
167- "message" : "URL length exceeds maximum limit" ,
168- },
133+ status_code = 413 , content = {"success" : False , "input_url" : payload .url }
169134 )
170135
171- # 2️⃣ Protocol presence check (http / https only)
172- if not raw_url .startswith (("http" , "https" )):
173- return JSONResponse (
174- status_code = 400 ,
175- content = {
176- "success" : False ,
177- "error" : "INVALID_PROTOCOL" ,
178- "input_url" : payload .url ,
179- "message" : "URL must start with http:// or https://" ,
180- },
181- )
182-
183- # 3️⃣ Sanitize AFTER protocol presence
184136 original_url = sanitize_url (raw_url )
185137
186138 if not is_valid_url (original_url ):
@@ -190,43 +142,25 @@ def shorten_url(payload: ShortenRequest):
190142 "success" : False ,
191143 "error" : "INVALID_URL" ,
192144 "input_url" : payload .url ,
193- "message" : "URL format is invalid " ,
145+ "message" : "Invalid URL " ,
194146 },
195147 )
196148
197- # 🔁 Try reconnect if DB dropped
198- try :
199- if db_data .urls is None or db_data .url_stats is None :
200- db_data .connect_db ()
201- except Exception :
202- pass
203-
204- # 🔌 Offline fallback if DB still unavailable
205- if db_data .urls is None or db_data .url_stats is None :
206- short_code = generate_code ()
207- created_at = datetime .now (timezone .utc )
208- print ("⚠️ DB disconnected at runtime. API offline mode." )
149+ if db_data .urls is None :
150+ cached_short = get_short_from_cache (original_url )
151+ short_code = cached_short or generate_code ()
152+ set_cache_pair (short_code , original_url )
209153 return {
210154 "success" : True ,
211155 "input_url" : original_url ,
212156 "short_code" : short_code ,
213- "created_on" : created_at ,
157+ "created_on" : datetime . now ( timezone . utc ) ,
214158 }
215159
216160 try :
217- existing = db_data .urls .find_one (
218- {"original_url" : original_url }, sort = [("created_at" , 1 )]
219- )
161+ existing = db_data .urls .find_one ({"original_url" : original_url })
220162 except PyMongoError :
221- short_code = generate_code ()
222- created_at = datetime .now (timezone .utc )
223- print ("⚠️ DB error during request. API offline mode." )
224- return {
225- "success" : True ,
226- "input_url" : original_url ,
227- "short_code" : short_code ,
228- "created_on" : created_at ,
229- }
163+ existing = None
230164
231165 if existing :
232166 return {
@@ -236,95 +170,34 @@ def shorten_url(payload: ShortenRequest):
236170 "created_on" : existing ["created_at" ],
237171 }
238172
239- # 6️⃣ Create new short code
240173 short_code = generate_code ()
241- while True :
242- try :
243- if not db_data .urls .find_one ({"short_code" : short_code }):
244- break
245- short_code = generate_code ()
246- except PyMongoError :
247- break
248-
249- created_at = datetime .now (timezone .utc )
250-
251174 try :
252175 db_data .urls .insert_one (
253176 {
254177 "short_code" : short_code ,
255178 "original_url" : original_url ,
256- "created_at" : created_at ,
179+ "created_at" : datetime . now ( timezone . utc ) ,
257180 }
258181 )
259- db_data .url_stats .insert_one ({"short_code" : short_code , "visit_count" : 0 })
260182 except PyMongoError :
261- print ( "⚠️ DB disconnected during insert. API offline mode." )
183+ pass
262184
263185 return {
264186 "success" : True ,
265187 "input_url" : original_url ,
266188 "short_code" : short_code ,
267- "created_on" : created_at ,
189+ "created_on" : datetime . now ( timezone . utc ) ,
268190 }
269191
270192
271- # API v1 – Version
272-
273-
274- @app .get ("/version" , response_model = VersionResponse )
193+ @app .get ("/version" )
275194def api_version ():
276- return VersionResponse (version = __version__ )
277-
278-
279- # ----------------------------------------
280- # Register API router FIRST
281- # ----------------------------------------
195+ return {"version" : __version__ }
282196
283197
284- @app .get ("/{short_code}" , tags = ["Redirect" ])
285- def redirect_to_original (short_code : str ):
286- if not SHORT_CODE_PATTERN .match (short_code ):
287- raise HTTPException (status_code = 404 )
288-
289- try :
290- if db_data .urls is None or db_data .url_stats is None :
291- db_data .connect_db ()
292- except Exception :
293- pass
294-
295- if db_data .urls is None :
296- raise HTTPException (
297- status_code = 404 , detail = "Short code not available (offline mode)"
298- )
299-
300- try :
301- record = db_data .urls .find_one ({"short_code" : short_code })
302- except PyMongoError :
303- raise HTTPException (status_code = 503 , detail = "Database disconnected" )
304-
305- if not record :
306- raise HTTPException (status_code = 404 , detail = "Short code not found" )
307-
308- try :
309- db_data .url_stats .update_one (
310- {"short_code" : short_code }, {"$inc" : {"visit_count" : 1 }}, upsert = True
311- )
312- except PyMongoError :
313- pass
314-
315- return RedirectResponse (url = record ["original_url" ])
316-
317-
318- @api_v1 .get ("/help" , tags = ["Help" ])
198+ @api_v1 .get ("/help" )
319199def get_help ():
320- return JSONResponse (
321- status_code = 200 ,
322- content = {
323- "message" : "Welcome to the Tiny URL Shortener API! Visit /docs for API documentation."
324- },
325- )
200+ return {"message" : "Welcome to Tiny API. Visit /docs for API documentation." }
326201
327202
328- # Register router
329- # -------------------------------------------------
330203app .include_router (api_v1 )
0 commit comments