from typing import Annotated from anydi.ext.fastapi import Inject from fastapi import APIRouter, BackgroundTasks, Depends, Query from fastapi.responses import JSONResponse, RedirectResponse, Response from url_shortener.api import schemas from url_shortener.api.dependencies import get_request_info from url_shortener.config import Settings from url_shortener.dtos import RequestInfo, ShortUrl, ShortUrls from url_shortener.exceptions import ( DynamodbBatchWriteError, NotAllowedDomainError, PathAlreadyInUseError, PathAndPathPrefixProvidedError, PathDoesNotExistsError, ) from url_shortener.repositories import PathsRepository from url_shortener.services import ( DeletePathsService, ShortenUrlService, ShortUrlRedirectService, UpdatePathService, ) router = APIRouter(tags=["Main"]) @router.get( "/check-path", response_model=schemas.CheckPathResponse, operation_id="checkPath", ) async def check_path( params: Annotated[schemas.CheckPathParams, Query()], repository: Annotated[PathsRepository, Inject()], ) -> dict[str, bool]: is_available = await repository.is_path_available(params.path) return {"is_available": is_available} @router.get("/redirect/{path}", operation_id="shortUrlRedirect") async def short_url_redirect( path: str, request_info: Annotated[RequestInfo, Depends(get_request_info)], service: Annotated[ShortUrlRedirectService, Inject()], background_tasks: BackgroundTasks, ) -> Response: result = await service.get_redirect_url(path) if not result: return Response("

Not Found

", status_code=404) background_tasks.add_task( service.collect_analytics, path=path, redirect_url=result["url"], is_personalized=result["is_personalized"], created_at=result["created_at"], additional_attributes=result["additional_attributes"], **request_info.model_dump(), ) return RedirectResponse(result["url"], status_code=301) @router.post( "/shorten", response_model=schemas.ShortenResponse, operation_id="shortenUrl", ) async def shorten_url( body: schemas.ShortenRequestBody, service: Annotated[ShortenUrlService, Inject()], ) -> ShortUrl | Response: try: short_url = await service.get_short_url( body.url, body.domain, path_length=body.path_length, path=body.path, path_prefix=body.path_prefix, additional_attributes=body.additional_attributes, ) except PathAlreadyInUseError: return JSONResponse(status_code=400, content={"error": "Path already in use"}) except NotAllowedDomainError: return JSONResponse( status_code=400, content={"error": "This domain is not allowed"} ) except PathAndPathPrefixProvidedError: return JSONResponse( status_code=400, content={"error": "Please provide only one of two (path, path_prefix)"}, ) return short_url @router.post( "/bulk-shorten", response_model=schemas.BulkShortenResponse, operation_id="bulkShortenUrl", ) async def bulk_shorten_url( body: schemas.BulkShortenRequestBody, service: Annotated[ShortenUrlService, Inject()], ) -> ShortUrls | Response: try: short_urls = await service.get_short_urls( body.url, body.domain, path_length=body.path_length, path_prefix=body.path_prefix, urls_count=body.urls_count, additional_attributes=body.additional_attributes, ) except NotAllowedDomainError: return JSONResponse( status_code=400, content={"error": "This domain is not allowed"} ) except DynamodbBatchWriteError: return JSONResponse( status_code=500, content={"error": "Short urls batch save error"} ) return short_urls @router.get("/allowed-domains", response_model=list[str], operation_id="allowedDomains") async def allowed_domains(settings: Annotated[Settings, Inject()]): return settings.allowed_domains @router.post( "/delete-paths", operation_id="deletePaths", ) async def delete_paths( body: schemas.DeletePathsRequestBody, service: Annotated[DeletePathsService, Inject()], ) -> Response: await service.delete_paths(body.paths) return Response(status_code=200) @router.patch( "/paths/{path}", operation_id="updatePath", ) async def update_path( path: str, body: schemas.UpdatePathRequestBody, service: Annotated[UpdatePathService, Inject()], ) -> Response: try: await service.update_path(path, body.model_dump(exclude_unset=True)) return Response(status_code=200) except PathDoesNotExistsError: return JSONResponse(status_code=404, content={"error": "Path does not exists"}) except NotAllowedDomainError: return JSONResponse( status_code=400, content={"error": "This domain is not allowed"} )