hypern 0.3.11__cp310-cp310-musllinux_1_2_armv7l.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.
Files changed (74) hide show
  1. hypern/__init__.py +24 -0
  2. hypern/application.py +495 -0
  3. hypern/args_parser.py +73 -0
  4. hypern/auth/__init__.py +0 -0
  5. hypern/auth/authorization.py +2 -0
  6. hypern/background.py +4 -0
  7. hypern/caching/__init__.py +6 -0
  8. hypern/caching/backend.py +31 -0
  9. hypern/caching/redis_backend.py +201 -0
  10. hypern/caching/strategies.py +208 -0
  11. hypern/cli/__init__.py +0 -0
  12. hypern/cli/commands.py +0 -0
  13. hypern/config.py +246 -0
  14. hypern/database/__init__.py +0 -0
  15. hypern/database/sqlalchemy/__init__.py +4 -0
  16. hypern/database/sqlalchemy/config.py +66 -0
  17. hypern/database/sqlalchemy/repository.py +290 -0
  18. hypern/database/sqlx/__init__.py +36 -0
  19. hypern/database/sqlx/field.py +246 -0
  20. hypern/database/sqlx/migrate.py +263 -0
  21. hypern/database/sqlx/model.py +117 -0
  22. hypern/database/sqlx/query.py +904 -0
  23. hypern/datastructures.py +40 -0
  24. hypern/enum.py +13 -0
  25. hypern/exceptions/__init__.py +34 -0
  26. hypern/exceptions/base.py +62 -0
  27. hypern/exceptions/common.py +12 -0
  28. hypern/exceptions/errors.py +15 -0
  29. hypern/exceptions/formatters.py +56 -0
  30. hypern/exceptions/http.py +76 -0
  31. hypern/gateway/__init__.py +6 -0
  32. hypern/gateway/aggregator.py +32 -0
  33. hypern/gateway/gateway.py +41 -0
  34. hypern/gateway/proxy.py +60 -0
  35. hypern/gateway/service.py +52 -0
  36. hypern/hypern.cpython-310-arm-linux-gnueabihf.so +0 -0
  37. hypern/hypern.pyi +333 -0
  38. hypern/i18n/__init__.py +0 -0
  39. hypern/logging/__init__.py +3 -0
  40. hypern/logging/logger.py +82 -0
  41. hypern/middleware/__init__.py +17 -0
  42. hypern/middleware/base.py +13 -0
  43. hypern/middleware/cache.py +177 -0
  44. hypern/middleware/compress.py +78 -0
  45. hypern/middleware/cors.py +41 -0
  46. hypern/middleware/i18n.py +1 -0
  47. hypern/middleware/limit.py +177 -0
  48. hypern/middleware/security.py +184 -0
  49. hypern/openapi/__init__.py +5 -0
  50. hypern/openapi/schemas.py +51 -0
  51. hypern/openapi/swagger.py +3 -0
  52. hypern/processpool.py +139 -0
  53. hypern/py.typed +0 -0
  54. hypern/reload.py +46 -0
  55. hypern/response/__init__.py +3 -0
  56. hypern/response/response.py +142 -0
  57. hypern/routing/__init__.py +5 -0
  58. hypern/routing/dispatcher.py +70 -0
  59. hypern/routing/endpoint.py +30 -0
  60. hypern/routing/parser.py +98 -0
  61. hypern/routing/queue.py +175 -0
  62. hypern/routing/route.py +280 -0
  63. hypern/scheduler.py +5 -0
  64. hypern/worker.py +274 -0
  65. hypern/ws/__init__.py +4 -0
  66. hypern/ws/channel.py +80 -0
  67. hypern/ws/heartbeat.py +74 -0
  68. hypern/ws/room.py +76 -0
  69. hypern/ws/route.py +26 -0
  70. hypern-0.3.11.dist-info/METADATA +134 -0
  71. hypern-0.3.11.dist-info/RECORD +74 -0
  72. hypern-0.3.11.dist-info/WHEEL +4 -0
  73. hypern-0.3.11.dist-info/licenses/LICENSE +24 -0
  74. hypern.libs/libgcc_s-5b5488a6.so.1 +0 -0
@@ -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,12 @@
1
+ class InvalidPortNumber(Exception):
2
+ pass
3
+
4
+
5
+ class OutOfScopeApplicationException(Exception):
6
+ pass
7
+
8
+
9
+ class DBFieldValidationError(ValueError):
10
+ """Custom exception for field validation errors."""
11
+
12
+ pass
@@ -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,6 @@
1
+ from .gateway import APIGateway
2
+ from .aggregator import Aggregator
3
+ from .proxy import Proxy
4
+ from .service import ServiceConfig, ServiceRegistry
5
+
6
+ __all__ = ["APIGateway", "Aggregator", "Proxy", "ServiceConfig", "ServiceRegistry"]
@@ -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)
@@ -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)