hypern 0.3.11__cp310-cp310-macosx_11_0_arm64.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.
- hypern/__init__.py +24 -0
- hypern/application.py +495 -0
- hypern/args_parser.py +73 -0
- hypern/auth/__init__.py +0 -0
- hypern/auth/authorization.py +2 -0
- hypern/background.py +4 -0
- hypern/caching/__init__.py +6 -0
- hypern/caching/backend.py +31 -0
- hypern/caching/redis_backend.py +201 -0
- hypern/caching/strategies.py +208 -0
- hypern/cli/__init__.py +0 -0
- hypern/cli/commands.py +0 -0
- hypern/config.py +246 -0
- hypern/database/__init__.py +0 -0
- hypern/database/sqlalchemy/__init__.py +4 -0
- hypern/database/sqlalchemy/config.py +66 -0
- hypern/database/sqlalchemy/repository.py +290 -0
- hypern/database/sqlx/__init__.py +36 -0
- hypern/database/sqlx/field.py +246 -0
- hypern/database/sqlx/migrate.py +263 -0
- hypern/database/sqlx/model.py +117 -0
- hypern/database/sqlx/query.py +904 -0
- hypern/datastructures.py +40 -0
- hypern/enum.py +13 -0
- hypern/exceptions/__init__.py +34 -0
- hypern/exceptions/base.py +62 -0
- hypern/exceptions/common.py +12 -0
- hypern/exceptions/errors.py +15 -0
- hypern/exceptions/formatters.py +56 -0
- hypern/exceptions/http.py +76 -0
- hypern/gateway/__init__.py +6 -0
- hypern/gateway/aggregator.py +32 -0
- hypern/gateway/gateway.py +41 -0
- hypern/gateway/proxy.py +60 -0
- hypern/gateway/service.py +52 -0
- hypern/hypern.cpython-310-darwin.so +0 -0
- hypern/hypern.pyi +333 -0
- hypern/i18n/__init__.py +0 -0
- hypern/logging/__init__.py +3 -0
- hypern/logging/logger.py +82 -0
- hypern/middleware/__init__.py +17 -0
- hypern/middleware/base.py +13 -0
- hypern/middleware/cache.py +177 -0
- hypern/middleware/compress.py +78 -0
- hypern/middleware/cors.py +41 -0
- hypern/middleware/i18n.py +1 -0
- hypern/middleware/limit.py +177 -0
- hypern/middleware/security.py +184 -0
- hypern/openapi/__init__.py +5 -0
- hypern/openapi/schemas.py +51 -0
- hypern/openapi/swagger.py +3 -0
- hypern/processpool.py +139 -0
- hypern/py.typed +0 -0
- hypern/reload.py +46 -0
- hypern/response/__init__.py +3 -0
- hypern/response/response.py +142 -0
- hypern/routing/__init__.py +5 -0
- hypern/routing/dispatcher.py +70 -0
- hypern/routing/endpoint.py +30 -0
- hypern/routing/parser.py +98 -0
- hypern/routing/queue.py +175 -0
- hypern/routing/route.py +280 -0
- hypern/scheduler.py +5 -0
- hypern/worker.py +274 -0
- hypern/ws/__init__.py +4 -0
- hypern/ws/channel.py +80 -0
- hypern/ws/heartbeat.py +74 -0
- hypern/ws/room.py +76 -0
- hypern/ws/route.py +26 -0
- hypern-0.3.11.dist-info/METADATA +134 -0
- hypern-0.3.11.dist-info/RECORD +73 -0
- hypern-0.3.11.dist-info/WHEEL +4 -0
- hypern-0.3.11.dist-info/licenses/LICENSE +24 -0
hypern/datastructures.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from enum import Enum
|
3
|
+
from pydantic import BaseModel, AnyUrl
|
4
|
+
|
5
|
+
|
6
|
+
class BaseModelWithConfig(BaseModel):
|
7
|
+
model_config = {"extra": "allow"}
|
8
|
+
|
9
|
+
|
10
|
+
class Contact(BaseModelWithConfig):
|
11
|
+
name: Optional[str] = None
|
12
|
+
url: Optional[AnyUrl] = None
|
13
|
+
email: Optional[str] = None
|
14
|
+
|
15
|
+
|
16
|
+
class License(BaseModelWithConfig):
|
17
|
+
name: str
|
18
|
+
identifier: Optional[str] = None
|
19
|
+
url: Optional[AnyUrl] = None
|
20
|
+
|
21
|
+
|
22
|
+
class Info(BaseModelWithConfig):
|
23
|
+
title: str
|
24
|
+
summary: Optional[str] = None
|
25
|
+
description: Optional[str] = None
|
26
|
+
contact: Optional[Contact] = None
|
27
|
+
license: Optional[License] = None
|
28
|
+
version: str
|
29
|
+
|
30
|
+
|
31
|
+
class HTTPMethod(Enum):
|
32
|
+
GET = "GET"
|
33
|
+
POST = "POST"
|
34
|
+
PUT = "PUT"
|
35
|
+
DELETE = "DELETE"
|
36
|
+
PATCH = "PATCH"
|
37
|
+
OPTIONS = "OPTIONS"
|
38
|
+
HEAD = "HEAD"
|
39
|
+
TRACE = "TRACE"
|
40
|
+
CONNECT = "CONNECT"
|
hypern/enum.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
from enum import Enum
|
3
|
+
|
4
|
+
|
5
|
+
class ErrorCode(Enum):
|
6
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
7
|
+
BAD_REQUEST = "BAD_REQUEST"
|
8
|
+
FORBIDDEN = "FORBIDDEN"
|
9
|
+
SERVER_ERROR = "SERVER_ERROR"
|
10
|
+
NOT_FOUND = "NOT_FOUND"
|
11
|
+
METHOD_NOT_ALLOW = "METHOD_NOT_ALLOW"
|
12
|
+
UNAUTHORIZED = "UNAUTHORIZED"
|
13
|
+
VALIDATION_ERROR = "VALIDATION_ERROR"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from .base import HTTPException, ResponseFormatter, HypernError
|
2
|
+
from .errors import ErrorDefinitions
|
3
|
+
from .formatters import SimpleFormatter, DetailedFormatter, LocalizedFormatter
|
4
|
+
from .http import (
|
5
|
+
BadRequestException,
|
6
|
+
UnauthorizedException,
|
7
|
+
ForbiddenException,
|
8
|
+
NotFoundException,
|
9
|
+
ValidationException,
|
10
|
+
InternalServerException,
|
11
|
+
RateLimitException,
|
12
|
+
)
|
13
|
+
|
14
|
+
from .common import DBFieldValidationError, InvalidPortNumber, OutOfScopeApplicationException
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
"HTTPException",
|
18
|
+
"ResponseFormatter",
|
19
|
+
"HypernError",
|
20
|
+
"ErrorDefinitions",
|
21
|
+
"SimpleFormatter",
|
22
|
+
"DetailedFormatter",
|
23
|
+
"LocalizedFormatter",
|
24
|
+
"BadRequestException",
|
25
|
+
"UnauthorizedException",
|
26
|
+
"ForbiddenException",
|
27
|
+
"NotFoundException",
|
28
|
+
"ValidationException",
|
29
|
+
"InternalServerException",
|
30
|
+
"RateLimitException",
|
31
|
+
"DBFieldValidationError",
|
32
|
+
"InvalidPortNumber",
|
33
|
+
"OutOfScopeApplicationException",
|
34
|
+
]
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import uuid
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from datetime import datetime, timezone
|
4
|
+
from http import HTTPStatus
|
5
|
+
from typing import Any, Dict, Optional
|
6
|
+
|
7
|
+
|
8
|
+
class ResponseFormatter(ABC):
|
9
|
+
@abstractmethod
|
10
|
+
def format_error(self, exception: "HTTPException") -> Dict[str, Any]:
|
11
|
+
"""Format exception into response dictionary"""
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
15
|
+
class DefaultFormatter(ResponseFormatter):
|
16
|
+
def format_error(self, exception: "HTTPException") -> Dict[str, Any]:
|
17
|
+
return {
|
18
|
+
"error": {
|
19
|
+
"code": exception.error.code if exception.error else "UNKNOWN_ERROR",
|
20
|
+
"message": exception.error.message if exception.error else "Unknown error occurred",
|
21
|
+
"details": exception.details or {},
|
22
|
+
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
|
23
|
+
"request_id": str(uuid.uuid4()),
|
24
|
+
},
|
25
|
+
"status": exception.status_code,
|
26
|
+
}
|
27
|
+
|
28
|
+
|
29
|
+
class HypernError:
|
30
|
+
"""Base error definition"""
|
31
|
+
|
32
|
+
def __init__(self, message: str, code: str):
|
33
|
+
self.message = message
|
34
|
+
self.code = code
|
35
|
+
|
36
|
+
|
37
|
+
class HTTPException(Exception):
|
38
|
+
"""Base HTTP exception"""
|
39
|
+
|
40
|
+
_formatter: ResponseFormatter = DefaultFormatter()
|
41
|
+
|
42
|
+
@classmethod
|
43
|
+
def set_formatter(cls, formatter: ResponseFormatter):
|
44
|
+
cls._formatter = formatter
|
45
|
+
|
46
|
+
def __init__(
|
47
|
+
self,
|
48
|
+
status_code: int = HTTPStatus.BAD_REQUEST,
|
49
|
+
error: Optional[HypernError] = None,
|
50
|
+
details: Optional[Dict[str, Any]] = None,
|
51
|
+
headers: Optional[Dict[str, str]] = None,
|
52
|
+
formatter: Optional[ResponseFormatter] = None,
|
53
|
+
):
|
54
|
+
self.status_code = status_code
|
55
|
+
self.error = error
|
56
|
+
self.details = details or {}
|
57
|
+
self.headers = headers or {}
|
58
|
+
self._instance_formatter = formatter
|
59
|
+
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
61
|
+
formatter = self._instance_formatter or self._formatter
|
62
|
+
return formatter.format_error(self)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from .base import HypernError
|
2
|
+
|
3
|
+
|
4
|
+
class ErrorDefinitions:
|
5
|
+
"""Standard error definitions"""
|
6
|
+
|
7
|
+
BAD_REQUEST = HypernError(message="Bad request", code="BAD_REQUEST")
|
8
|
+
UNAUTHORIZED = HypernError(message="Unauthorized access", code="UNAUTHORIZED")
|
9
|
+
FORBIDDEN = HypernError(message="Access forbidden", code="FORBIDDEN")
|
10
|
+
NOT_FOUND = HypernError(message="Resource not found", code="NOT_FOUND")
|
11
|
+
METHOD_NOT_ALLOWED = HypernError(message="Method not allowed", code="METHOD_NOT_ALLOWED")
|
12
|
+
VALIDATION_ERROR = HypernError(message="Validation error", code="VALIDATION_ERROR")
|
13
|
+
INTERNAL_ERROR = HypernError(message="Internal server error", code="INTERNAL_SERVER_ERROR")
|
14
|
+
CONFLICT = HypernError(message="Resource conflict", code="CONFLICT")
|
15
|
+
TOO_MANY_REQUESTS = HypernError(message="Too many requests", code="TOO_MANY_REQUESTS")
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import uuid
|
2
|
+
from datetime import datetime, timezone
|
3
|
+
from typing import Any, Dict
|
4
|
+
|
5
|
+
from .base import HTTPException, ResponseFormatter
|
6
|
+
|
7
|
+
|
8
|
+
class SimpleFormatter(ResponseFormatter):
|
9
|
+
def format_error(self, exception: HTTPException) -> Dict[str, Any]:
|
10
|
+
return {
|
11
|
+
"code": exception.error.code if exception.error else "UNKNOWN_ERROR",
|
12
|
+
"message": exception.error.message if exception.error else "Unknown error occurred",
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
class DetailedFormatter(ResponseFormatter):
|
17
|
+
def format_error(self, exception: HTTPException) -> Dict[str, Any]:
|
18
|
+
return {
|
19
|
+
"status": {"code": exception.status_code, "text": str(exception.status_code)},
|
20
|
+
"error": {
|
21
|
+
"type": exception.error.code if exception.error else "UNKNOWN_ERROR",
|
22
|
+
"message": exception.error.message if exception.error else "Unknown error occurred",
|
23
|
+
"details": exception.details or {},
|
24
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
25
|
+
},
|
26
|
+
"request": {"path": exception.path, "id": str(uuid.uuid4())},
|
27
|
+
}
|
28
|
+
|
29
|
+
|
30
|
+
class LocalizedFormatter(ResponseFormatter):
|
31
|
+
def __init__(self, language: str = "en"):
|
32
|
+
self.language = language
|
33
|
+
self.translations = {
|
34
|
+
"en": {
|
35
|
+
"BAD_REQUEST": "Bad request",
|
36
|
+
"VALIDATION_ERROR": "Validation error",
|
37
|
+
"NOT_FOUND": "Resource not found",
|
38
|
+
# Add more translations
|
39
|
+
},
|
40
|
+
"vi": {
|
41
|
+
"BAD_REQUEST": "Yêu cầu không hợp lệ",
|
42
|
+
"VALIDATION_ERROR": "Lỗi xác thực",
|
43
|
+
"NOT_FOUND": "Không tìm thấy tài nguyên",
|
44
|
+
# Add more translations
|
45
|
+
},
|
46
|
+
}
|
47
|
+
|
48
|
+
def format_error(self, exception: HTTPException) -> Dict[str, Any]:
|
49
|
+
error_code = exception.error.code if exception.error else "UNKNOWN_ERROR"
|
50
|
+
translated_message = self.translations.get(self.language, {}).get(error_code, exception.error.message if exception.error else "Unknown error occurred")
|
51
|
+
|
52
|
+
return {
|
53
|
+
"error": {"code": error_code, "message": translated_message, "details": exception.details or {}},
|
54
|
+
"status": exception.status_code,
|
55
|
+
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
|
56
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
from http import HTTPStatus
|
2
|
+
from typing import Any, Dict, Optional
|
3
|
+
|
4
|
+
from .base import HTTPException, ResponseFormatter, HypernError
|
5
|
+
from .errors import ErrorDefinitions
|
6
|
+
|
7
|
+
|
8
|
+
class BadRequestException(HTTPException):
|
9
|
+
def __init__(
|
10
|
+
self,
|
11
|
+
error: Optional[HypernError] = ErrorDefinitions.BAD_REQUEST,
|
12
|
+
details: Optional[Dict[str, Any]] = None,
|
13
|
+
headers: Optional[Dict[str, str]] = None,
|
14
|
+
formatter: Optional[ResponseFormatter] = None,
|
15
|
+
):
|
16
|
+
super().__init__(status_code=HTTPStatus.BAD_REQUEST, error=error, details=details, headers=headers, formatter=formatter)
|
17
|
+
|
18
|
+
|
19
|
+
class UnauthorizedException(HTTPException):
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
error: Optional[HypernError] = ErrorDefinitions.UNAUTHORIZED,
|
23
|
+
details: Optional[Dict[str, Any]] = None,
|
24
|
+
headers: Optional[Dict[str, str]] = None,
|
25
|
+
formatter: Optional[ResponseFormatter] = None,
|
26
|
+
):
|
27
|
+
super().__init__(status_code=HTTPStatus.UNAUTHORIZED, error=error, details=details, headers=headers, formatter=formatter)
|
28
|
+
|
29
|
+
|
30
|
+
class ForbiddenException(HTTPException):
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
error: Optional[HypernError] = ErrorDefinitions.FORBIDDEN,
|
34
|
+
details: Optional[Dict[str, Any]] = None,
|
35
|
+
headers: Optional[Dict[str, str]] = None,
|
36
|
+
formatter: Optional[ResponseFormatter] = None,
|
37
|
+
):
|
38
|
+
super().__init__(status_code=HTTPStatus.FORBIDDEN, error=error, details=details, headers=headers, formatter=formatter)
|
39
|
+
|
40
|
+
|
41
|
+
class NotFoundException(HTTPException):
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
error: Optional[HypernError] = ErrorDefinitions.NOT_FOUND,
|
45
|
+
details: Optional[Dict[str, Any]] = None,
|
46
|
+
headers: Optional[Dict[str, str]] = None,
|
47
|
+
formatter: Optional[ResponseFormatter] = None,
|
48
|
+
):
|
49
|
+
super().__init__(status_code=HTTPStatus.NOT_FOUND, error=error, details=details, headers=headers, formatter=formatter)
|
50
|
+
|
51
|
+
|
52
|
+
class ValidationException(HTTPException):
|
53
|
+
def __init__(self, details: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, formatter: Optional[ResponseFormatter] = None):
|
54
|
+
super().__init__(status_code=HTTPStatus.BAD_REQUEST, error=ErrorDefinitions.VALIDATION_ERROR, details=details, headers=headers, formatter=formatter)
|
55
|
+
|
56
|
+
|
57
|
+
class InternalServerException(HTTPException):
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
error: Optional[HypernError] = ErrorDefinitions.INTERNAL_ERROR,
|
61
|
+
details: Optional[Dict[str, Any]] = None,
|
62
|
+
headers: Optional[Dict[str, str]] = None,
|
63
|
+
formatter: Optional[ResponseFormatter] = None,
|
64
|
+
):
|
65
|
+
super().__init__(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, error=error, details=details, headers=headers, formatter=formatter)
|
66
|
+
|
67
|
+
|
68
|
+
class RateLimitException(HTTPException):
|
69
|
+
def __init__(self, retry_after: int, details: Optional[Dict[str, Any]] = None, formatter: Optional[ResponseFormatter] = None):
|
70
|
+
super().__init__(
|
71
|
+
status_code=HTTPStatus.TOO_MANY_REQUESTS,
|
72
|
+
error=ErrorDefinitions.TOO_MANY_REQUESTS,
|
73
|
+
details=details,
|
74
|
+
headers={"Retry-After": str(retry_after)},
|
75
|
+
formatter=formatter,
|
76
|
+
)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Any, Dict, List
|
3
|
+
|
4
|
+
from hypern.response import JSONResponse
|
5
|
+
|
6
|
+
from .proxy import Proxy
|
7
|
+
from .service import ServiceRegistry
|
8
|
+
|
9
|
+
|
10
|
+
class Aggregator:
|
11
|
+
def __init__(self, registry: ServiceRegistry, proxy: Proxy):
|
12
|
+
self._registry = registry
|
13
|
+
self._proxy = proxy
|
14
|
+
|
15
|
+
async def aggregate_responses(self, requests: List[Dict[str, Any]]) -> JSONResponse:
|
16
|
+
tasks = []
|
17
|
+
for req in requests:
|
18
|
+
service = self._registry.get_service(req["service"])
|
19
|
+
if service:
|
20
|
+
tasks.append(self._proxy.forward_request(service, req["request"]))
|
21
|
+
|
22
|
+
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
23
|
+
|
24
|
+
aggregated = {}
|
25
|
+
for i, response in enumerate(responses):
|
26
|
+
service_name = requests[i]["service"]
|
27
|
+
if isinstance(response, Exception):
|
28
|
+
aggregated[service_name] = {"status": "error", "error": str(response)}
|
29
|
+
else:
|
30
|
+
aggregated[service_name] = {"status": "success", "data": response.body}
|
31
|
+
|
32
|
+
return JSONResponse(content=aggregated)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
2
|
+
|
3
|
+
from hypern import Hypern
|
4
|
+
from hypern.hypern import Request
|
5
|
+
from hypern.response import JSONResponse
|
6
|
+
|
7
|
+
from .aggregator import Aggregator
|
8
|
+
from .proxy import Proxy
|
9
|
+
from .service import ServiceConfig, ServiceRegistry
|
10
|
+
|
11
|
+
|
12
|
+
class APIGateway:
|
13
|
+
def __init__(self, app: Hypern):
|
14
|
+
self.app = app
|
15
|
+
self.registry = ServiceRegistry()
|
16
|
+
self.proxy = Proxy(self.registry)
|
17
|
+
self.aggregator = Aggregator(self.registry, self.proxy)
|
18
|
+
|
19
|
+
def register_service(self, config: ServiceConfig, metadata: Optional[Dict[str, Any]] = None):
|
20
|
+
"""Register a new service with the gateway"""
|
21
|
+
self.registry.register(config, metadata)
|
22
|
+
|
23
|
+
async def startup(self):
|
24
|
+
"""Initialize the gateway components"""
|
25
|
+
await self.proxy.startup()
|
26
|
+
|
27
|
+
async def shutdown(self):
|
28
|
+
"""Cleanup gateway resources"""
|
29
|
+
await self.proxy.shutdown()
|
30
|
+
|
31
|
+
async def handle_request(self, request: Request) -> Any:
|
32
|
+
"""Main request handler"""
|
33
|
+
service = self.registry.get_service_by_prefix(request.path)
|
34
|
+
if not service:
|
35
|
+
return JSONResponse(content={"error": "Service not found"}, status_code=404)
|
36
|
+
|
37
|
+
return await self.proxy.forward_request(service, request)
|
38
|
+
|
39
|
+
async def aggregate(self, requests: List[Dict[str, Any]]) -> Any:
|
40
|
+
"""Handle aggregated requests"""
|
41
|
+
return await self.aggregator.aggregate_responses(requests)
|
hypern/gateway/proxy.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Any, Dict, Optional
|
3
|
+
|
4
|
+
import aiohttp
|
5
|
+
import orjson
|
6
|
+
import traceback
|
7
|
+
|
8
|
+
from hypern.hypern import Request
|
9
|
+
from hypern.response import JSONResponse
|
10
|
+
|
11
|
+
from .service import ServiceConfig, ServiceRegistry, ServiceStatus
|
12
|
+
|
13
|
+
|
14
|
+
class Proxy:
|
15
|
+
def __init__(self, service_registry: ServiceRegistry):
|
16
|
+
self._registry = service_registry
|
17
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
18
|
+
self._rate_limiters: Dict[str, asyncio.Semaphore] = {}
|
19
|
+
|
20
|
+
async def startup(self):
|
21
|
+
self._session = aiohttp.ClientSession()
|
22
|
+
for service in self._registry._services.values():
|
23
|
+
self._rate_limiters[service.name] = asyncio.Semaphore(100) # Default 100 concurrent requests
|
24
|
+
|
25
|
+
async def shutdown(self):
|
26
|
+
if self._session:
|
27
|
+
await self._session.close()
|
28
|
+
|
29
|
+
async def forward_request(self, service: ServiceConfig, request: Request) -> Any:
|
30
|
+
if not self._session:
|
31
|
+
await self.startup()
|
32
|
+
|
33
|
+
target_path = request.path.replace(service.prefix, "", 1)
|
34
|
+
target_url = f"{service.url}{target_path}"
|
35
|
+
|
36
|
+
headers = request.headers.get_headers()
|
37
|
+
# Remove hop-by-hop headers
|
38
|
+
for header in ["connection", "keep-alive", "transfer-encoding"]:
|
39
|
+
headers.pop(header, None)
|
40
|
+
|
41
|
+
async with self._rate_limiters[service.name]:
|
42
|
+
try:
|
43
|
+
async with self._session.request(
|
44
|
+
method=request.method,
|
45
|
+
url=target_url,
|
46
|
+
headers=headers,
|
47
|
+
params=request.query_params.to_dict(),
|
48
|
+
data=await request.json() if request.method in ["POST", "PUT", "PATCH"] else None,
|
49
|
+
timeout=aiohttp.ClientTimeout(total=service.timeout),
|
50
|
+
) as response:
|
51
|
+
body = await response.read()
|
52
|
+
return JSONResponse(
|
53
|
+
content=orjson.loads(body) if response.content_type == "application/json" else body.decode(),
|
54
|
+
status_code=response.status,
|
55
|
+
headers=dict(response.headers),
|
56
|
+
)
|
57
|
+
except Exception as e:
|
58
|
+
traceback.print_exc()
|
59
|
+
self._registry.update_status(service.name, ServiceStatus.DEGRADED)
|
60
|
+
return JSONResponse(content={"error": "Service unavailable", "details": str(e)}, status_code=503)
|
@@ -0,0 +1,52 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
3
|
+
from typing import Any, Dict, Optional
|
4
|
+
|
5
|
+
|
6
|
+
class ServiceStatus(Enum):
|
7
|
+
ONLINE = "online"
|
8
|
+
OFFLINE = "offline"
|
9
|
+
DEGRADED = "degraded"
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class ServiceConfig:
|
14
|
+
name: str
|
15
|
+
url: str
|
16
|
+
prefix: str
|
17
|
+
timeout: float = 30.0
|
18
|
+
max_retries: int = 3
|
19
|
+
health_check_path: str = "/health"
|
20
|
+
|
21
|
+
|
22
|
+
class ServiceRegistry:
|
23
|
+
def __init__(self):
|
24
|
+
self._services: Dict[str, ServiceConfig] = {}
|
25
|
+
self._status: Dict[str, ServiceStatus] = {}
|
26
|
+
self._metadata: Dict[str, Dict[str, Any]] = {}
|
27
|
+
|
28
|
+
def register(self, service: ServiceConfig, metadata: Optional[Dict[str, Any]] = None):
|
29
|
+
self._services[service.name] = service
|
30
|
+
self._status[service.name] = ServiceStatus.ONLINE
|
31
|
+
self._metadata[service.name] = metadata or {}
|
32
|
+
|
33
|
+
def unregister(self, service_name: str):
|
34
|
+
self._services.pop(service_name, None)
|
35
|
+
self._status.pop(service_name, None)
|
36
|
+
self._metadata.pop(service_name, None)
|
37
|
+
|
38
|
+
def get_service(self, service_name: str) -> Optional[ServiceConfig]:
|
39
|
+
return self._services.get(service_name)
|
40
|
+
|
41
|
+
def get_service_by_prefix(self, path: str) -> Optional[ServiceConfig]:
|
42
|
+
for service in self._services.values():
|
43
|
+
if path.startswith(service.prefix):
|
44
|
+
return service
|
45
|
+
return None
|
46
|
+
|
47
|
+
def update_status(self, service_name: str, status: ServiceStatus):
|
48
|
+
if service_name in self._services:
|
49
|
+
self._status[service_name] = status
|
50
|
+
|
51
|
+
def get_status(self, service_name: str) -> ServiceStatus:
|
52
|
+
return self._status.get(service_name, ServiceStatus.OFFLINE)
|
Binary file
|