Skip to content

Commit

Permalink
Merge pull request #76 from ponytailer/support-file-response
Browse files Browse the repository at this point in the history
Support file response
  • Loading branch information
ponytailer authored Oct 30, 2024
2 parents 1124010 + 2b59b54 commit 774e669
Show file tree
Hide file tree
Showing 15 changed files with 97 additions and 25 deletions.
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ And see the examples.
<details>
<summary> Change Log </summary>

### v1.0.0: refactor all the code, to be simple. remove the group client.

### v1.0.1: simple to use.

### v1.0.2: use enum to define the client type.

### v1.0.3: you can define your own client session in `client_config`

```python
Expand All @@ -101,4 +95,17 @@ client_config = ClientConfig(


```
### v1.0.5: support file response type.

```python
from pydantic_client.schema.file import File
from pydantic_client import post

@post("/download")
def download_file(self) -> File:
# you will get the bytes content of the file
...

```

</details>
2 changes: 1 addition & 1 deletion pydantic_client/clients/abstract_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self, config: ClientConfig):
def get_session(self):
return self.config.client_session

def do_request(self, request: HttpRequest) -> Dict[str, Any]:
def do_request(self, request: HttpRequest) -> Any:
raise NotImplementedError

def parse(self, request: HttpRequest) -> Dict[str, Any]:
Expand Down
8 changes: 5 additions & 3 deletions pydantic_client/clients/aiohttp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Callable, Dict
from typing import Any, Callable

from pydantic_client.clients.abstract_client import AbstractClient
from pydantic_client.schema.http_request import HttpRequest
Expand All @@ -15,7 +15,7 @@ def get_session(self) -> Callable[[], ClientSession]:
session = super().get_session()
return lambda: ClientSession() if not session else session()

async def do_request(self, request: HttpRequest) -> Dict[str, Any]:
async def do_request(self, request: HttpRequest) -> Any:
session_factory = self.get_session()
s = session_factory()
async with s as session:
Expand All @@ -24,6 +24,8 @@ async def do_request(self, request: HttpRequest) -> Dict[str, Any]:
async with req as resp:
resp.raise_for_status()
if resp.status == 200:
return await resp.json()
if not request.is_file:
return await resp.json()
return await resp.content.read()
except BaseException as e:
raise e
8 changes: 5 additions & 3 deletions pydantic_client/clients/httpx.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import Any

from pydantic_client.clients.abstract_client import AbstractClient
from pydantic_client.schema.http_request import HttpRequest
Expand All @@ -16,12 +16,14 @@ def get_session(self):
return session if isinstance(session, AsyncClient) \
else AsyncClient(http2=self.config.http2)

async def do_request(self, request: HttpRequest) -> Dict[str, Any]:
async def do_request(self, request: HttpRequest) -> Any:
async with self.get_session() as session:
try:
response = await session.request(**self.parse(request))
response.raise_for_status()
if response.is_success:
return response.json()
if not request.is_file:
return response.json()
return response.read()
except BaseException as e:
raise e
8 changes: 5 additions & 3 deletions pydantic_client/clients/requests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import Any

from pydantic_client.clients.abstract_client import AbstractClient
from pydantic_client.schema.http_request import HttpRequest
Expand All @@ -16,8 +16,10 @@ def get_session(self) -> Session:
session = super().get_session()
return session if isinstance(session, Session) else self.session

def do_request(self, request: HttpRequest) -> Dict[str, Any]:
def do_request(self, request: HttpRequest) -> Any:
try:
return self.get_session().request(**self.parse(request)).json()
response = self.get_session().request(**self.parse(request))
response.raise_for_status()
return response.json() if not request.is_file else response.content
except BaseException as e:
raise e
3 changes: 2 additions & 1 deletion pydantic_client/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ def get_request(method_info: MethodInfo, *args, **kwargs):
data=data,
json_body=json,
method=method_info.http_method,
request_headers=request_headers
request_headers=request_headers,
is_file=method_info.response_type is bytes
)


Expand Down
4 changes: 4 additions & 0 deletions pydantic_client/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic._internal._model_construction import ModelMetaclass

from pydantic_client.container import container
from pydantic_client.schema.file import File
from pydantic_client.schema.method_info import MethodInfo

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -51,6 +52,9 @@ def convert(value, target_type):
rt = method_info.response_type
if not rt:
return value

if isinstance(rt, File) and isinstance(value, bytes):
return value
if isinstance(value, dict) and isinstance(rt, ModelMetaclass):
return target_type(**value)
try:
Expand Down
3 changes: 3 additions & 0 deletions pydantic_client/schema/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from typing_extensions import TypeAlias

File: TypeAlias = bytes
1 change: 1 addition & 0 deletions pydantic_client/schema/http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ class HttpRequest(BaseModel):
url: str
method: str
request_headers: Optional[Dict] = None
is_file: bool = False
6 changes: 4 additions & 2 deletions pydantic_client/schema/method_info.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from typing import Any, Callable, Dict, Optional, Type
from typing import Any, Callable, Dict, Optional

from pydantic import BaseModel

from pydantic_client.schema.file import File


class MethodInfo(BaseModel):
func: Callable
http_method: str
url: str
request_type: Dict[str, Any]
response_type: Optional[Type]
response_type: Optional[Any]
form_body: bool
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydantic-client"
version = "1.0.4"
version = "1.0.5"
description = "Http client base pydantic, with requests or aiohttp"
authors = ["ponytailer <[email protected]>"]
readme = "README.md"
Expand Down
1 change: 1 addition & 0 deletions tests/book.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test
21 changes: 19 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import pytest
import typing
from aiohttp import ClientSession
from httpx import AsyncClient
from requests import Session

from pydantic_client import delete, get, patch, post, put, \
ClientConfig, pydantic_client_manager, ClientType
from pydantic_client.schema.file import File
from tests.book import Book

server_url = "http://localhost:12098"

config_1 = ClientConfig(
base_url=server_url,
timeout=10
)

config_2 = ClientConfig(
Expand Down Expand Up @@ -50,6 +51,10 @@
@pydantic_client_manager.register(config_1)
class R:

@get("/book_list")
def get_books(self) -> typing.List:
...

@get("/books/{book_id}?query={query}")
def get_book(self, book_id: int, query: str) -> Book:
...
Expand Down Expand Up @@ -80,9 +85,18 @@ def delete_book(self, book_id: int) -> Book:
def patch_book(self, book_id: int, book: Book) -> Book:
...

@post("/books/file")
def download(self) -> File:
...


@pydantic_client_manager.register(config_3)
class AsyncR:

@get("/book_list")
async def get_books(self) -> typing.List:
...

@get("/books/{book_id}?query={query}")
async def get_book(self, book_id: int, query: str) -> Book:
...
Expand Down Expand Up @@ -113,6 +127,10 @@ async def delete_book(self, book_id: int) -> Book:
async def patch_book(self, book_id: int, book: Book) -> Book:
...

@post("/books/file")
async def download(self) -> File:
...


@pydantic_client_manager.register(config_2)
class HttpxR(AsyncR):
Expand Down Expand Up @@ -141,7 +159,6 @@ def fastapi_server_url() -> str:
from threading import Thread
host = "localhost"
port = 12098 # TODO: add port availability check

def start_server():
run(app, host=host, port=port)

Expand Down
18 changes: 15 additions & 3 deletions tests/fastapi_service.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import json

from typing_extensions import Annotated
import typing

from fastapi import FastAPI, Form
from starlette.responses import FileResponse
from typing_extensions import Annotated

from tests.book import Book, get_the_book

Expand All @@ -14,6 +15,11 @@ async def get() -> Book:
return get_the_book()


@app.get("/book_list")
async def get_list() -> typing.List:
return ["1", "2"]


@app.get("/books/{book_id}")
def get_raw_book(book_id: int):
return get_the_book()
Expand All @@ -25,7 +31,8 @@ def get_book_num_pages(book_id: int) -> str:


@app.post("/books")
def create_book_form(name: Annotated[str, Form()], age: Annotated[int, Form()]) -> Book:
def create_book_form(name: Annotated[str, Form()],
age: Annotated[int, Form()]) -> Book:
return Book(name=name, age=age)


Expand All @@ -42,3 +49,8 @@ def delete_book(book_id: int) -> Book:
@app.patch("/books/{book_id}")
def patch_book(book_id: int, book: Book) -> Book:
return book


@app.post("/books/file")
def download_book():
return FileResponse("tests/book.txt")
18 changes: 18 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ async def test_get(clients):
assert book.age == 1


@pytest.mark.asyncio
async def test_get(clients):
for cl in clients:
book = cl.get_books()
if inspect.isawaitable(book):
book = await book
assert book == ["1", "2"]


@pytest.mark.asyncio
async def test_get_num_pages(clients):
for cl in clients:
Expand Down Expand Up @@ -71,3 +80,12 @@ async def test_patch(clients):
if inspect.isawaitable(book):
book = await book
assert book == book_to_send


@pytest.mark.asyncio
async def test_download_file(clients):
for cl in clients:
content = cl.download()
if inspect.isawaitable(content):
content = await content
assert content == b"test"

0 comments on commit 774e669

Please sign in to comment.