Skip to content

Commit 88722a9

Browse files
authored
Merge pull request #87 from SimplifyJobs/renovate/fastapi-0.x
fix(deps): update dependency fastapi to >=0.110.0,<0.136.0
2 parents e27f4e2 + 3cfce41 commit 88722a9

7 files changed

Lines changed: 127 additions & 23 deletions

File tree

examples/full/serializer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Third Party Imports
2-
from pydantic.v1 import BaseModel
2+
from pydantic import BaseModel
33

44

55
class Payload(BaseModel):

examples/simple/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# Third Party Imports
66
from fastapi import FastAPI
77
from fastapi.routing import APIRouter
8-
from pydantic.v1 import BaseModel
8+
from pydantic import BaseModel
99

1010
# Imports from this repository
1111
from fastapi_gcp_tasks import DelayedRouteBuilder

fastapi_gcp_tasks/exception.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
# Third Party Imports
2-
from pydantic.v1.errors import MissingError, PydanticValueError
3-
4-
# TODO: Migrate to Pydantic v2.0 Errors
5-
6-
7-
class MissingParamError(MissingError):
1+
class MissingParamError(ValueError):
82
"""Error raised when a required parameter is missing."""
93

104
msg_template = "field required: {param}"
115

6+
def __init__(self, **ctx: object) -> None:
7+
super().__init__(self.msg_template.format(**ctx))
128

13-
class WrongTypeError(PydanticValueError):
9+
10+
class WrongTypeError(ValueError):
1411
"""Error raised when a parameter is of the wrong type."""
1512

1613
msg_template = "Expected {field} to be of type {type}"
1714

15+
def __init__(self, **ctx: object) -> None:
16+
super().__init__(self.msg_template.format(**ctx))
17+
1818

1919
class BadMethodError(Exception):
2020
"""Error raised when an invalid method is passed to a task."""

fastapi_gcp_tasks/requester.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Standard Library Imports
2-
from typing import Any, Dict, List, Tuple
2+
from typing import Any, Dict, List, Tuple, get_origin
33
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
44

55
# Third Party Imports
66
from fastapi.dependencies.utils import request_params_to_args
77
from fastapi.encoders import jsonable_encoder
88
from fastapi.routing import APIRoute
9-
from pydantic.v1.error_wrappers import ErrorWrapper
109

1110
# Imports from this repository
1211
from fastapi_gcp_tasks.exception import MissingParamError, WrongTypeError
@@ -86,20 +85,23 @@ def _body(self, *, values: Dict[str, Any]) -> bytes | None:
8685
if body_field and body_field.name:
8786
got_body = values.get(body_field.name)
8887
if got_body is None:
89-
if body_field.required:
90-
raise MissingParamError(name=body_field.name)
88+
if body_field.field_info.is_required():
89+
raise MissingParamError(param=body_field.name)
9190
got_body = body_field.get_default()
92-
if not isinstance(got_body, body_field.type_):
93-
raise WrongTypeError(field=body_field.name, type=body_field.type_)
91+
body_type = body_field.field_info.annotation
92+
check_type = get_origin(body_type) or body_type
93+
if body_type is not None and isinstance(check_type, type) and not isinstance(got_body, check_type):
94+
raise WrongTypeError(field=body_field.name, type=body_type)
9495
body = json.dumps(jsonable_encoder(got_body)).encode()
9596
return body
9697

9798

98-
def _err_val(resp: Tuple[Dict, List[ErrorWrapper]]) -> Dict:
99+
def _err_val(resp: Tuple[Dict[str, Any], List[Any]]) -> Dict[str, Any]:
99100
values, errors = resp
100101

101102
if len(errors) != 0:
102103
# TODO: Log everything but raise first only
103104
# TODO: find a better way to raise and display these errors
104-
raise errors[0].exc
105+
err = errors[0]
106+
raise ValueError(str(err))
105107
return values

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ requires-python = ">=3.11,<3.14"
99
dependencies = [
1010
"google-cloud-tasks>=2.21.0,<2.22.0",
1111
"google-cloud-scheduler>=2.13.3,<2.20.0",
12-
"fastapi>=0.110.0,<0.130.0",
12+
"fastapi>=0.110.0,<0.136.0",
1313
]
1414

1515
[dependency-groups]

tests/test_requester.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Unit tests for Requester._body covering missing-param and generic-type bugs."""
2+
3+
# Standard Library Imports
4+
from typing import List, Union
5+
6+
# Third Party Imports
7+
import pytest
8+
from fastapi import FastAPI
9+
from fastapi.routing import APIRoute
10+
from pydantic import BaseModel
11+
12+
# Imports from this repository
13+
from fastapi_gcp_tasks.exception import MissingParamError, WrongTypeError
14+
from fastapi_gcp_tasks.requester import Requester
15+
16+
17+
class Item(BaseModel):
18+
"""Simple model for testing."""
19+
20+
name: str
21+
22+
23+
app = FastAPI()
24+
25+
26+
@app.post("/required_body")
27+
async def required_body_endpoint(item: Item) -> None:
28+
"""Endpoint with a required body param."""
29+
30+
31+
@app.post("/optional_body")
32+
async def optional_body_endpoint(item: Item = Item(name="default")) -> None:
33+
"""Endpoint with an optional body param."""
34+
35+
36+
@app.post("/list_body")
37+
async def list_body_endpoint(items: List[Item]) -> None:
38+
"""Endpoint with a parameterized generic body."""
39+
40+
41+
@app.post("/union_body")
42+
async def union_body_endpoint(item: Union[Item, str] = "fallback") -> None:
43+
"""Endpoint with a Union-typed body."""
44+
45+
46+
def _get_route(path: str) -> APIRoute:
47+
for route in app.routes:
48+
if isinstance(route, APIRoute) and route.path == path:
49+
return route
50+
raise ValueError(f"Route {path} not found")
51+
52+
53+
class TestMissingRequiredBody:
54+
"""Bug: MissingParamError called with name= but template expects {param}."""
55+
56+
def test_missing_required_body_raises_missing_param_error(self) -> None:
57+
"""Missing required body should raise MissingParamError, not KeyError."""
58+
route = _get_route("/required_body")
59+
requester = Requester(route=route, base_url="http://localhost")
60+
with pytest.raises(MissingParamError, match="field required"):
61+
requester._body(values={})
62+
63+
def test_optional_body_uses_default(self) -> None:
64+
"""Optional body should fall back to default when not provided."""
65+
route = _get_route("/optional_body")
66+
requester = Requester(route=route, base_url="http://localhost")
67+
body = requester._body(values={})
68+
assert body is not None
69+
assert b"default" in body
70+
71+
72+
class TestGenericBodyType:
73+
"""Bug: isinstance crashes with parameterized generics like list[Item]."""
74+
75+
def test_list_body_does_not_crash_with_valid_input(self) -> None:
76+
"""Parameterized generic body should not raise TypeError on isinstance."""
77+
route = _get_route("/list_body")
78+
requester = Requester(route=route, base_url="http://localhost")
79+
items = [Item(name="a"), Item(name="b")]
80+
body = requester._body(values={"items": items})
81+
assert body is not None
82+
83+
def test_list_body_wrong_type_raises(self) -> None:
84+
"""Wrong type for a generic body should raise WrongTypeError."""
85+
route = _get_route("/list_body")
86+
requester = Requester(route=route, base_url="http://localhost")
87+
with pytest.raises(WrongTypeError):
88+
requester._body(values={"items": "not a list"})
89+
90+
def test_simple_body_wrong_type_raises(self) -> None:
91+
"""Wrong type for a simple body should raise WrongTypeError."""
92+
route = _get_route("/required_body")
93+
requester = Requester(route=route, base_url="http://localhost")
94+
with pytest.raises(WrongTypeError):
95+
requester._body(values={"item": "not an Item"})
96+
97+
def test_union_body_does_not_crash(self) -> None:
98+
"""Union-typed body should not raise TypeError on isinstance check."""
99+
route = _get_route("/union_body")
100+
requester = Requester(route=route, base_url="http://localhost")
101+
body = requester._body(values={"item": Item(name="test")})
102+
assert body is not None

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)