Coverage for app / schemas / link_schemas.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-22 00:48 +0300

1from datetime import UTC, datetime, timedelta 

2from typing import Annotated 

3 

4from pydantic import BaseModel, Field, HttpUrl, PlainSerializer, field_validator 

5 

6from app.core.config import settings 

7 

8short_code_pattern = r"^[0-9a-z_-]{5,30}$" 

9 

10CustomAlias = Annotated[ 

11 str | None, 

12 Field( 

13 default=None, 

14 pattern=short_code_pattern, 

15 description="Custom alias to use instead of a random short code", 

16 ), 

17] 

18 

19DatetimeUTC = Annotated[ 

20 datetime, 

21 PlainSerializer( 

22 lambda d: d.astimezone(tz=UTC).isoformat().replace("+00:00", "Z"), 

23 when_used="json", 

24 ), 

25] 

26 

27 

28class LinkShortenRequest(BaseModel): 

29 long_url: HttpUrl 

30 custom_alias: CustomAlias 

31 auto_prolong: Annotated[ 

32 bool, 

33 Field( 

34 default=False, 

35 description=( 

36 "Extends access to the short link if set to True. " 

37 "If there are less than 15 days left until the link's expiration date, " 

38 "the first redirection to the long URL updates the expiration date " 

39 f"by adding {settings.link_expire_days} day(s) to the redirection date." 

40 ), 

41 ), 

42 ] 

43 expires_at: Annotated[ 

44 datetime | None, 

45 Field( 

46 default=None, 

47 examples=["2026-03-21T10:00Z"], 

48 description=( 

49 "Link expiration date in format 'YYYY-MM-DDTHH:MMZ'. " 

50 f"Сannot exceed {settings.link_expire_days} days from current UTC date." 

51 ), 

52 ), 

53 ] 

54 

55 @field_validator("expires_at") 

56 @classmethod 

57 def validate_link_expiration_date(csl, v: datetime | None): 

58 if v is None: 

59 return None 

60 

61 # обработаем возможный некорректный ввод даты и времени 

62 

63 # если дата без Z на конце, считаем, что это UTC 

64 if v.tzinfo is None: 

65 v = v.replace(tzinfo=UTC) 

66 # если дата содержит часовой пояс, то приводим к UTC 

67 else: 

68 v = v.astimezone(tz=UTC) 

69 

70 now = datetime.now(tz=UTC) # текущее всемирное координированное время 

71 

72 if v < now: 

73 raise ValueError("Expiration date cannot be less than current UTC date") 

74 

75 if v - now > timedelta(days=settings.link_expire_days): 

76 raise ValueError( 

77 f"Expiration date cannot exceed {settings.link_expire_days} days from current UTC date" 

78 ) 

79 

80 return v.replace(tzinfo=None) 

81 

82 

83class LinkShortenResponse(BaseModel): 

84 long_url: HttpUrl 

85 short_url: HttpUrl 

86 msg: str | None = None 

87 

88 

89class LinkUpdateRequest(BaseModel): 

90 custom_alias: CustomAlias 

91 

92 

93class LinkUpdateResponse(BaseModel): 

94 long_url: HttpUrl 

95 old_short_url: HttpUrl 

96 new_short_url: HttpUrl 

97 msg: str | None = None 

98 

99 

100class LinkSearchResponse(BaseModel): 

101 short_urls: list[HttpUrl] 

102 

103 

104class LinkStatsResponse(BaseModel): 

105 long_url: HttpUrl 

106 created_at: DatetimeUTC 

107 visits_counter: int 

108 last_visited_at: DatetimeUTC | None = None