backend-utils 0.2.0
backend utils
Requires Python: >=3.10,<4.0

# backend-utils
Useful python tools for backend services


## Documentation

### Tools

* `Singleton` - metaclass to make singletons
```python
from backend_utils.tools import Singleton

class A(metaclass=Singleton):
    pass

print(id(A()) == id(A())) # True
```

* `StrEnum` - subclasses that create variants using `auto()` will have values equal to their names
```python
from enum import auto

from backend_utils.tools import StrEnum

class Bit(StrEnum):
    one = auto()
    two = auto()

print(Bit.one.value) # 'one'
print(Bit.two.value) # 'two'
```

### Server

* `Router`, `compile_routers`, `register_routers` - build routers 
for fastapi app

```python
from fastapi import FastAPI, APIRouter

from backend_utils.server import (
    Router, compile_routers, register_routers
)
router = APIRouter()

routers = [
    Router(router=router, tags=['Users'], prefix='/users'),
]


compiled_routers = compile_routers(
    routers=routers,
    root_prefix='/api/v1'
)

app = FastAPI()
register_routers(
    app=app,
    routers=[*compiled_routers]
)
```
This code will compile routers:
`/api/v1/users/*`

### Http
* `RequestMethods` - enum for http methods
  
* `BaseRequester` - base class for http clients. You can specify `URL`, `TIMEOUT`, `MIDDLEWARE`.
Use `self.make_request()` to make http requests
  
```python
import os
import asyncio
from typing import Optional

import httpx
from backend_utils.http import BaseRequester, RequestMethods


class JsonPlaceholder(BaseRequester):
    URL = os.getenv('JSONPLACEHOLDER_URL', 'https://jsonplaceholder.typicode.com')
    TIMEOUT = 10

    async def get_todo(self, todo_id: int) -> Optional[httpx.Response]:
        return await self.make_request(
            method=RequestMethods.GET,
            uri=f'/todos/{todo_id}',
        )

    
if __name__ == '__main__':
    jp = JsonPlaceholder()
    asyncio.run(jp.get_todo(todo_id=1))
```

You also can validate response with pydantic models
```python
import os
import asyncio
from typing import Optional

from pydantic import BaseModel, Field
from backend_utils.http import BaseRequester, RequestMethods

class GetTodo(BaseModel):
    user_id: int = Field(..., alias='userId')
    id: int
    title: str
    completed: bool

class JsonPlaceholder(BaseRequester):
    async def get_todo_model(self, todo_id: int) -> Optional[GetTodo]:
        url = os.getenv('JSONPLACEHOLDER_URL', 'https://jsonplaceholder.typicode.com')
        return await self.make_request(
            method=RequestMethods.GET,
            url=f'{url}/todos/{todo_id}',
            response_model=GetTodo,
            timeout=5.5
        )

    
if __name__ == '__main__':
    jp = JsonPlaceholder()
    asyncio.run(jp.get_todo_model(todo_id=1))
```
So it will return `GetTodo` model or `None` if request failed

* `BaseMiddleware` - base class for http request middleware.
Available methods:
  - `before_request(self, request: httpx.Request)`
  - `after_request(self, request: httpx.Request, response: httpx.Response):`
  - `on_request_error(self, request: httpx.Request, error: httpx.HTTPError)`
  - `on_request_status_error(self, request: httpx.Request)`
  - `on_validation_error(self, response: httpx.Response, error: [ValidationError, json.decoder.JSONDecodeError])`
    
For example middleware to print request time
```python
import os
import time
import asyncio
from typing import Optional

import httpx
from backend_utils.http import BaseRequester, RequestMethods, BaseMiddleware


class PrintRequestTime(BaseMiddleware):
    async def before_request(self, request: httpx.Request):
        request.start_time = time.perf_counter()

    async def after_request(self, request: httpx.Request, response: httpx.Response):
        request_time = time.perf_counter() - request.start_time
        print(request_time)


class JsonPlaceholder(BaseRequester):
    URL = os.getenv('JSONPLACEHOLDER_URL', 'https://jsonplaceholder.typicode.com')
    MIDDLEWARES = [PrintRequestTime()]
    
    async def get_todo(self, todo_id: int) -> Optional[httpx.Response]:
        return await self.make_request(
            method=RequestMethods.GET,
            uri=f'/todos/{todo_id}',
        )

    
if __name__ == '__main__':
    jp = JsonPlaceholder()
    asyncio.run(jp.get_todo(todo_id=1))
```