Skip to content

Commit

Permalink
Merge pull request #3 from CrazyProger1/paginator
Browse files Browse the repository at this point in the history
Release 0.0.1
  • Loading branch information
CrazyProger1 authored Jan 26, 2024
2 parents 60a014e + e8a022f commit 6d432b9
Show file tree
Hide file tree
Showing 29 changed files with 788 additions and 23 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock

# poetry
poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
Expand Down Expand Up @@ -161,3 +161,6 @@ cython_debug/

# Linter
.ruff_cache

# MyPy
.mypy_cache
126 changes: 125 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,126 @@
# RestyClient
RestyClient is a simple, easy-to-use Python library for interacting with REST APIs using Pydantic's powerful data validation and deserialization tools. This library provides an intuitive API that makes it easy to make HTTP requests and handle data on the client side.

<p align="center">
<img src="docs/resty-cat.png" alt="resty lib logo">
</p>

<p align="center">
<a href="https://github.com/CrazyProger1/RestyClient/blob/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/CrazyProger1/RestyClient"></a>
<a href="https://github.com/CrazyProger1/RestyClient/releases/latest"><img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/CrazyProger1/RestyClient"></a>
<a href="https://pypi.org/project/resty-client/"><img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dm/resty-client"></a>
<a><img src="https://img.shields.io/pypi/status/resty-client" alt="Status"></a>
</p>

RestyClient is a simple, easy-to-use Python library for interacting with REST APIs using Pydantic's powerful data
validation and deserialization tools. This library provides an intuitive API that makes it easy to make HTTP requests
and handle data on the client side.

## Features

- Middleware system, which allows you to implement any pagination, filtering or authentication.

## Installation

Using pip:

```shell
pip install resty-client
```

Using Poetry:

```shell
poetry add resty-client
```

## Getting-Started

### Schema

```python
from pydantic import BaseModel


class Product(BaseModel):
id: int | None = None
name: str
description: str
code: str
```

### Serializer

```python
from resty.serializers import Serializer


class ProductSerializer(Serializer):
schema = Product
```

### Manager

```python
from resty.enums import (
Endpoint,
Field
)
from resty.managers import Manager


class ProductManager(Manager):
serializer = ProductSerializer
endpoints = {
Endpoint.CREATE: '/products/',
Endpoint.READ: '/products/',
Endpoint.READ_ONE: '/products/{pk}/',
Endpoint.UPDATE: '/products/{pk}/',
Endpoint.DELETE: '/products/{pk}/',
}
fields = {
Field.PRIMARY: 'id',
}
```

### CRUD

```python
from httpx import AsyncClient

from resty.clients.httpx import RESTClient


async def main():
xclient = AsyncClient(base_url='http://localhost:8000/')
rest_client = RESTClient(xclient=xclient)

product = Product(
name='First prod',
description='My Desc',
code='123W31Q'
)

# Create
created = await ProductManager.create(rest_client, product)

# Read
my_product = await ProductManager.read_one(rest_client, created.id)

for prod in await ProductManager.read(rest_client):
print(prod.name)

# Update
my_product.description = 'QWERTY'
await ProductManager.update(rest_client, my_product)

# Delete
await ProductManager.delete(rest_client, my_product.id)
```

## Status

``0.0.1`` - INDEV

## Licence

RestyClient is released under the MIT License. See the bundled [LICENSE](LICENSE) file for details.
File renamed without changes.
Binary file added docs/resty-cat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions examples/crud/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import asyncio
import httpx

from resty.clients.httpx import RESTClient
from resty.ext.django.middlewares import DjangoPagePaginationMiddleware

from managers import ProductManager
from schemas import Product


async def main():
xclient = httpx.AsyncClient(base_url='http://localhost:8000/')

rest_client = RESTClient(xclient=xclient)

rest_client.add_middleware(DjangoPagePaginationMiddleware())

product = Product(
name='My Product',
description='My Desc',
code='123W31QQW'
)

# Create
created = await ProductManager.create(rest_client, product)

# Read
my_product = await ProductManager.read_one(rest_client, created.id)

for prod in await ProductManager.read(rest_client):
print(prod.name)

# Update
my_product.description = 'QWERTY'
await ProductManager.update(rest_client, my_product)

# Delete
await ProductManager.delete(rest_client, my_product.id)


if __name__ == '__main__':
asyncio.run(main())
21 changes: 21 additions & 0 deletions examples/crud/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from resty.enums import (
Endpoint,
Field
)
from resty.managers import Manager

from serializers import ProductSerializer


class ProductManager(Manager):
serializer = ProductSerializer
endpoints = {
Endpoint.CREATE: '/products/',
Endpoint.READ: '/products/',
Endpoint.READ_ONE: '/products/{pk}/',
Endpoint.UPDATE: '/products/{pk}/',
Endpoint.DELETE: '/products/{pk}/',
}
fields = {
Field.PRIMARY: 'id',
}
9 changes: 9 additions & 0 deletions examples/crud/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic import BaseModel


class Product(BaseModel):
id: int | None = None
name: str
description: str
code: str

7 changes: 7 additions & 0 deletions examples/crud/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from resty.serializers import Serializer

from schemas import Product


class ProductSerializer(Serializer):
schema = Product
36 changes: 21 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"


[project]
name = "RestyClient"
[tool.poetry]
name = "resty-client"
version = "0.0.1"
authors = [
{ name = "crazyproger1", email = "[email protected]" },
]
description = "RestyClient is a simple, easy-to-use Python library for interacting with REST APIs usingPydantic's powerful data validation and deserialization tools."
description = "RestyClient is a simple, easy-to-use Python library for interacting with REST APIs using Pydantic's powerful data validation and deserialization tools."
authors = ["CrazyProger1 <[email protected]>"]
license = "MIT"
readme = "README.md"
requires-python = ">=3.7"
license = { text = "MIT" }
dependencies = [
"pydantic",
packages = [
{ include = "resty" },
]

[tool.poetry.dependencies]
python = "^3.12"
pydantic = "^2.5.3"

[tool.poetry.group.dev.dependencies]
mypy = "^1.8.0"
ruff = "^0.1.14"
pytest = "^7.4.4"

[tool.ruff]
exclude = [
".bzr",
Expand Down Expand Up @@ -47,3 +48,8 @@ files = ["resty"]
show_error_codes = true
strict = true



[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added resty/clients/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions resty/clients/httpx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
try:
import httpx
except ImportError:
raise ImportError('You should to install httpx to use httpx rest client')

from .client import RESTClient

__all__ = [
'RESTClient'
]
98 changes: 98 additions & 0 deletions resty/clients/httpx/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import json.decoder
from typing import Container

import httpx

from resty.constants import (
DEFAULT_CODES,
STATUS_ERRORS
)
from resty.types import (
BaseRESTClient,
Request,
Response,
BaseMiddleware,
BaseMiddlewareManager
)
from resty.exceptions import (
HTTPError
)
from resty.middlewares import MiddlewareManager


class RESTClient(BaseRESTClient):

def __init__(self, xclient: httpx.AsyncClient, middleware_manager: BaseMiddlewareManager = None):
self._xclient = xclient
self._middleware_manager = middleware_manager or MiddlewareManager(
default_middlewares=None,
)

@staticmethod
def _parse_xresponse(xresponse: httpx.Response) -> dict | list | None:
try:
data = xresponse.json()
except json.decoder.JSONDecodeError:
data = {}

return data

@staticmethod
def _check_status(status: int, expected_status: int | Container[int], request: Request, url: str):
if status != expected_status:
if isinstance(expected_status, Container) and status in expected_status:
pass
else:
exc: type[HTTPError] = STATUS_ERRORS.get(status, HTTPError)
raise exc(
request=request,
status=status,
url=url
)

def add_middleware(self, middleware: BaseMiddleware):
self._middleware_manager.add_middleware(middleware=middleware)

async def request(self, request: Request, **kwargs) -> Response:
if not isinstance(request, Request):
raise TypeError('request is not of type Request')

expected_status: int = kwargs.pop('expected_status', DEFAULT_CODES.get(request.method))
check_status: bool = kwargs.pop('check_status', True)

if not isinstance(expected_status, (int, Container[int])):
raise TypeError('expected status should be type of int or Container[int]')

await self._middleware_manager.call_pre_middlewares(request=request, **kwargs)

xresponse = await self._xclient.request(
method=request.method.value,
url=request.url,
headers=request.headers,
data=request.data,
params=request.params,
cookies=request.cookies,
follow_redirects=request.redirects,
timeout=request.timeout
)

status = xresponse.status_code

if check_status:
self._check_status(
status=status,
expected_status=expected_status,
request=request,
url=str(xresponse.url)
)

response = Response(
request=request,
status=status,
data=self._parse_xresponse(
xresponse=xresponse
)
)
await self._middleware_manager.call_post_middlewares(response=response, **kwargs)

return response
Loading

0 comments on commit 6d432b9

Please sign in to comment.