From 64076d70881d91e778ebfe2e7ec624077a0925f6 Mon Sep 17 00:00:00 2001 From: huangsong Date: Tue, 8 Oct 2024 16:04:32 +0800 Subject: [PATCH 1/3] simple to use --- README.md | 25 ++++---- pydantic_client/__init__.py | 5 +- pydantic_client/clients/__init__.py | 4 +- pydantic_client/clients/aiohttp.py | 7 ++- pydantic_client/clients/httpx.py | 7 ++- pydantic_client/clients/requests.py | 7 ++- pydantic_client/factory.py | 77 +++++++------------------ pydantic_client/main.py | 5 -- pydantic_client/schema/client_config.py | 18 +++++- tests/config.toml | 12 ---- tests/conftest.py | 53 ++++++++--------- tests/test_factory.py | 15 ----- tests/test_load_toml.py | 12 ---- 13 files changed, 96 insertions(+), 151 deletions(-) delete mode 100644 tests/config.toml delete mode 100644 tests/test_factory.py delete mode 100644 tests/test_load_toml.py diff --git a/README.md b/README.md index af42daf..a83a4dd 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,7 @@ Only support the json response. ```python from pydantic import BaseModel -from pydantic_client import delete, get, post, put, PydanticClient -from pydantic_client.clients import RequestsClient +from pydantic_client import delete, get, post, put, pydantic_client_factory from pydantic_client import ClientConfig @@ -34,6 +33,14 @@ class Book(BaseModel): age: int +@pydantic_client_factory.register( + ClientConfig( + client_type="requests", + base_url="https://example.com", + headers={"Authorization": "Bearer abcdefg"}, + timeout=10 + ) +) class WebClient: @get("/books/{book_id}?query={query}") @@ -55,17 +62,7 @@ class WebClient: ... -client: WebClient = PydanticClient( - ClientConfig( - base_url="https://example.com", - headers={"Authorization": "Bearer abcdefg"}, - timeout=10 - ) -).bind_client(RequestsClient) \ - .bind_protocol(WebClient) \ - .build() - - +client = pydantic_client_factory.get_client() book: Book = client.get_book(1) ``` @@ -75,3 +72,5 @@ And see the examples to get more examples. # change log ### v1.0.0: refactor all the code, to be simple. remove the group client. + +### v1.0.1: simple to use. diff --git a/pydantic_client/__init__.py b/pydantic_client/__init__.py index 4e5a6dd..f993987 100644 --- a/pydantic_client/__init__.py +++ b/pydantic_client/__init__.py @@ -3,9 +3,10 @@ from pydantic_client.main import PydanticClient from pydantic_client.schema.client_config import ClientConfig +pydantic_client_factory = PydanticClientFactory() + __all__ = [ - "PydanticClient", - "PydanticClientFactory", + "pydantic_client_factory", "ClientConfig", "patch", "get", diff --git a/pydantic_client/clients/__init__.py b/pydantic_client/clients/__init__.py index 6c75ce1..de22df5 100644 --- a/pydantic_client/clients/__init__.py +++ b/pydantic_client/clients/__init__.py @@ -1,3 +1,3 @@ -from .requests import RequestsClient from .aiohttp import AIOHttpClient -from .httpx import HttpxClient \ No newline at end of file +from .httpx import HttpxClient +from .requests import RequestsClient diff --git a/pydantic_client/clients/aiohttp.py b/pydantic_client/clients/aiohttp.py index e8e020b..ba7c3bf 100644 --- a/pydantic_client/clients/aiohttp.py +++ b/pydantic_client/clients/aiohttp.py @@ -1,10 +1,13 @@ from typing import Any -from aiohttp.client import ClientSession - from pydantic_client.clients.abstract_client import AbstractClient from pydantic_client.schema.http_request import HttpRequest +try: + from aiohttp.client import ClientSession +except ImportError: + raise ImportError("Please install `aiohttp` to use AIOHttpClient") + class AIOHttpClient(AbstractClient): diff --git a/pydantic_client/clients/httpx.py b/pydantic_client/clients/httpx.py index 05ba894..ea07007 100644 --- a/pydantic_client/clients/httpx.py +++ b/pydantic_client/clients/httpx.py @@ -1,10 +1,13 @@ from typing import Any -from httpx import AsyncClient - from pydantic_client.clients.abstract_client import AbstractClient from pydantic_client.schema.http_request import HttpRequest +try: + from httpx import AsyncClient +except ImportError: + raise ImportError("Please install `httpx` to use HttpxClient") + class HttpxClient(AbstractClient): diff --git a/pydantic_client/clients/requests.py b/pydantic_client/clients/requests.py index 86db80d..c4692e5 100644 --- a/pydantic_client/clients/requests.py +++ b/pydantic_client/clients/requests.py @@ -1,10 +1,13 @@ from typing import Any -from requests import Session - from pydantic_client.clients.abstract_client import AbstractClient from pydantic_client.schema.http_request import HttpRequest +try: + from requests import Session +except ImportError: + raise ImportError("Please install `requests` to use RequestsClient") + class RequestsClient(AbstractClient): session = Session() diff --git a/pydantic_client/factory.py b/pydantic_client/factory.py index cd08ad0..ca15e42 100644 --- a/pydantic_client/factory.py +++ b/pydantic_client/factory.py @@ -1,73 +1,36 @@ -from pydantic_client.schema.client_config import FactoryConfig +from pydantic_client.schema.client_config import ClientConfig class PydanticClientFactory: - """ - 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 - - factory = PydanticClientFactory.from_toml("pydantic_client.toml") \ - .register_client("book_client", RequestsClient, BookProtocol) \ - .register_client("author_client", HttpxClient, AuthorProtocol) - .build() - - book: Book = factory.get_client(BookProtocol).get_books(1) - - """ - - def __init__(self, config: FactoryConfig): + def __init__(self): # name: client_class self.pydantic_clients = {} - self.config = config - - @classmethod - def from_toml(cls, toml_file: str) -> "PydanticClientFactory": - config = FactoryConfig.load_toml(toml_file) - return cls(config) + # self.config = config - def register_client( + def register( self, - client_name: str, - client_class, - protocol_class, - *args, - **kwargs + client_config: ClientConfig ): - """ - params: - client_name: str, name of the client which in toml file. - client_class: client class which in pydantic-client. - protocol_class: protocol class which define the routers. - """ - cfg = self.config.get_by_name(client_name) - if not cfg: - raise ValueError(f"client {client_name} not found in config") + def wrapper(protocol_class): + from pydantic_client.main import PydanticClient - from pydantic_client import PydanticClient + client = PydanticClient(client_config) - client = PydanticClient(cfg) + self.pydantic_clients[protocol_class] = client \ + .bind_client(client_config.get_client()) \ + .bind_protocol(protocol_class) \ + .build() - self.pydantic_clients[protocol_class] = client \ - .bind_client(client_class) \ - .bind_protocol(protocol_class, *args, **kwargs) \ - .build() - return self + return protocol_class - def build(self): - return self + return wrapper - def get_client(self, protocol_class): + def get_client(self, protocol_class=None): + if len(self.pydantic_clients) == 1: + return self.pydantic_clients[list(self.pydantic_clients.keys())[0]] + if not protocol_class: + raise ValueError( + "Multiple clients exists, protocol_class is required") client = self.pydantic_clients.get(protocol_class) if not client: raise ValueError(f"client for {protocol_class} not found") diff --git a/pydantic_client/main.py b/pydantic_client/main.py index 4cf6c8c..05185f8 100644 --- a/pydantic_client/main.py +++ b/pydantic_client/main.py @@ -43,11 +43,6 @@ def __init__(self, config: ClientConfig): self.client = None self.protocol = None - @classmethod - def from_toml(cls, toml_path: str) -> "PydanticClient": - config = ClientConfig.load_toml(toml_path) - return cls(config=config) - def bind_client(self, client_class: Type[AbstractClient]): self.client = client_class(self.config) return self diff --git a/pydantic_client/schema/client_config.py b/pydantic_client/schema/client_config.py index 069380a..dee9edc 100644 --- a/pydantic_client/schema/client_config.py +++ b/pydantic_client/schema/client_config.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, Literal from pydantic import BaseModel @@ -7,11 +7,27 @@ class ClientConfig(BaseModel): + # unique name for the client + name: Optional[str] = None + # requests, httpx, aiohttp + client_type: Literal["requests", "httpx", "aiohttp"] = "requests" base_url: str headers: Dict[str, Any] = {} + # only httpx support http2 http2: bool = False timeout: Optional[int] = None + def get_client(self): + if self.client_type == "requests": + from pydantic_client.clients import RequestsClient + return RequestsClient + elif self.client_type == "httpx": + from pydantic_client.clients import HttpxClient + return HttpxClient + else: + from pydantic_client.clients import AIOHttpClient + return AIOHttpClient + @classmethod def load_toml(cls, path: str): try: diff --git a/tests/config.toml b/tests/config.toml deleted file mode 100644 index eb4bb3f..0000000 --- a/tests/config.toml +++ /dev/null @@ -1,12 +0,0 @@ -[tools.pydantic-client.config] -base_url = "http://localhost:12098" -headers.authorization = "Bearer xxxxxx" - -[[tools.pydantic-client.factory]] -name = "book" -base_url = "http://localhost:12098" -headers.authorization = "Bearer xxxxxx" - -[[tools.pydantic-client.factory]] -name = "book_2" -base_url = "http://localhost:12098" diff --git a/tests/conftest.py b/tests/conftest.py index c5b7f19..c046b08 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,30 @@ import pytest -from pydantic_client import delete, get, patch, post, put, PydanticClient -from pydantic_client.clients.aiohttp import AIOHttpClient -from pydantic_client.clients.httpx import HttpxClient -from pydantic_client.clients.requests import RequestsClient -from pydantic_client.factory import PydanticClientFactory +from pydantic_client import delete, get, patch, post, put, \ + ClientConfig, pydantic_client_factory from tests.book import Book +server_url = "http://localhost:12098" +config_1 = ClientConfig( + base_url=server_url, + timeout=10 +) + +config_2 = ClientConfig( + client_type="httpx", + base_url=server_url, + timeout=10 +) + +config_3 = ClientConfig( + client_type="aiohttp", + base_url=server_url, + timeout=10 +) + + +@pydantic_client_factory.register(config_1) class R: @get("/books/{book_id}?query={query}") @@ -41,6 +58,7 @@ def patch_book(self, book_id: int, book: Book) -> Book: ... +@pydantic_client_factory.register(config_3) class AsyncR: @get("/books/{book_id}?query={query}") async def get_book(self, book_id: int, query: str) -> Book: @@ -73,6 +91,7 @@ async def patch_book(self, book_id: int, book: Book) -> Book: ... +@pydantic_client_factory.register(config_2) class HttpxR(AsyncR): ... @@ -108,26 +127,8 @@ def start_server(): @pytest.fixture def clients(fastapi_server_url): - client_1 = PydanticClient.from_toml("tests/config.toml") \ - .bind_client(RequestsClient) \ - .bind_protocol(R).build() - - client_3 = PydanticClient.from_toml("tests/config.toml") \ - .bind_client(HttpxClient) \ - .bind_protocol(HttpxR).build() - - client_2 = PydanticClient.from_toml("tests/config.toml") \ - .bind_client(AIOHttpClient) \ - .bind_protocol(AsyncR).build() + client_1 = pydantic_client_factory.get_client(R) + client_2 = pydantic_client_factory.get_client(AsyncR) + client_3 = pydantic_client_factory.get_client(HttpxR) yield client_1, client_2, client_3 - - -@pytest.fixture -def factory(fastapi_server_url): - factory = PydanticClientFactory.from_toml("tests/config.toml") \ - .register_client("book", RequestsClient, R) \ - .register_client("book_2", HttpxClient, HttpxR) \ - .build() - - yield factory diff --git a/tests/test_factory.py b/tests/test_factory.py deleted file mode 100644 index 9017cbf..0000000 --- a/tests/test_factory.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from tests.conftest import R, HttpxR - - -@pytest.mark.asyncio -async def test_get(factory): - book = factory.get_client(R).get_book(1, "world") - assert book.name == "name" - assert book.age == 1 - - client = factory.get_client(HttpxR) - book = await client.get_book(1, "world") - assert book.name == "name" - assert book.age == 1 diff --git a/tests/test_load_toml.py b/tests/test_load_toml.py deleted file mode 100644 index b36d748..0000000 --- a/tests/test_load_toml.py +++ /dev/null @@ -1,12 +0,0 @@ -from pydantic_client.clients.requests import RequestsClient -from pydantic_client import ClientConfig - - -def test_load_toml(): - config = ClientConfig.load_toml("tests/config.toml") - assert config.base_url == "http://localhost:12098" - assert config.headers == {"authorization": "Bearer xxxxxx"} - - client = RequestsClient.from_toml(config) - assert config.base_url == "http://localhost:12098" - assert client.config.headers == {"authorization": "Bearer xxxxxx"} From 84689720f109ec14678a47020071a5b7ff916dac Mon Sep 17 00:00:00 2001 From: huangsong Date: Tue, 8 Oct 2024 16:09:21 +0800 Subject: [PATCH 2/3] update example --- example/use_config_to_init.py | 17 ++++++++++------- example/use_factory.py | 12 ------------ example/use_toml_to_init.py | 11 ----------- 3 files changed, 10 insertions(+), 30 deletions(-) delete mode 100644 example/use_factory.py delete mode 100644 example/use_toml_to_init.py diff --git a/example/use_config_to_init.py b/example/use_config_to_init.py index a656765..30e7a6b 100644 --- a/example/use_config_to_init.py +++ b/example/use_config_to_init.py @@ -1,14 +1,17 @@ -from example.protocol import BookProtocol, Book -from pydantic_client import PydanticClient, ClientConfig -from pydantic_client.clients import RequestsClient +from example.protocol import BookProtocol, Book, AuthorProtocol, Author +from pydantic_client import pydantic_client_factory, ClientConfig if __name__ == "__main__": cfg = ClientConfig( + client_type="requests", base_url="https://example.com/api" ) - client = PydanticClient(cfg) \ - .bind_client(RequestsClient) \ - .bind_protocol(BookProtocol) \ - .build() + pydantic_client_factory.register(cfg)(BookProtocol) + pydantic_client_factory.register(cfg)(AuthorProtocol) + + client = pydantic_client_factory.get_client(BookProtocol) book: Book = client.get_book(1, "name") + + author_client = pydantic_client_factory.get_client(AuthorProtocol) + author: Author = author_client.get_author() diff --git a/example/use_factory.py b/example/use_factory.py deleted file mode 100644 index d70406b..0000000 --- a/example/use_factory.py +++ /dev/null @@ -1,12 +0,0 @@ -from example.protocol import AuthorProtocol, BookProtocol, Book, Author -from pydantic_client import PydanticClientFactory -from pydantic_client.clients import RequestsClient, HttpxClient - -if __name__ == '__main__': - factory = PydanticClientFactory.from_toml("pydantic_client.toml") \ - .register_client("book_client", RequestsClient, BookProtocol) \ - .register_client("author_client", HttpxClient, AuthorProtocol) \ - .build() - - book: Book = factory.get_client(BookProtocol).get_book(1, "name") - author: Author = factory.get_client(AuthorProtocol).get_author() diff --git a/example/use_toml_to_init.py b/example/use_toml_to_init.py deleted file mode 100644 index acca40b..0000000 --- a/example/use_toml_to_init.py +++ /dev/null @@ -1,11 +0,0 @@ -from example.protocol import BookProtocol, Book -from pydantic_client import PydanticClient -from pydantic_client.clients import RequestsClient - -if __name__ == "__main__": - client = PydanticClient.from_toml("config.toml") \ - .bind_client(RequestsClient) \ - .bind_protocol(BookProtocol) \ - .build() - - book: Book = client.get_book(1, "name") From 9a63fcf20e2eeaebe56eeb7c7799e74668974dae Mon Sep 17 00:00:00 2001 From: huangsong Date: Tue, 8 Oct 2024 16:09:46 +0800 Subject: [PATCH 3/3] update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28beae2..affa25d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydantic-client" -version = "1.0.0" +version = "1.0.1" description = "Http client base pydantic, with requests or aiohttp" authors = ["ponytailer "] readme = "README.md"