rb-commons 0.7.17__py3-none-any.whl → 0.7.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rb_commons/broker/consumer.py +28 -28
- rb_commons/configs/config.py +51 -51
- rb_commons/configs/injections.py +8 -8
- rb_commons/configs/rabbitmq.py +25 -25
- rb_commons/configs/v2/config.py +51 -51
- rb_commons/http/base_api.py +96 -96
- rb_commons/http/consul.py +43 -43
- rb_commons/http/exceptions.py +41 -41
- rb_commons/orm/enum.py +23 -19
- rb_commons/orm/exceptions.py +10 -10
- rb_commons/orm/managers.py +733 -733
- rb_commons/orm/querysets.py +56 -56
- rb_commons/orm/services.py +44 -44
- rb_commons/permissions/role_permissions.py +31 -31
- rb_commons/schemes/jwt.py +66 -66
- rb_commons/schemes/pagination.py +46 -46
- rb_commons/utils/media.py +33 -33
- {rb_commons-0.7.17.dist-info → rb_commons-0.7.19.dist-info}/METADATA +1 -1
- rb_commons-0.7.19.dist-info/RECORD +30 -0
- rb_commons-0.7.17.dist-info/RECORD +0 -30
- {rb_commons-0.7.17.dist-info → rb_commons-0.7.19.dist-info}/WHEEL +0 -0
- {rb_commons-0.7.17.dist-info → rb_commons-0.7.19.dist-info}/top_level.txt +0 -0
rb_commons/broker/consumer.py
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
from aio_pika import ExchangeType, IncomingMessage
|
|
2
|
-
from rb_commons.configs.rabbitmq import RabbitMQConnection
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class BaseRabbitMQConsumer:
|
|
6
|
-
def __init__(self, exchange_name: str, queue_name: str, routing_key: str):
|
|
7
|
-
self.exchange_name = exchange_name
|
|
8
|
-
self.queue_name = queue_name
|
|
9
|
-
self.routing_key = routing_key
|
|
10
|
-
|
|
11
|
-
async def setup(self, channel):
|
|
12
|
-
exchange = await channel.declare_exchange(self.exchange_name, ExchangeType.DIRECT, durable=True)
|
|
13
|
-
queue = await channel.declare_queue(self.queue_name, durable=True)
|
|
14
|
-
await queue.bind(exchange, routing_key=self.routing_key)
|
|
15
|
-
return queue
|
|
16
|
-
|
|
17
|
-
async def consume(self):
|
|
18
|
-
async with RabbitMQConnection.get_channel() as channel:
|
|
19
|
-
queue = await self.setup(channel)
|
|
20
|
-
async with queue.iterator() as queue_iter:
|
|
21
|
-
async for message in queue_iter:
|
|
22
|
-
await self.process_message(message)
|
|
23
|
-
|
|
24
|
-
async def process_message(self, message: IncomingMessage):
|
|
25
|
-
async with message.process():
|
|
26
|
-
await self.handle_message(message.body.decode())
|
|
27
|
-
|
|
28
|
-
async def handle_message(self, body: str):
|
|
1
|
+
from aio_pika import ExchangeType, IncomingMessage
|
|
2
|
+
from rb_commons.configs.rabbitmq import RabbitMQConnection
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseRabbitMQConsumer:
|
|
6
|
+
def __init__(self, exchange_name: str, queue_name: str, routing_key: str):
|
|
7
|
+
self.exchange_name = exchange_name
|
|
8
|
+
self.queue_name = queue_name
|
|
9
|
+
self.routing_key = routing_key
|
|
10
|
+
|
|
11
|
+
async def setup(self, channel):
|
|
12
|
+
exchange = await channel.declare_exchange(self.exchange_name, ExchangeType.DIRECT, durable=True)
|
|
13
|
+
queue = await channel.declare_queue(self.queue_name, durable=True)
|
|
14
|
+
await queue.bind(exchange, routing_key=self.routing_key)
|
|
15
|
+
return queue
|
|
16
|
+
|
|
17
|
+
async def consume(self):
|
|
18
|
+
async with RabbitMQConnection.get_channel() as channel:
|
|
19
|
+
queue = await self.setup(channel)
|
|
20
|
+
async with queue.iterator() as queue_iter:
|
|
21
|
+
async for message in queue_iter:
|
|
22
|
+
await self.process_message(message)
|
|
23
|
+
|
|
24
|
+
async def process_message(self, message: IncomingMessage):
|
|
25
|
+
async with message.process():
|
|
26
|
+
await self.handle_message(message.body.decode())
|
|
27
|
+
|
|
28
|
+
async def handle_message(self, body: str):
|
|
29
29
|
raise NotImplementedError("This method should be overridden by subclasses.")
|
rb_commons/configs/config.py
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
from typing import Optional, Any
|
|
2
|
-
|
|
3
|
-
from pydantic import field_validator
|
|
4
|
-
from pydantic_settings import BaseSettings
|
|
5
|
-
|
|
6
|
-
class CommonConfigs(BaseSettings):
|
|
7
|
-
service_name: Optional[str] = None
|
|
8
|
-
service_port: Optional[int] = None
|
|
9
|
-
service_id: Optional[str] = None
|
|
10
|
-
service_hostname: Optional[Any] = '127.0.0.1'
|
|
11
|
-
service_host: Optional[str] = None
|
|
12
|
-
|
|
13
|
-
# db
|
|
14
|
-
POSTGRES_HOST: Optional[str] = None
|
|
15
|
-
POSTGRES_USER: Optional[str] = None
|
|
16
|
-
POSTGRES_PORT: Optional[int] = None
|
|
17
|
-
POSTGRES_PASSWORD: Optional[str] = None
|
|
18
|
-
POSTGRES_DB: Optional[str] = None
|
|
19
|
-
DB_ALEMBIC_URL: Optional[str] = None
|
|
20
|
-
|
|
21
|
-
# Broker
|
|
22
|
-
|
|
23
|
-
RABBITMQ_URL: Optional[str] = None
|
|
24
|
-
|
|
25
|
-
DIGITALOCEAN_STORAGE_BUCKET_NAME: Optional[str] = None
|
|
26
|
-
DIGITALOCEAN_S3_ENDPOINT_URL: Optional[str] = None
|
|
27
|
-
|
|
28
|
-
@property
|
|
29
|
-
def database_url(self) -> Optional[str]:
|
|
30
|
-
"""Construct the database URL if all required fields are present."""
|
|
31
|
-
required_fields = [
|
|
32
|
-
self.POSTGRES_USER,
|
|
33
|
-
self.POSTGRES_PASSWORD,
|
|
34
|
-
self.POSTGRES_HOST,
|
|
35
|
-
self.POSTGRES_PORT,
|
|
36
|
-
self.POSTGRES_DB
|
|
37
|
-
]
|
|
38
|
-
if all(required_fields):
|
|
39
|
-
return (
|
|
40
|
-
f"postgresql+asyncpg://{self.POSTGRES_USER}:"
|
|
41
|
-
f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:"
|
|
42
|
-
f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
|
43
|
-
)
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
class Config:
|
|
47
|
-
env_file = ".env"
|
|
48
|
-
env_file_encoding = "utf-8"
|
|
49
|
-
extra = "ignore"
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
class CommonConfigs(BaseSettings):
|
|
7
|
+
service_name: Optional[str] = None
|
|
8
|
+
service_port: Optional[int] = None
|
|
9
|
+
service_id: Optional[str] = None
|
|
10
|
+
service_hostname: Optional[Any] = '127.0.0.1'
|
|
11
|
+
service_host: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
# db
|
|
14
|
+
POSTGRES_HOST: Optional[str] = None
|
|
15
|
+
POSTGRES_USER: Optional[str] = None
|
|
16
|
+
POSTGRES_PORT: Optional[int] = None
|
|
17
|
+
POSTGRES_PASSWORD: Optional[str] = None
|
|
18
|
+
POSTGRES_DB: Optional[str] = None
|
|
19
|
+
DB_ALEMBIC_URL: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
# Broker
|
|
22
|
+
|
|
23
|
+
RABBITMQ_URL: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
DIGITALOCEAN_STORAGE_BUCKET_NAME: Optional[str] = None
|
|
26
|
+
DIGITALOCEAN_S3_ENDPOINT_URL: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def database_url(self) -> Optional[str]:
|
|
30
|
+
"""Construct the database URL if all required fields are present."""
|
|
31
|
+
required_fields = [
|
|
32
|
+
self.POSTGRES_USER,
|
|
33
|
+
self.POSTGRES_PASSWORD,
|
|
34
|
+
self.POSTGRES_HOST,
|
|
35
|
+
self.POSTGRES_PORT,
|
|
36
|
+
self.POSTGRES_DB
|
|
37
|
+
]
|
|
38
|
+
if all(required_fields):
|
|
39
|
+
return (
|
|
40
|
+
f"postgresql+asyncpg://{self.POSTGRES_USER}:"
|
|
41
|
+
f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:"
|
|
42
|
+
f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
|
43
|
+
)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
class Config:
|
|
47
|
+
env_file = ".env"
|
|
48
|
+
env_file_encoding = "utf-8"
|
|
49
|
+
extra = "ignore"
|
|
50
|
+
|
|
51
|
+
|
|
52
52
|
configs = CommonConfigs()
|
rb_commons/configs/injections.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from typing import Annotated
|
|
2
|
-
from rb_commons.schemes.jwt import Claims
|
|
3
|
-
from fastapi import Request, Depends
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def get_claims(request: Request) -> Claims:
|
|
7
|
-
return Claims.from_headers(dict(request.headers))
|
|
8
|
-
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from rb_commons.schemes.jwt import Claims
|
|
3
|
+
from fastapi import Request, Depends
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def get_claims(request: Request) -> Claims:
|
|
7
|
+
return Claims.from_headers(dict(request.headers))
|
|
8
|
+
|
|
9
9
|
ClaimsDep = Annotated[Claims, Depends(get_claims)]
|
rb_commons/configs/rabbitmq.py
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
from contextlib import asynccontextmanager
|
|
2
|
-
from aio_pika import connect_robust, Message, ExchangeType
|
|
3
|
-
from rb_commons.configs.config import configs
|
|
4
|
-
|
|
5
|
-
class RabbitMQConnection:
|
|
6
|
-
_connection = None
|
|
7
|
-
_channel = None
|
|
8
|
-
|
|
9
|
-
@classmethod
|
|
10
|
-
@asynccontextmanager
|
|
11
|
-
async def get_channel(cls):
|
|
12
|
-
if not cls._connection:
|
|
13
|
-
cls._connection = await connect_robust(configs.RABBITMQ_URL)
|
|
14
|
-
cls._channel = await cls._connection.channel()
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
yield cls._channel
|
|
18
|
-
finally:
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
@classmethod
|
|
22
|
-
async def close(cls):
|
|
23
|
-
if cls._connection:
|
|
24
|
-
await cls._connection.close()
|
|
25
|
-
cls._connection = None
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from aio_pika import connect_robust, Message, ExchangeType
|
|
3
|
+
from rb_commons.configs.config import configs
|
|
4
|
+
|
|
5
|
+
class RabbitMQConnection:
|
|
6
|
+
_connection = None
|
|
7
|
+
_channel = None
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
@asynccontextmanager
|
|
11
|
+
async def get_channel(cls):
|
|
12
|
+
if not cls._connection:
|
|
13
|
+
cls._connection = await connect_robust(configs.RABBITMQ_URL)
|
|
14
|
+
cls._channel = await cls._connection.channel()
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
yield cls._channel
|
|
18
|
+
finally:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
async def close(cls):
|
|
23
|
+
if cls._connection:
|
|
24
|
+
await cls._connection.close()
|
|
25
|
+
cls._connection = None
|
|
26
26
|
cls._channel = None
|
rb_commons/configs/v2/config.py
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
from typing import Optional, Any
|
|
2
|
-
|
|
3
|
-
from pydantic import field_validator
|
|
4
|
-
from pydantic_settings import BaseSettings
|
|
5
|
-
|
|
6
|
-
class CommonConfigs(BaseSettings):
|
|
7
|
-
service_name: Optional[str] = None
|
|
8
|
-
service_port: Optional[int] = None
|
|
9
|
-
service_id: Optional[str] = None
|
|
10
|
-
service_hostname: Optional[Any] = '127.0.0.1'
|
|
11
|
-
service_host: Optional[str] = None
|
|
12
|
-
|
|
13
|
-
# db
|
|
14
|
-
POSTGRES_HOST: Optional[str] = None
|
|
15
|
-
POSTGRES_USER: Optional[str] = None
|
|
16
|
-
POSTGRES_PORT: Optional[int] = None
|
|
17
|
-
POSTGRES_PASSWORD: Optional[str] = None
|
|
18
|
-
POSTGRES_DB: Optional[str] = None
|
|
19
|
-
DB_ALEMBIC_URL: Optional[str] = None
|
|
20
|
-
|
|
21
|
-
# Broker
|
|
22
|
-
|
|
23
|
-
RABBITMQ_URL: Optional[str] = None
|
|
24
|
-
|
|
25
|
-
DIGITALOCEAN_STORAGE_BUCKET_NAME: Optional[str] = None
|
|
26
|
-
DIGITALOCEAN_S3_ENDPOINT_URL: Optional[str] = None
|
|
27
|
-
|
|
28
|
-
@property
|
|
29
|
-
def database_url(self) -> Optional[str]:
|
|
30
|
-
"""Construct the database URL if all required fields are present."""
|
|
31
|
-
required_fields = [
|
|
32
|
-
self.POSTGRES_USER,
|
|
33
|
-
self.POSTGRES_PASSWORD,
|
|
34
|
-
self.POSTGRES_HOST,
|
|
35
|
-
self.POSTGRES_PORT,
|
|
36
|
-
self.POSTGRES_DB
|
|
37
|
-
]
|
|
38
|
-
if all(required_fields):
|
|
39
|
-
return (
|
|
40
|
-
f"postgresql+asyncpg://{self.POSTGRES_USER}:"
|
|
41
|
-
f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:"
|
|
42
|
-
f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
|
43
|
-
)
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
class Config:
|
|
47
|
-
env_file = "/robosell/.env"
|
|
48
|
-
env_file_encoding = "utf-8"
|
|
49
|
-
extra = "ignore"
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
class CommonConfigs(BaseSettings):
|
|
7
|
+
service_name: Optional[str] = None
|
|
8
|
+
service_port: Optional[int] = None
|
|
9
|
+
service_id: Optional[str] = None
|
|
10
|
+
service_hostname: Optional[Any] = '127.0.0.1'
|
|
11
|
+
service_host: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
# db
|
|
14
|
+
POSTGRES_HOST: Optional[str] = None
|
|
15
|
+
POSTGRES_USER: Optional[str] = None
|
|
16
|
+
POSTGRES_PORT: Optional[int] = None
|
|
17
|
+
POSTGRES_PASSWORD: Optional[str] = None
|
|
18
|
+
POSTGRES_DB: Optional[str] = None
|
|
19
|
+
DB_ALEMBIC_URL: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
# Broker
|
|
22
|
+
|
|
23
|
+
RABBITMQ_URL: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
DIGITALOCEAN_STORAGE_BUCKET_NAME: Optional[str] = None
|
|
26
|
+
DIGITALOCEAN_S3_ENDPOINT_URL: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def database_url(self) -> Optional[str]:
|
|
30
|
+
"""Construct the database URL if all required fields are present."""
|
|
31
|
+
required_fields = [
|
|
32
|
+
self.POSTGRES_USER,
|
|
33
|
+
self.POSTGRES_PASSWORD,
|
|
34
|
+
self.POSTGRES_HOST,
|
|
35
|
+
self.POSTGRES_PORT,
|
|
36
|
+
self.POSTGRES_DB
|
|
37
|
+
]
|
|
38
|
+
if all(required_fields):
|
|
39
|
+
return (
|
|
40
|
+
f"postgresql+asyncpg://{self.POSTGRES_USER}:"
|
|
41
|
+
f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:"
|
|
42
|
+
f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
|
43
|
+
)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
class Config:
|
|
47
|
+
env_file = "/robosell/.env"
|
|
48
|
+
env_file_encoding = "utf-8"
|
|
49
|
+
extra = "ignore"
|
|
50
|
+
|
|
51
|
+
|
|
52
52
|
configs = CommonConfigs()
|
rb_commons/http/base_api.py
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import requests
|
|
3
|
-
from rb_commons.http.exceptions import BadRequestException, InternalException
|
|
4
|
-
from requests import RequestException
|
|
5
|
-
|
|
6
|
-
class BaseAPI:
|
|
7
|
-
def __init__(self, base_url: str):
|
|
8
|
-
self.BASE_URL = base_url
|
|
9
|
-
|
|
10
|
-
def _make_request(self, method: str, path: str, data: dict = None,
|
|
11
|
-
params: dict = None, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
12
|
-
"""
|
|
13
|
-
A general method to make HTTP requests (GET, POST, etc.)
|
|
14
|
-
|
|
15
|
-
This method abstracts the process of making HTTP requests, allowing for
|
|
16
|
-
different types of requests (GET, POST, PUT, DELETE) to be easily performed
|
|
17
|
-
by specifying the method and other parameters. This adheres to the DRY
|
|
18
|
-
(Don't Repeat Yourself) principle by centralizing request handling.
|
|
19
|
-
|
|
20
|
-
:param method: HTTP method to use for the request (e.g., 'GET', 'POST', 'PUT', 'DELETE').
|
|
21
|
-
:param path: The API endpoint path to which the request is made. This should be
|
|
22
|
-
relative to the base URL defined in the class.
|
|
23
|
-
:param data: Optional dictionary containing the data to be sent in the body of
|
|
24
|
-
the request (used for POST and PUT requests). Default is None.
|
|
25
|
-
:param params: Optional dictionary containing query parameters to be included
|
|
26
|
-
in the request URL. Default is None.
|
|
27
|
-
:param headers: Optional dictionary containing any custom headers to be sent
|
|
28
|
-
with the request. Default is None.
|
|
29
|
-
:return: Response object containing the server's response to the HTTP request.
|
|
30
|
-
:raises HttpException: Raises a custom HttpException if the request fails for
|
|
31
|
-
any reason, including invalid HTTP methods, network issues,
|
|
32
|
-
or JSON parsing errors.
|
|
33
|
-
|
|
34
|
-
The method constructs the full URL from the base URL and the provided path.
|
|
35
|
-
It uses a dictionary to map HTTP methods to their respective request functions
|
|
36
|
-
from the requests library. After making the request, it checks the response
|
|
37
|
-
status and attempts to parse the response as JSON.
|
|
38
|
-
"""
|
|
39
|
-
url = self.BASE_URL + path
|
|
40
|
-
|
|
41
|
-
if reset_base_url:
|
|
42
|
-
url = path
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
request_methods = {
|
|
46
|
-
'POST': requests.post,
|
|
47
|
-
'GET': requests.get,
|
|
48
|
-
'PUT': requests.put,
|
|
49
|
-
'DELETE': requests.delete
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if method not in request_methods:
|
|
53
|
-
raise BadRequestException(f"Unsupported HTTP method: {method}")
|
|
54
|
-
|
|
55
|
-
kwargs = {
|
|
56
|
-
'params': params,
|
|
57
|
-
'headers': headers,
|
|
58
|
-
'data' if form_encoded else 'json': data
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
response = request_methods[method](url, **kwargs)
|
|
62
|
-
|
|
63
|
-
try:
|
|
64
|
-
if response.text.strip():
|
|
65
|
-
data = response.json()
|
|
66
|
-
else:
|
|
67
|
-
data = {}
|
|
68
|
-
except ValueError as e:
|
|
69
|
-
error_message = data.get("message") or data.get("detail") or response.text
|
|
70
|
-
raise BadRequestException(f"Invalid JSON response", additional_info={"error_message": error_message})
|
|
71
|
-
if not (200 <= response.status_code < 300):
|
|
72
|
-
error_message = data.get("message") or data.get("detail") or response.text
|
|
73
|
-
raise BadRequestException(f"Unexpected error occured", additional_info={"error_message": error_message})
|
|
74
|
-
|
|
75
|
-
return response
|
|
76
|
-
|
|
77
|
-
except RequestException as e:
|
|
78
|
-
raise BadRequestException(f"Request exception: {str(e)}", additional_info={"error_message": str(e)})
|
|
79
|
-
except BadRequestException:
|
|
80
|
-
raise
|
|
81
|
-
except (json.JSONDecodeError, ValueError) as e:
|
|
82
|
-
raise InternalException(f"Failed to parse JSON: {str(e)}")
|
|
83
|
-
except Exception as e:
|
|
84
|
-
raise InternalException(f"Unhandled error: {str(e)}")
|
|
85
|
-
|
|
86
|
-
def _post(self, path: str, data: dict | list, headers: dict = None, params: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
87
|
-
return self._make_request('POST', path, data=data, headers=headers, params=params, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
|
88
|
-
|
|
89
|
-
def _get(self, path: str, params: dict = None, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
90
|
-
return self._make_request('GET', path, params=params, headers=headers, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
|
91
|
-
|
|
92
|
-
def _put(self, path: str, params: dict = None, data: dict = None, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
93
|
-
return self._make_request('PUT', path, params=params, data=data, headers=headers, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
|
94
|
-
|
|
95
|
-
def _delete(self, path: str, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
96
|
-
return self._make_request('DELETE', path, headers=headers, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from rb_commons.http.exceptions import BadRequestException, InternalException
|
|
4
|
+
from requests import RequestException
|
|
5
|
+
|
|
6
|
+
class BaseAPI:
|
|
7
|
+
def __init__(self, base_url: str):
|
|
8
|
+
self.BASE_URL = base_url
|
|
9
|
+
|
|
10
|
+
def _make_request(self, method: str, path: str, data: dict = None,
|
|
11
|
+
params: dict = None, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
12
|
+
"""
|
|
13
|
+
A general method to make HTTP requests (GET, POST, etc.)
|
|
14
|
+
|
|
15
|
+
This method abstracts the process of making HTTP requests, allowing for
|
|
16
|
+
different types of requests (GET, POST, PUT, DELETE) to be easily performed
|
|
17
|
+
by specifying the method and other parameters. This adheres to the DRY
|
|
18
|
+
(Don't Repeat Yourself) principle by centralizing request handling.
|
|
19
|
+
|
|
20
|
+
:param method: HTTP method to use for the request (e.g., 'GET', 'POST', 'PUT', 'DELETE').
|
|
21
|
+
:param path: The API endpoint path to which the request is made. This should be
|
|
22
|
+
relative to the base URL defined in the class.
|
|
23
|
+
:param data: Optional dictionary containing the data to be sent in the body of
|
|
24
|
+
the request (used for POST and PUT requests). Default is None.
|
|
25
|
+
:param params: Optional dictionary containing query parameters to be included
|
|
26
|
+
in the request URL. Default is None.
|
|
27
|
+
:param headers: Optional dictionary containing any custom headers to be sent
|
|
28
|
+
with the request. Default is None.
|
|
29
|
+
:return: Response object containing the server's response to the HTTP request.
|
|
30
|
+
:raises HttpException: Raises a custom HttpException if the request fails for
|
|
31
|
+
any reason, including invalid HTTP methods, network issues,
|
|
32
|
+
or JSON parsing errors.
|
|
33
|
+
|
|
34
|
+
The method constructs the full URL from the base URL and the provided path.
|
|
35
|
+
It uses a dictionary to map HTTP methods to their respective request functions
|
|
36
|
+
from the requests library. After making the request, it checks the response
|
|
37
|
+
status and attempts to parse the response as JSON.
|
|
38
|
+
"""
|
|
39
|
+
url = self.BASE_URL + path
|
|
40
|
+
|
|
41
|
+
if reset_base_url:
|
|
42
|
+
url = path
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
request_methods = {
|
|
46
|
+
'POST': requests.post,
|
|
47
|
+
'GET': requests.get,
|
|
48
|
+
'PUT': requests.put,
|
|
49
|
+
'DELETE': requests.delete
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if method not in request_methods:
|
|
53
|
+
raise BadRequestException(f"Unsupported HTTP method: {method}")
|
|
54
|
+
|
|
55
|
+
kwargs = {
|
|
56
|
+
'params': params,
|
|
57
|
+
'headers': headers,
|
|
58
|
+
'data' if form_encoded else 'json': data
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
response = request_methods[method](url, **kwargs)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
if response.text.strip():
|
|
65
|
+
data = response.json()
|
|
66
|
+
else:
|
|
67
|
+
data = {}
|
|
68
|
+
except ValueError as e:
|
|
69
|
+
error_message = data.get("message") or data.get("detail") or response.text
|
|
70
|
+
raise BadRequestException(f"Invalid JSON response", additional_info={"error_message": error_message})
|
|
71
|
+
if not (200 <= response.status_code < 300):
|
|
72
|
+
error_message = data.get("message") or data.get("detail") or response.text
|
|
73
|
+
raise BadRequestException(f"Unexpected error occured", additional_info={"error_message": error_message})
|
|
74
|
+
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
except RequestException as e:
|
|
78
|
+
raise BadRequestException(f"Request exception: {str(e)}", additional_info={"error_message": str(e)})
|
|
79
|
+
except BadRequestException:
|
|
80
|
+
raise
|
|
81
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
82
|
+
raise InternalException(f"Failed to parse JSON: {str(e)}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise InternalException(f"Unhandled error: {str(e)}")
|
|
85
|
+
|
|
86
|
+
def _post(self, path: str, data: dict | list, headers: dict = None, params: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
87
|
+
return self._make_request('POST', path, data=data, headers=headers, params=params, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
|
88
|
+
|
|
89
|
+
def _get(self, path: str, params: dict = None, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
90
|
+
return self._make_request('GET', path, params=params, headers=headers, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
|
91
|
+
|
|
92
|
+
def _put(self, path: str, params: dict = None, data: dict = None, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
93
|
+
return self._make_request('PUT', path, params=params, data=data, headers=headers, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
|
94
|
+
|
|
95
|
+
def _delete(self, path: str, headers: dict = None, reset_base_url: bool = False, form_encoded: bool = False) -> requests.Response:
|
|
96
|
+
return self._make_request('DELETE', path, headers=headers, reset_base_url=reset_base_url, form_encoded=form_encoded)
|
rb_commons/http/consul.py
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import aiocache
|
|
2
|
-
import consul
|
|
3
|
-
from fastapi import HTTPException, Request
|
|
4
|
-
from rb_commons.configs.config import configs
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class ServiceDiscovery:
|
|
8
|
-
def __init__(self, host: str, port: int) -> None:
|
|
9
|
-
self.consul_client = consul.Consul(host=host, port=port)
|
|
10
|
-
self.cache = aiocache.Cache(aiocache.SimpleMemoryCache)
|
|
11
|
-
|
|
12
|
-
async def _get_service_instances(self, service_name: str) -> dict:
|
|
13
|
-
"""Get a healthy service instance from Consul"""
|
|
14
|
-
index, services = self.consul_client.health.service(service_name, passing=True)
|
|
15
|
-
|
|
16
|
-
if not services:
|
|
17
|
-
raise HTTPException(status_code=503,
|
|
18
|
-
detail="Service not available")
|
|
19
|
-
return services
|
|
20
|
-
|
|
21
|
-
def _select_instance(self, instances: list, host: str) -> dict:
|
|
22
|
-
"""Select instance using consistent hashing"""
|
|
23
|
-
if not instances:
|
|
24
|
-
raise HTTPException(status_code=503, detail="No healthy instances")
|
|
25
|
-
|
|
26
|
-
key = host
|
|
27
|
-
hash_value = hash(key)
|
|
28
|
-
return instances[hash_value % len(instances)]
|
|
29
|
-
|
|
30
|
-
async def _get_cached_service_instances(self, service_name: str) -> list:
|
|
31
|
-
"""Get service instances with caching"""
|
|
32
|
-
cache_key = f"service:{service_name}"
|
|
33
|
-
instances = await self.cache.get(cache_key)
|
|
34
|
-
if not instances:
|
|
35
|
-
instances = await self._get_service_instances(service_name)
|
|
36
|
-
if instances:
|
|
37
|
-
await self.cache.set(cache_key, instances, ttl=30) # Cache for 30 seconds
|
|
38
|
-
return instances
|
|
39
|
-
|
|
40
|
-
def _build_instance_url(self, instance: dict) -> str:
|
|
41
|
-
"""Build target URL with proper path handling"""
|
|
42
|
-
service_address = instance['Service']['Address'] or instance['Node']['Address']
|
|
43
|
-
service_port = instance['Service']['Port']
|
|
1
|
+
import aiocache
|
|
2
|
+
import consul
|
|
3
|
+
from fastapi import HTTPException, Request
|
|
4
|
+
from rb_commons.configs.config import configs
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ServiceDiscovery:
|
|
8
|
+
def __init__(self, host: str, port: int) -> None:
|
|
9
|
+
self.consul_client = consul.Consul(host=host, port=port)
|
|
10
|
+
self.cache = aiocache.Cache(aiocache.SimpleMemoryCache)
|
|
11
|
+
|
|
12
|
+
async def _get_service_instances(self, service_name: str) -> dict:
|
|
13
|
+
"""Get a healthy service instance from Consul"""
|
|
14
|
+
index, services = self.consul_client.health.service(service_name, passing=True)
|
|
15
|
+
|
|
16
|
+
if not services:
|
|
17
|
+
raise HTTPException(status_code=503,
|
|
18
|
+
detail="Service not available")
|
|
19
|
+
return services
|
|
20
|
+
|
|
21
|
+
def _select_instance(self, instances: list, host: str) -> dict:
|
|
22
|
+
"""Select instance using consistent hashing"""
|
|
23
|
+
if not instances:
|
|
24
|
+
raise HTTPException(status_code=503, detail="No healthy instances")
|
|
25
|
+
|
|
26
|
+
key = host
|
|
27
|
+
hash_value = hash(key)
|
|
28
|
+
return instances[hash_value % len(instances)]
|
|
29
|
+
|
|
30
|
+
async def _get_cached_service_instances(self, service_name: str) -> list:
|
|
31
|
+
"""Get service instances with caching"""
|
|
32
|
+
cache_key = f"service:{service_name}"
|
|
33
|
+
instances = await self.cache.get(cache_key)
|
|
34
|
+
if not instances:
|
|
35
|
+
instances = await self._get_service_instances(service_name)
|
|
36
|
+
if instances:
|
|
37
|
+
await self.cache.set(cache_key, instances, ttl=30) # Cache for 30 seconds
|
|
38
|
+
return instances
|
|
39
|
+
|
|
40
|
+
def _build_instance_url(self, instance: dict) -> str:
|
|
41
|
+
"""Build target URL with proper path handling"""
|
|
42
|
+
service_address = instance['Service']['Address'] or instance['Node']['Address']
|
|
43
|
+
service_port = instance['Service']['Port']
|
|
44
44
|
return f"http://{service_address}:{service_port}"
|