Coverage for src/lexigram/web/pipes/builtin/file.py: 30%
40 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""File validation pipes for file uploads.
3Pipes that validate file uploads (size, type, etc).
4"""
6from __future__ import annotations
8from typing import Any
10from lexigram.web.protocols import ParamMetadata, PipeProtocol
13class FileSizeValidationPipe(PipeProtocol):
14 """PipeProtocol that validates file upload size.
16 Example:
17 ```python
18 class UploadController(Controller):
19 @post("/upload")
20 async def upload(
21 self,
22 @file(pipe=FileSizeValidationPipe(max_size_mb=10)) file: UploadFile
23 ):
24 ...
25 ```
26 """
28 def __init__(self, max_size_mb: int = 10):
29 """Initialize the pipe.
31 Args:
32 max_size_mb: Maximum file size in megabytes.
33 """
34 self._max_size = max_size_mb * 1024 * 1024 # Convert to bytes
36 async def transform(self, value: Any, metadata: ParamMetadata) -> Any:
37 """Validate file size.
39 Args:
40 value: The uploaded file.
41 metadata: Metadata about the parameter.
43 Returns:
44 The file if valid.
46 Raises:
47 ValueError: If file exceeds max size.
48 """
49 if value is None:
50 return None
52 # Check if it's a file-like object
53 if hasattr(value, "size"):
54 if value.size > self._max_size:
55 raise ValueError(
56 f"File '{getattr(value, 'filename', 'unknown')}' "
57 f"exceeds maximum size of {self._max_size // (1024 * 1024)}MB",
58 )
59 elif hasattr(value, "content_length"):
60 if value.content_length and value.content_length > self._max_size:
61 raise ValueError(
62 f"File exceeds maximum size of {self._max_size // (1024 * 1024)}MB",
63 )
65 return value
68class FileTypeValidationPipe(PipeProtocol):
69 """PipeProtocol that validates file MIME types.
71 Example:
72 ```python
73 class UploadController(Controller):
74 @post("/upload")
75 async def upload(
76 self,
77 @file(pipe=FileTypeValidationPipe(allowed_types=["image/png", "image/jpeg"])) file: UploadFile
78 ):
79 ...
80 ```
81 """
83 def __init__(self, allowed_types: list[str] | None = None):
84 """Initialize the pipe.
86 Args:
87 allowed_types: List of allowed MIME types. If None, accepts all.
88 """
89 self._allowed_types = allowed_types or []
91 async def transform(self, value: Any, metadata: ParamMetadata) -> Any:
92 """Validate file type.
94 Args:
95 value: The uploaded file.
96 metadata: Metadata about the parameter.
98 Returns:
99 The file if valid.
101 Raises:
102 ValueError: If file type is not allowed.
103 """
104 if value is None:
105 return None
107 if not self._allowed_types:
108 return value
110 # Get content type
111 content_type = None
112 if hasattr(value, "content_type"):
113 content_type = value.content_type
114 elif hasattr(value, "headers"):
115 content_type = value.headers.get("content-type")
117 if content_type and content_type not in self._allowed_types:
118 raise ValueError(
119 f"File type '{content_type}' not allowed. "
120 f"Allowed types: {', '.join(self._allowed_types)}",
121 )
123 return value
126class TrimPipe(PipeProtocol):
127 """PipeProtocol that trims whitespace from string inputs.
129 Example:
130 ```python
131 class SearchController(Controller):
132 @get("/search")
133 async def search(
134 self,
135 @query(pipe=TrimPipe()) query: str,
136 ):
137 ...
138 ```
139 """
141 async def transform(self, value: Any, metadata: ParamMetadata) -> Any:
142 """Trim whitespace from string.
144 Args:
145 value: The value to trim.
146 metadata: Metadata about the parameter.
148 Returns:
149 Trimmed string.
150 """
151 if value is None:
152 return None
154 if isinstance(value, str):
155 return value.strip()
157 return value
160__all__ = [
161 "FileSizeValidationPipe",
162 "FileTypeValidationPipe",
163 "TrimPipe",
164]