Coverage for backend / app / core / schemas.py: 100%

139 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-17 21:34 +0000

1"""Schemas for the JAM database 

2Create schemas should be used to create entries in the database. 

3Out schemas should be used to return data to the user. 

4Min schemas should be used to return minimal data to the user (enough to display the entry as a badge) and should not 

5contain reference to other tables. 

6Update schemas should be used to update existing entries in the database.""" 

7 

8import datetime as dt 

9from typing import Literal 

10 

11from pydantic import BaseModel, field_validator 

12 

13ThemeMode = Literal["dark", "light", "system"] 

14 

15from app.base_schemas import Out, OwnedOut, EmailField 

16 

17 

18# ------------------------------------------------------- SETTINGS ------------------------------------------------------ 

19 

20 

21class SettingCreate(BaseModel): 

22 """Setting create schema""" 

23 

24 name: str 

25 value: str 

26 description: str | None = None 

27 is_active: bool = True 

28 

29 

30class SettingOut(SettingCreate, Out): 

31 """Setting output schema""" 

32 

33 pass 

34 

35 

36class SettingUpdate(SettingCreate): 

37 """Keyword update schema""" 

38 

39 name: str | None = None 

40 value: str | None = None 

41 

42 

43# ------------------------------------------------------ REGISTER ------------------------------------------------------ 

44 

45 

46class UserRegister(BaseModel): 

47 """User create schema""" 

48 

49 email: EmailField 

50 password: str 

51 first_name: str 

52 last_name: str 

53 

54 

55# -------------------------------------------------------- LOGIN ------------------------------------------------------- 

56 

57 

58class UserLogin(BaseModel): 

59 """User login schema""" 

60 

61 email: EmailField 

62 password: str 

63 

64 

65class Token(BaseModel): 

66 access_token: str 

67 token_type: str 

68 

69 

70class TokenData(BaseModel): 

71 id: str | None = None 

72 token_version: int = 0 

73 is_demo: bool = False 

74 

75 

76# ------------------------------------------------- USER PREFERENCES --------------------------------------------------- 

77 

78 

79class UserPreferencesCreate(BaseModel): 

80 """User preferences create schema 

81 Defaults are handled in the database layer.""" 

82 

83 theme: str | None = None 

84 dark_mode: ThemeMode = "system" 

85 chase_threshold: int | None = None 

86 deadline_threshold: int | None = None 

87 update_limit: int | None = None 

88 default_currency: str | None = None 

89 extension_banner_dismissed: bool | None = None 

90 

91 

92class UserPreferencesUpdate(UserPreferencesCreate): 

93 """User preferences update schema""" 

94 

95 pass 

96 

97 

98class UserPreferencesOut(Out, UserPreferencesUpdate): 

99 """User preferences output schema""" 

100 

101 pass 

102 

103 

104# --------------------------------------------------- PREMIUM DETAILS -------------------------------------------------- 

105 

106 

107class PremiumDetailsCreate(BaseModel): 

108 """Premium details create schema""" 

109 

110 is_active: bool = False 

111 job_scraping_active: bool = True 

112 job_rating_active: bool = True 

113 

114 

115class PremiumDetailsOut(PremiumDetailsCreate, Out): 

116 """Premium details output schema""" 

117 

118 pass 

119 

120 

121class PremiumDetailsUpdate(PremiumDetailsCreate): 

122 """Premium details update schema""" 

123 

124 pass 

125 

126 

127class CurrentUserPremiumDetailsUpdate(BaseModel): 

128 """Premium details update schema""" 

129 

130 job_scraping_active: bool | None = None 

131 job_rating_active: bool | None = None 

132 

133 

134# ------------------------------------------------------- STRIPE ------------------------------------------------------- 

135 

136 

137class StripeDetails(BaseModel): 

138 """Stripe details schema""" 

139 

140 subscription_status: str | None = None 

141 trial_end_date: int | None = None 

142 

143 

144# -------------------------------------------------------- USERS ------------------------------------------------------- 

145 

146 

147class UserCreate(BaseModel): 

148 """User create schema for the admin endpoint""" 

149 

150 email: EmailField 

151 password: str 

152 is_active: bool = True 

153 is_admin: bool = False 

154 is_demo: bool = False 

155 first_name: str | None = None 

156 last_name: str | None = None 

157 premium: PremiumDetailsCreate | None = None 

158 preferences: UserPreferencesCreate | None = None 

159 

160 

161class UserOut(Out): 

162 """User output schema for the admin endpoint""" 

163 

164 email: EmailField 

165 is_active: bool 

166 is_admin: bool 

167 is_demo: bool 

168 is_verified: bool 

169 last_login: dt.datetime | None 

170 previous_login: dt.datetime | None 

171 app_version: str | None 

172 first_name: str | None = None 

173 last_name: str | None = None 

174 name: str | None = None 

175 token_version: int 

176 pending_email_change: str | None 

177 preferences: UserPreferencesOut | None 

178 premium: PremiumDetailsOut | None 

179 stripe_details: StripeDetails | None 

180 

181 

182class UserUpdate(BaseModel): 

183 """User account update schema for the admin endpoint""" 

184 

185 email: EmailField | None = None 

186 password: str | None = None 

187 is_active: bool = True 

188 is_admin: bool = False 

189 is_demo: bool = False 

190 first_name: str | None = None 

191 last_name: str | None = None 

192 preferences: UserPreferencesUpdate | None = None 

193 premium: PremiumDetailsUpdate | None = None 

194 

195 

196class CurrentUserUpdate(BaseModel): 

197 """User account update schema""" 

198 

199 email: EmailField | None = None 

200 current_password: str | None = None 

201 password: str | None = None 

202 first_name: str | None = None 

203 last_name: str | None = None 

204 app_version: str | None = None 

205 preferences: UserPreferencesUpdate | None = None 

206 premium: CurrentUserPremiumDetailsUpdate | None = None 

207 

208 

209class CurrentUserUpdateResponse(BaseModel): 

210 success: bool 

211 message: str 

212 logged_out: bool | None = None 

213 

214 

215# ------------------------------------------------- USER QUALIFICATIONS ------------------------------------------------ 

216 

217 

218class UserQualificationUpsert(BaseModel): 

219 """User qualification create schema""" 

220 

221 id: int | None = None 

222 experience: str | None = None 

223 skills: str | None = None 

224 education: str | None = None 

225 qualities: str | None = None 

226 interests: str | None = None 

227 

228 @field_validator("experience") 

229 @classmethod 

230 def validate_experience(cls, v: str | None) -> str | None: 

231 EXPERIENCE_CHAR_LIMIT: int = 10000 

232 if v and len(v) > EXPERIENCE_CHAR_LIMIT: 

233 raise ValueError(f"Experience must not exceed {EXPERIENCE_CHAR_LIMIT} characters") 

234 return v 

235 

236 @field_validator("skills", "education", "qualities", "interests") 

237 @classmethod 

238 def validate_other_fields(cls, v: str | None) -> str | None: 

239 OTHER_CHAR_LIMIT: int = 3500 

240 if v and len(v) > OTHER_CHAR_LIMIT: 

241 raise ValueError(f"This field must not exceed {OTHER_CHAR_LIMIT} characters") 

242 return v 

243 

244 

245class UserQualificationOut(UserQualificationUpsert, OwnedOut): 

246 """User qualification output schema""" 

247 

248 pass 

249 

250 

251# --------------------------------------------------- PASSWORD RESET --------------------------------------------------- 

252 

253 

254class PasswordResetRequest(BaseModel): 

255 """Email request schema for password reset""" 

256 

257 email: EmailField 

258 

259 

260class PasswordReset(BaseModel): 

261 """Password reset schema""" 

262 

263 token: str 

264 new_password: str 

265 

266 

267# ---------------------------------------------------- EMAIL CHANGE ---------------------------------------------------- 

268 

269 

270class CheckPendingEmailResponse(BaseModel): 

271 """Response for checking pending email""" 

272 

273 has_pending_email: bool 

274 pending_email: str | None = None 

275 

276 

277# -------------------------------------------------- ACCOUNT DELETION -------------------------------------------------- 

278 

279 

280class AccountDeleteRequest(BaseModel): 

281 """Account deletion request schema""" 

282 

283 password: str