-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidators.py
More file actions
279 lines (227 loc) · 8.58 KB
/
Copy pathvalidators.py
File metadata and controls
279 lines (227 loc) · 8.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""
Request validation models and input sanitization utilities for the F1 Points Calculator API.
"""
from pydantic import BaseModel, Field, field_validator, model_validator
from typing import List, Optional, Literal
import re
# ============================================================================
# Constants
# ============================================================================
MIN_SEASON_YEAR = 1950 # First F1 season
MAX_SEASON_YEAR = 2030 # Reasonable upper limit
MAX_CUSTOM_POINTS_LENGTH = 30 # Max positions to award points
MAX_DRIVER_IDS = 50 # Max drivers in a selection
# ============================================================================
# Utility Functions
# ============================================================================
def sanitize_string(value: str, max_length: int = 200) -> str:
"""Sanitize string input by removing potentially dangerous characters."""
if not value:
return value
# Remove any HTML/script tags
value = re.sub(r'<[^>]*>', '', value)
# Remove null bytes
value = value.replace('\x00', '')
# Truncate to max length
return value[:max_length].strip()
def validate_positive_int(value: int, field_name: str, min_val: int = 1, max_val: int = 999999) -> int:
"""Validate that an integer is within expected bounds."""
if value < min_val or value > max_val:
raise ValueError(f"{field_name} must be between {min_val} and {max_val}")
return value
# ============================================================================
# Request Models
# ============================================================================
class StandingsRequest(BaseModel):
"""Request model for calculating standings."""
season_year: int = Field(
...,
ge=MIN_SEASON_YEAR,
le=MAX_SEASON_YEAR,
description="The F1 season year to calculate standings for"
)
points_system: Optional[List[int]] = Field(
default=None,
max_length=MAX_CUSTOM_POINTS_LENGTH,
description="Custom points system array (e.g., [25, 18, 15, 12, 10, 8, 6, 4, 2, 1])"
)
selected_driver_ids: Optional[List[int]] = Field(
default=None,
max_length=MAX_DRIVER_IDS,
description="Optional list of driver IDs to filter results"
)
@field_validator('points_system')
@classmethod
def validate_points_system(cls, v: Optional[List[int]]) -> Optional[List[int]]:
if v is None:
return v
if len(v) == 0:
raise ValueError("Points system cannot be empty if provided")
for i, pts in enumerate(v):
if pts < 0:
raise ValueError(f"Points at position {i+1} cannot be negative")
if pts > 1000:
raise ValueError(f"Points at position {i+1} exceeds maximum of 1000")
return v
@field_validator('selected_driver_ids')
@classmethod
def validate_driver_ids(cls, v: Optional[List[int]]) -> Optional[List[int]]:
if v is None:
return v
for driver_id in v:
if driver_id < 1:
raise ValueError("Driver IDs must be positive integers")
return list(set(v)) # Remove duplicates
class SimulateSeasonRequest(BaseModel):
"""Request model for season simulation with AI."""
season_year: int = Field(
...,
ge=MIN_SEASON_YEAR,
le=MAX_SEASON_YEAR,
description="The F1 season year to simulate"
)
points_system: Optional[List[int]] = Field(
default=None,
max_length=MAX_CUSTOM_POINTS_LENGTH,
description="Custom points system array"
)
class RaceResultsRequest(BaseModel):
"""Request model for fetching race results."""
season_year: int = Field(
...,
ge=MIN_SEASON_YEAR,
le=MAX_SEASON_YEAR,
description="The F1 season year"
)
race_number: Optional[int] = Field(
default=None,
ge=1,
le=30,
description="The race number/round within the season"
)
race_id: Optional[int] = Field(
default=None,
ge=1,
description="Internal database race ID"
)
@model_validator(mode='after')
def check_at_least_one_id(self):
if self.race_number is None and self.race_id is None:
raise ValueError("Either race_number or race_id must be provided")
return self
class HeadToHeadRequest(BaseModel):
"""Request model for head-to-head comparison."""
driver1_id: int = Field(..., ge=1, description="First driver ID")
driver2_id: int = Field(..., ge=1, description="Second driver ID")
season: Optional[int] = Field(
default=None,
ge=MIN_SEASON_YEAR,
le=MAX_SEASON_YEAR,
description="Season year for comparison (None for career comparison)"
)
mode: Literal['season', 'career'] = Field(
default='season',
description="Comparison mode: 'season' or 'career'"
)
@model_validator(mode='after')
def validate_different_drivers(self):
if self.driver1_id == self.driver2_id:
raise ValueError("Cannot compare a driver with themselves")
return self
class DriverQueryParams(BaseModel):
"""Query parameters for driver endpoints."""
season: Optional[int] = Field(
default=None,
ge=MIN_SEASON_YEAR,
le=MAX_SEASON_YEAR,
description="Filter drivers by season"
)
class RaceQueryParams(BaseModel):
"""Query parameters for race endpoints."""
season: int = Field(
...,
ge=MIN_SEASON_YEAR,
le=MAX_SEASON_YEAR,
description="Season year to get races for"
)
# ============================================================================
# Response Models
# ============================================================================
class HealthResponse(BaseModel):
"""Response model for health check endpoint."""
status: Literal['healthy', 'degraded', 'unhealthy']
version: str
database: Literal['connected', 'disconnected']
cache: Literal['connected', 'disconnected', 'disabled']
timestamp: str
class ErrorResponse(BaseModel):
"""Standard error response model."""
error: str
detail: Optional[str] = None
status_code: int
path: Optional[str] = None
timestamp: str
class SeasonResponse(BaseModel):
"""Response model for seasons list."""
seasons: List[int]
class DriverInfo(BaseModel):
"""Driver information model."""
driverId: int
forename: str
surname: str
class DriversResponse(BaseModel):
"""Response model for drivers list."""
drivers: List[DriverInfo]
class RaceInfo(BaseModel):
"""Race information model."""
raceId: int
name: str
round: Optional[int] = None
class RacesResponse(BaseModel):
"""Response model for races list."""
races: List[RaceInfo]
class PointsSystemInfo(BaseModel):
"""Points system information."""
name: str
points: List[int]
class PointsSystemsResponse(BaseModel):
"""Response model for available points systems."""
points_systems: dict
# ============================================================================
# Validation Helpers
# ============================================================================
class InputValidator:
"""Static class for input validation utilities."""
@staticmethod
def validate_season_range(start_year: int, end_year: int) -> tuple[int, int]:
"""Validate a season range."""
if start_year > end_year:
raise ValueError("Start year cannot be after end year")
if end_year - start_year > 50:
raise ValueError("Season range cannot exceed 50 years")
return start_year, end_year
@staticmethod
def validate_race_id(race_id: int) -> int:
"""Validate a race ID."""
if race_id < 1 or race_id > 999999:
raise ValueError("Invalid race ID")
return race_id
@staticmethod
def validate_constructor_id(constructor_id: int) -> int:
"""Validate a constructor ID."""
if constructor_id < 1 or constructor_id > 99999:
raise ValueError("Invalid constructor ID")
return constructor_id
@staticmethod
def is_safe_filename(filename: str) -> bool:
"""Check if a filename is safe (no path traversal)."""
if not filename:
return False
# Check for path traversal attempts
if '..' in filename or '/' in filename or '\\' in filename:
return False
# Check for null bytes
if '\x00' in filename:
return False
# Only allow alphanumeric, underscore, hyphen, and dot
return bool(re.match(r'^[\w\-. ]+$', filename))