Skip to content

Commit

Permalink
Merge pull request #63 from ponytailer/inject-v2
Browse files Browse the repository at this point in the history
Inject v2
  • Loading branch information
ponytailer authored Sep 26, 2024
2 parents 50ff9f9 + 45ba08b commit 007a1d5
Show file tree
Hide file tree
Showing 25 changed files with 513 additions and 323 deletions.
141 changes: 82 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![codecov](https://codecov.io/gh/ponytailer/pydantic-client/branch/main/graph/badge.svg?token=CZX5V1YP22)](https://codecov.io/gh/ponytailer/pydantic-client) [![Upload Python Package](https://github.com/ponytailer/pydantic-client/actions/workflows/python-publish.yml/badge.svg)](https://github.com/ponytailer/pydantic-client/actions/workflows/python-publish.yml)

Http client base pydantic, with requests, aiohttp and httpx.
Only support the json response.

### How to install

Expand All @@ -23,16 +24,17 @@ Http client base pydantic, with requests, aiohttp and httpx.
```python
from pydantic import BaseModel

from pydantic_client import delete, get, post, put
from pydantic_client.clients.requests import RequestsClient
from pydantic_client import delete, get, post, put, PydanticClient
from pydantic_client.clients import RequestsClient, AIOHttpClient, HttpxClient
from pydantic_client import ClientConfig, PydanticClientFactory


class Book(BaseModel):
name: str
age: int


class R(RequestsClient):
class WebClient:

@get("/books/{book_id}?query={query}")
def get_book(self, book_id: int, query: str) -> Book:
Expand All @@ -52,72 +54,93 @@ class R(RequestsClient):
def change_book(self, book_id: int) -> Book:
...


my_client = R("http://localhost/v1")
get_book: Book = my_client.get_book(1)
```

The Group

```python

from pydantic_client import Group
from pydantic_client.clients.requests import RequestsClient

group = Group("/book")
person_group = Group("/person")


class GroupClient(RequestsClient):
def __init__(self):
super().__init__("http://localhost")

@group.get("/{book_id}")
def get(self, book_id: int) -> Book: # type: ignore
"""
toml config example:
[tools.pydantic-client.config]
base_url = "http://localhost:5000" (have to set)
headers.authorization = "Bearer xxxxxx" (optional)
http2 = true (optional)
timeout = 10 (optional)
"""


client = PydanticClient.from_toml("your_toml_path.toml") \
.bind_client(RequestsClient) \
.bind_protocol(WebClient) \
.build()

# or

client: WebClient = PydanticClient(
ClientConfig(
base_url="https://example.com",
headers={"Authorization": "Bearer abcdefg"},
timeout=10
)
).bind_client(RequestsClient) \
.bind_protocol(WebClient) \
.build()


get_book: Book = client.get_book(1)

# use the factory

"""
toml file example:
[[tools.pydantic_client.factory]]
name = "book_client
base_url = "https://example.com/api/v1"
timeout = 1
[[tools.pydantic_client.factory]]
name = "author_client
base_url = "https://example.com/api/v2"
timeout = 1
[[tools.pydantic_client.factory]]
name = "address_client
base_url = "https://example.com/api/v3"
timeout = 1
"""

class BookProtocol:
@get("/books/{book_id}?query={query}")
def get_book(self, book_id: int, query: str) -> Book:
...

@person_group.get("/{person_id}")
def get_person(self, person_id: int) -> Person: # type: ignore

class AuthorProtocol:
@get("/books/{book_id}?query={query}")
def get_book(self, book_id: int, query: str) -> Book:
...

class AddressProtocol:
@get("/books/{book_id}?query={query}")
def get_book(self, book_id: int, query: str) -> Book:
...

factory = PydanticClientFactory.from_toml("pydantic_client.toml") \
.register_client("book_client", RequestsClient, BookProtocol) \
.register_client("author_client", HttpxClient, AuthorProtocol) \
.register_client("address_client", AIOHttpClient, AddressProtocol) \
.build()

client = GroupClient()
book = client.get(1)
person = client.get_person(1)


book: Book = factory.get_client(BookProtocol).get_book(1, "name")
author: Book = factory.get_client(AuthorProtocol).get_book(1, "name")
```

# change log

### v0.1.14: add global or request level headers

```python

# global level headers
my_client = R("http://localhost/v1", headers={"Authorization": "xxxxxxx"})
### v1.0.0: refactor all the code, to be simple. remove the group client.

# request level headers, and its priority is higher than global.
factory = PydanticClientFactory.from_toml("pydantic_client.toml") \
.register_client("book_client", RequestsClient, BookProtocol) \
.register_client("author_client", HttpxClient, AuthorProtocol) \
.register_client("address_client", AIOHttpClient, AddressProtocol) \
.build()

# header should be xxxxxxx
my_client.delete(1)
# header should be zzzzz
my_client.get(1, request_headers={"Authorization": "zzzzz"})
# header should be yyyyy
my_client.post(1, request_headers={"Authorization": "yyyyy"})
book: Book = factory.get_client(BookProtocol).get_books(1)
```
### v0.1.17: load config from toml to init client.

```python

# config.toml or pyproject.toml
[tool.pydantic_client.config]
base_url = "http://localhost/v1"
headers.authorization = "xxxxxxx"
headers.user = "xxxxxxx"

client = R.load_config_from_toml("config.toml")

# change log
```
### v1.0.0: refactor all the code, to be simple. remove the group client.
10 changes: 7 additions & 3 deletions pydantic_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from .decorators import delete, get, patch, post, put
from .group import Group
from pydantic_client.decorators import delete, get, patch, post, put
from pydantic_client.factory import PydanticClientFactory
from pydantic_client.main import PydanticClient
from pydantic_client.schema.client_config import ClientConfig

__all__ = [
"PydanticClient",
"PydanticClientFactory",
"ClientConfig",
"patch",
"get",
"post",
"put",
"delete",
"Group",
]
3 changes: 3 additions & 0 deletions pydantic_client/clients/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .requests import RequestsClient
from .aiohttp import AIOHttpClient
from .httpx import HttpxClient
18 changes: 5 additions & 13 deletions pydantic_client/clients/abstract_client.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
from typing import Any, Dict, Tuple
from typing import Any, Dict, Tuple, Optional

from pydantic_client.schema.client_config import ClientConfig
from pydantic_client.schema.http_request import HttpRequest


class AbstractClient:

def __init__(
self, base_url: str, headers: Dict[str, Any] = None,
http2: bool = False
):
self.base_url = base_url.rstrip("/")
self.headers = headers
self.http2 = http2

@staticmethod
def data_encoder(x):
return x
def __init__(self, config: ClientConfig):
self.config = config
self.base_url = config.base_url.rstrip("/")

def do_request(self, request: HttpRequest) -> Any:
raise NotImplementedError
Expand All @@ -31,4 +23,4 @@ def parse_request(request: HttpRequest) -> Tuple[Dict, Dict]:

@classmethod
def from_toml(cls, toml_config: ClientConfig):
return cls(**toml_config.model_dump())
return cls(toml_config)
9 changes: 4 additions & 5 deletions pydantic_client/clients/aiohttp.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
from typing import Any, Type, Dict
from typing import Any

from aiohttp.client import ClientSession

from pydantic_client.clients.abstract_client import AbstractClient
from pydantic_client.proxy import AsyncClientProxy, Proxy
from pydantic_client.schema.http_request import HttpRequest


class AIOHttpClient(AbstractClient):
runner_class: Proxy = AsyncClientProxy

async def do_request(self, request: HttpRequest) -> Any:
data, json = self.parse_request(request)
headers = request.request_headers if request.request_headers \
else self.headers
else self.config.headers
async with ClientSession() as session:
try:
req = session.request(
url=self.base_url + request.url,
method=request.method,
json=json,
data=data,
headers=headers
headers=headers,
timeout=self.config.timeout
)

async with req as resp:
Expand Down
12 changes: 6 additions & 6 deletions pydantic_client/clients/httpx.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
from typing import Any, Type
from typing import Any

from httpx import AsyncClient

from pydantic_client.clients.abstract_client import AbstractClient
from pydantic_client.proxy import AsyncClientProxy, Proxy
from pydantic_client.schema.http_request import HttpRequest


class HttpxClient(AbstractClient):
runner_class: Type[Proxy] = AsyncClientProxy

async def do_request(self, request: HttpRequest) -> Any:
data, json = self.parse_request(request)
headers = request.request_headers if request.request_headers \
else self.headers
async with AsyncClient(http2=self.http2) as session:
else self.config.headers
async with AsyncClient(http2=self.config.http2) as session:
try:
response = await session.request(
url=self.base_url + request.url,
method=request.method,
json=json,
data=data,
headers=headers
headers=headers,
timeout=self.config.timeout
)
response.raise_for_status()
if response.is_success:
Expand Down
9 changes: 4 additions & 5 deletions pydantic_client/clients/requests.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
from typing import Any, Type
from typing import Any

from requests import Session

from pydantic_client.clients.abstract_client import AbstractClient
from pydantic_client.proxy import ClientProxy, Proxy
from pydantic_client.schema.http_request import HttpRequest


class RequestsClient(AbstractClient):
runner_class: Type[Proxy] = ClientProxy
session = Session()

def do_request(self, request: HttpRequest) -> Any:
data, json = self.parse_request(request)
headers = request.request_headers if request.request_headers \
else self.headers
else self.config.headers

try:
return self.session.request(
url=self.base_url + request.url,
method=request.method,
json=json,
data=data,
headers=headers
headers=headers,
timeout=self.config.timeout,
).json()
except BaseException as e:
raise e
Loading

0 comments on commit 007a1d5

Please sign in to comment.