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"}
)