fpl-mcp-server 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fpl_mcp_server-0.1.3.dist-info/METADATA +137 -0
- fpl_mcp_server-0.1.3.dist-info/RECORD +33 -0
- fpl_mcp_server-0.1.3.dist-info/WHEEL +4 -0
- fpl_mcp_server-0.1.3.dist-info/entry_points.txt +2 -0
- fpl_mcp_server-0.1.3.dist-info/licenses/LICENSE +21 -0
- src/cache.py +162 -0
- src/client.py +273 -0
- src/config.py +33 -0
- src/constants.py +118 -0
- src/exceptions.py +114 -0
- src/formatting.py +299 -0
- src/main.py +41 -0
- src/models.py +526 -0
- src/prompts/__init__.py +18 -0
- src/prompts/chips.py +127 -0
- src/prompts/league_analysis.py +250 -0
- src/prompts/player_analysis.py +141 -0
- src/prompts/squad_analysis.py +136 -0
- src/prompts/team_analysis.py +121 -0
- src/prompts/transfers.py +167 -0
- src/rate_limiter.py +101 -0
- src/resources/__init__.py +13 -0
- src/resources/bootstrap.py +183 -0
- src/state.py +443 -0
- src/tools/__init__.py +25 -0
- src/tools/fixtures.py +162 -0
- src/tools/gameweeks.py +392 -0
- src/tools/leagues.py +590 -0
- src/tools/players.py +840 -0
- src/tools/teams.py +397 -0
- src/tools/transfers.py +629 -0
- src/utils.py +226 -0
- src/validators.py +290 -0
src/utils.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Shared utility functions for FPL MCP Server tools."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResponseFormat(str, Enum):
|
|
11
|
+
"""Output format for tool responses."""
|
|
12
|
+
|
|
13
|
+
MARKDOWN = "markdown"
|
|
14
|
+
JSON = "json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def handle_api_error(e: Exception) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Consistent error formatting across all tools with actionable guidance.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
e: The exception that was raised
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
User-friendly error message with guidance on next steps
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(e, httpx.HTTPStatusError):
|
|
28
|
+
if e.response.status_code == 404:
|
|
29
|
+
return (
|
|
30
|
+
"Error: Resource not found. The ID or name you provided doesn't match any existing resources.\n"
|
|
31
|
+
"Next steps:\n"
|
|
32
|
+
"• For players: Use fpl_search_players or fpl_find_player to find the correct name\n"
|
|
33
|
+
"• For teams: Use fpl_list_all_teams to see all available team names\n"
|
|
34
|
+
"• For leagues: Verify the league ID from the FPL website URL (e.g., /leagues/12345/standings/)\n"
|
|
35
|
+
"• For managers: Check the exact name in league standings using fpl_get_league_standings"
|
|
36
|
+
)
|
|
37
|
+
elif e.response.status_code == 403:
|
|
38
|
+
return (
|
|
39
|
+
"Error: Access denied. You don't have permission to access this resource.\n"
|
|
40
|
+
"This may be because:\n"
|
|
41
|
+
"• The resource is private (some leagues are private)\n"
|
|
42
|
+
"• The manager has restricted access to their team\n"
|
|
43
|
+
"Try accessing public leagues or teams instead."
|
|
44
|
+
)
|
|
45
|
+
elif e.response.status_code == 429:
|
|
46
|
+
return (
|
|
47
|
+
"Error: Rate limit exceeded. The FPL API has temporarily blocked too many requests.\n"
|
|
48
|
+
"Next steps:\n"
|
|
49
|
+
"• Wait 60 seconds before making more requests\n"
|
|
50
|
+
"• Reduce the number of consecutive API calls\n"
|
|
51
|
+
"• Use fpl:// resources instead of tools when possible (they're cached)"
|
|
52
|
+
)
|
|
53
|
+
elif e.response.status_code == 500:
|
|
54
|
+
return (
|
|
55
|
+
"Error: FPL API server error. The Fantasy Premier League servers are experiencing issues.\n"
|
|
56
|
+
"Next steps:\n"
|
|
57
|
+
"• Try again in a few minutes\n"
|
|
58
|
+
"• Check if the FPL website is working: https://fantasy.premierleague.com\n"
|
|
59
|
+
"• This is usually temporary during high traffic periods"
|
|
60
|
+
)
|
|
61
|
+
return (
|
|
62
|
+
f"Error: API request failed with status {e.response.status_code}.\n"
|
|
63
|
+
"Try the following:\n"
|
|
64
|
+
"• Verify your input parameters are correct\n"
|
|
65
|
+
"• Check the FPL API status\n"
|
|
66
|
+
"• Try again in a few moments"
|
|
67
|
+
)
|
|
68
|
+
elif isinstance(e, httpx.TimeoutException):
|
|
69
|
+
return (
|
|
70
|
+
"Error: Request timed out. The FPL API is taking too long to respond.\n"
|
|
71
|
+
"Next steps:\n"
|
|
72
|
+
"• Try again - the API may be under heavy load\n"
|
|
73
|
+
"• Use more specific filters to reduce response size\n"
|
|
74
|
+
"• Check your internet connection"
|
|
75
|
+
)
|
|
76
|
+
elif isinstance(e, httpx.ConnectError):
|
|
77
|
+
return (
|
|
78
|
+
"Error: Cannot connect to FPL API. Network connection failed.\n"
|
|
79
|
+
"Next steps:\n"
|
|
80
|
+
"• Check your internet connection\n"
|
|
81
|
+
"• Verify you can access https://fantasy.premierleague.com\n"
|
|
82
|
+
"• Try again in a moment"
|
|
83
|
+
)
|
|
84
|
+
return f"Error: {type(e).__name__}: {str(e)}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_and_truncate(
|
|
88
|
+
content: str, character_limit: int, additional_message: str | None = None
|
|
89
|
+
) -> tuple[str, bool]:
|
|
90
|
+
"""
|
|
91
|
+
Check if content exceeds character limit and truncate if needed.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
content: The content to check
|
|
95
|
+
character_limit: Maximum allowed characters
|
|
96
|
+
additional_message: Optional message to append if truncated
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Tuple of (possibly truncated content, was_truncated boolean)
|
|
100
|
+
"""
|
|
101
|
+
if len(content) <= character_limit:
|
|
102
|
+
return content, False
|
|
103
|
+
|
|
104
|
+
# Truncate to ~80% of limit to leave room for truncation message
|
|
105
|
+
truncate_to = int(character_limit * 0.8)
|
|
106
|
+
truncated = content[:truncate_to]
|
|
107
|
+
|
|
108
|
+
# Add truncation notice
|
|
109
|
+
truncation_msg = f"\n\n[Response truncated - exceeded {character_limit:,} character limit. "
|
|
110
|
+
if additional_message:
|
|
111
|
+
truncation_msg += additional_message + "]"
|
|
112
|
+
else:
|
|
113
|
+
truncation_msg += "Use more specific filters to reduce results.]"
|
|
114
|
+
|
|
115
|
+
return truncated + truncation_msg, True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_pagination_metadata(total: int, count: int, limit: int, offset: int) -> dict[str, Any]:
|
|
119
|
+
"""
|
|
120
|
+
Create standardized pagination metadata.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
total: Total number of items available
|
|
124
|
+
count: Number of items in current response
|
|
125
|
+
limit: Maximum items requested
|
|
126
|
+
offset: Current offset
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dictionary with pagination metadata
|
|
130
|
+
"""
|
|
131
|
+
has_more = total > offset + count
|
|
132
|
+
next_offset = offset + count if has_more else None
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"total": total,
|
|
136
|
+
"count": count,
|
|
137
|
+
"limit": limit,
|
|
138
|
+
"offset": offset,
|
|
139
|
+
"has_more": has_more,
|
|
140
|
+
"next_offset": next_offset,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def format_player_price(price_in_tenths: int) -> str:
|
|
145
|
+
"""Format player price from tenths to pounds."""
|
|
146
|
+
return f"£{price_in_tenths / 10:.1f}m"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def format_status_indicator(status: str, news: str | None = None) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Format player status with indicators.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
status: Player status code ('a', 'i', 'd', 'u', 's', 'n')
|
|
155
|
+
news: Optional news text
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Formatted status string with indicators
|
|
159
|
+
"""
|
|
160
|
+
indicator = ""
|
|
161
|
+
if status != "a":
|
|
162
|
+
status_map = {
|
|
163
|
+
"i": "Injured",
|
|
164
|
+
"d": "Doubtful",
|
|
165
|
+
"u": "Unavailable",
|
|
166
|
+
"s": "Suspended",
|
|
167
|
+
"n": "Not in squad",
|
|
168
|
+
}
|
|
169
|
+
indicator = f" [{status_map.get(status, status)}]"
|
|
170
|
+
|
|
171
|
+
if news:
|
|
172
|
+
indicator += " ⚠️"
|
|
173
|
+
|
|
174
|
+
return indicator
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def format_player_status(status: str) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Format player status code to full readable name.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
status: Player status code ('a', 'i', 'd', 'u', 's', 'n')
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Full status name
|
|
186
|
+
"""
|
|
187
|
+
status_map = {
|
|
188
|
+
"a": "Available",
|
|
189
|
+
"i": "Injured",
|
|
190
|
+
"d": "Doubtful",
|
|
191
|
+
"u": "Unavailable",
|
|
192
|
+
"s": "Suspended",
|
|
193
|
+
"n": "Not in squad",
|
|
194
|
+
}
|
|
195
|
+
return status_map.get(status, status)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def format_json_response(data: Any, indent: int = 2) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Format data as JSON string with consistent formatting.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
data: Data to format as JSON
|
|
204
|
+
indent: Number of spaces for indentation
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
JSON-formatted string
|
|
208
|
+
"""
|
|
209
|
+
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def pluralize(count: int, singular: str, plural: str | None = None) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Pluralize a word based on count.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
count: Number of items
|
|
218
|
+
singular: Singular form of word
|
|
219
|
+
plural: Optional plural form (defaults to singular + 's')
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Formatted string with count and properly pluralized word
|
|
223
|
+
"""
|
|
224
|
+
if count == 1:
|
|
225
|
+
return f"{count} {singular}"
|
|
226
|
+
return f"{count} {plural or singular + 's'}"
|
src/validators.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input validation and sanitization for FPL MCP Server.
|
|
3
|
+
|
|
4
|
+
Provides validators to prevent injection attacks and ensure data integrity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .exceptions import ValidationError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("fpl_validators")
|
|
14
|
+
|
|
15
|
+
# Constants for validation
|
|
16
|
+
MAX_STRING_LENGTH = 1000
|
|
17
|
+
MAX_NAME_LENGTH = 200
|
|
18
|
+
MIN_ID = 1
|
|
19
|
+
MAX_ID = 999999
|
|
20
|
+
MAX_GAMEWEEK = 38
|
|
21
|
+
MIN_GAMEWEEK = 1
|
|
22
|
+
MAX_PAGE_SIZE = 100
|
|
23
|
+
MIN_PAGE_SIZE = 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sanitize_string(
|
|
27
|
+
value: str, field_name: str = "input", max_length: int = MAX_STRING_LENGTH
|
|
28
|
+
) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Sanitize string input to prevent injection attacks.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
value: String to sanitize
|
|
34
|
+
field_name: Name of the field for error messages
|
|
35
|
+
max_length: Maximum allowed length
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Sanitized string
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValidationError: If input is invalid
|
|
42
|
+
"""
|
|
43
|
+
if not isinstance(value, str):
|
|
44
|
+
raise ValidationError(f"{field_name} must be a string, got {type(value).__name__}")
|
|
45
|
+
|
|
46
|
+
# Remove null bytes and control characters
|
|
47
|
+
sanitized = value.replace("\0", "").strip()
|
|
48
|
+
|
|
49
|
+
# Check length
|
|
50
|
+
if len(sanitized) > max_length:
|
|
51
|
+
raise ValidationError(
|
|
52
|
+
f"{field_name} exceeds maximum length of {max_length} characters (got {len(sanitized)})"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not sanitized:
|
|
56
|
+
raise ValidationError(f"{field_name} cannot be empty after sanitization")
|
|
57
|
+
|
|
58
|
+
# Log suspicious patterns
|
|
59
|
+
if re.search(r"[<>]|script|javascript|onclick|onerror", sanitized, re.IGNORECASE):
|
|
60
|
+
logger.warning(f"Suspicious pattern detected in {field_name}: {sanitized[:50]}")
|
|
61
|
+
|
|
62
|
+
return sanitized
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate_player_name(name: str) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Validate and sanitize player name.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name: Player name to validate
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Sanitized player name
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValidationError: If name is invalid
|
|
77
|
+
"""
|
|
78
|
+
sanitized = sanitize_string(name, "player_name", MAX_NAME_LENGTH)
|
|
79
|
+
|
|
80
|
+
# Player names should only contain letters, spaces, hyphens, apostrophes, and dots
|
|
81
|
+
if not re.match(r"^[A-Za-z\s\-'.]+$", sanitized):
|
|
82
|
+
raise ValidationError(
|
|
83
|
+
"Player name contains invalid characters. "
|
|
84
|
+
"Only letters, spaces, hyphens, apostrophes, and dots are allowed"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return sanitized
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_team_name(name: str) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Validate and sanitize team name.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
name: Team name to validate
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Sanitized team name
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValidationError: If name is invalid
|
|
102
|
+
"""
|
|
103
|
+
sanitized = sanitize_string(name, "team_name", MAX_NAME_LENGTH)
|
|
104
|
+
|
|
105
|
+
# Team names can contain letters, spaces, numbers, and common punctuation
|
|
106
|
+
if not re.match(r"^[A-Za-z0-9\s\-'&.]+$", sanitized):
|
|
107
|
+
raise ValidationError(
|
|
108
|
+
"Team name contains invalid characters. "
|
|
109
|
+
"Only letters, numbers, spaces, hyphens, apostrophes, ampersands, and dots are allowed"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return sanitized
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def validate_positive_int(
|
|
116
|
+
value: Any,
|
|
117
|
+
field_name: str = "value",
|
|
118
|
+
min_value: int = MIN_ID,
|
|
119
|
+
max_value: int = MAX_ID,
|
|
120
|
+
) -> int:
|
|
121
|
+
"""
|
|
122
|
+
Validate that value is a positive integer within range.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
value: Value to validate
|
|
126
|
+
field_name: Name of the field for error messages
|
|
127
|
+
min_value: Minimum allowed value
|
|
128
|
+
max_value: Maximum allowed value
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Validated integer
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValidationError: If value is invalid
|
|
135
|
+
"""
|
|
136
|
+
if not isinstance(value, int):
|
|
137
|
+
try:
|
|
138
|
+
value = int(value)
|
|
139
|
+
except (ValueError, TypeError) as e:
|
|
140
|
+
raise ValidationError(
|
|
141
|
+
f"{field_name} must be an integer, got {type(value).__name__}"
|
|
142
|
+
) from e
|
|
143
|
+
|
|
144
|
+
if value < min_value:
|
|
145
|
+
raise ValidationError(f"{field_name} must be at least {min_value}, got {value}")
|
|
146
|
+
|
|
147
|
+
if value > max_value:
|
|
148
|
+
raise ValidationError(f"{field_name} must be at most {max_value}, got {value}")
|
|
149
|
+
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def validate_player_id(player_id: Any) -> int:
|
|
154
|
+
"""
|
|
155
|
+
Validate player ID.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
player_id: Player ID to validate
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Validated player ID
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValidationError: If ID is invalid
|
|
165
|
+
"""
|
|
166
|
+
return validate_positive_int(player_id, "player_id", MIN_ID, MAX_ID)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def validate_team_id(team_id: Any) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Validate team ID.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
team_id: Team ID to validate
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Validated team ID
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValidationError: If ID is invalid
|
|
181
|
+
"""
|
|
182
|
+
# FPL has 20 teams
|
|
183
|
+
return validate_positive_int(team_id, "team_id", MIN_ID, 20)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def validate_gameweek(gameweek: Any) -> int:
|
|
187
|
+
"""
|
|
188
|
+
Validate gameweek number.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
gameweek: Gameweek number to validate
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Validated gameweek
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
ValidationError: If gameweek is invalid
|
|
198
|
+
"""
|
|
199
|
+
return validate_positive_int(gameweek, "gameweek", MIN_GAMEWEEK, MAX_GAMEWEEK)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def validate_manager_id(manager_id: Any) -> int:
|
|
203
|
+
"""
|
|
204
|
+
Validate manager ID.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
manager_id: Manager ID to validate
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Validated manager ID
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
ValidationError: If ID is invalid
|
|
214
|
+
"""
|
|
215
|
+
return validate_positive_int(manager_id, "manager_id", MIN_ID, MAX_ID * 10)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def validate_league_id(league_id: Any) -> int:
|
|
219
|
+
"""
|
|
220
|
+
Validate league ID.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
league_id: League ID to validate
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Validated league ID
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ValidationError: If ID is invalid
|
|
230
|
+
"""
|
|
231
|
+
return validate_positive_int(league_id, "league_id", MIN_ID, MAX_ID * 10)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def validate_page_number(page: Any) -> int:
|
|
235
|
+
"""
|
|
236
|
+
Validate page number for pagination.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
page: Page number to validate
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Validated page number
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
ValidationError: If page is invalid
|
|
246
|
+
"""
|
|
247
|
+
return validate_positive_int(page, "page", 1, 10000)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def validate_page_size(page_size: Any) -> int:
|
|
251
|
+
"""
|
|
252
|
+
Validate page size for pagination.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
page_size: Page size to validate
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Validated page size
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValidationError: If page size is invalid
|
|
262
|
+
"""
|
|
263
|
+
return validate_positive_int(page_size, "page_size", MIN_PAGE_SIZE, MAX_PAGE_SIZE)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def validate_boolean(value: Any, field_name: str = "value") -> bool:
|
|
267
|
+
"""
|
|
268
|
+
Validate and convert to boolean.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
value: Value to validate
|
|
272
|
+
field_name: Name of the field for error messages
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Boolean value
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ValidationError: If value cannot be converted to boolean
|
|
279
|
+
"""
|
|
280
|
+
if isinstance(value, bool):
|
|
281
|
+
return value
|
|
282
|
+
|
|
283
|
+
if isinstance(value, str):
|
|
284
|
+
lower = value.lower()
|
|
285
|
+
if lower in ("true", "1", "yes"):
|
|
286
|
+
return True
|
|
287
|
+
if lower in ("false", "0", "no"):
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
raise ValidationError(f"{field_name} must be a boolean value, got {value}")
|