hypern 0.3.1__cp312-cp312-win32.whl → 0.3.2__cp312-cp312-win32.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.
@@ -4,13 +4,14 @@ import secrets
4
4
  import time
5
5
  from base64 import b64decode, b64encode
6
6
  from dataclasses import dataclass
7
- from datetime import datetime, timedelta
7
+ from datetime import datetime, timedelta, timezone
8
8
  from typing import Any, Dict, List, Optional
9
9
 
10
10
  import jwt
11
11
 
12
12
  from hypern.exceptions import Forbidden, Unauthorized
13
- from hypern.hypern import Middleware, Request, Response
13
+ from hypern.hypern import Request, Response
14
+ from .base import Middleware, MiddlewareConfig
14
15
 
15
16
 
16
17
  @dataclass
@@ -44,16 +45,16 @@ class SecurityConfig:
44
45
 
45
46
 
46
47
  class SecurityMiddleware(Middleware):
47
- def __init__(self, config: SecurityConfig):
48
- super().__init__()
49
- self.config = config
48
+ def __init__(self, secur_config: SecurityConfig, config: Optional[MiddlewareConfig] = None):
49
+ super().__init__(config)
50
+ self.secur_config = secur_config
50
51
  self._secret_key = secrets.token_bytes(32)
51
52
  self._token_lifetime = 3600
52
53
  self._rate_limit_storage = {}
53
54
 
54
55
  def _rate_limit_check(self, request: Request) -> Optional[Response]:
55
56
  """Check if the request exceeds rate limits"""
56
- if not self.config.rate_limiting:
57
+ if not self.secur_config.rate_limiting:
57
58
  return None
58
59
 
59
60
  client_ip = request.client.host
@@ -74,16 +75,20 @@ class SecurityMiddleware(Middleware):
74
75
 
75
76
  def _generate_jwt_token(self, user_data: Dict[str, Any]) -> str:
76
77
  """Generate a JWT token"""
77
- if not self.config.jwt_secret:
78
+ if not self.secur_config.jwt_secret:
78
79
  raise ValueError("JWT secret key is not configured")
79
80
 
80
- payload = {"user": user_data, "exp": datetime.utcnow() + timedelta(seconds=self.config.jwt_expires_in), "iat": datetime.utcnow()}
81
- return jwt.encode(payload, self.config.jwt_secret, algorithm=self.config.jwt_algorithm)
81
+ payload = {
82
+ "user": user_data,
83
+ "exp": datetime.now(tz=timezone.utc) + timedelta(seconds=self.secur_config.jwt_expires_in),
84
+ "iat": datetime.now(tz=timezone.utc),
85
+ }
86
+ return jwt.encode(payload, self.secur_config.jwt_secret, algorithm=self.secur_config.jwt_algorithm)
82
87
 
83
88
  def _verify_jwt_token(self, token: str) -> Dict[str, Any]:
84
89
  """Verify JWT token and return payload"""
85
90
  try:
86
- payload = jwt.decode(token, self.config.jwt_secret, algorithms=[self.config.jwt_algorithm])
91
+ payload = jwt.decode(token, self.secur_config.jwt_secret, algorithms=[self.secur_config.jwt_algorithm])
87
92
  return payload
88
93
  except jwt.ExpiredSignatureError:
89
94
  raise Unauthorized("Token has expired")
@@ -121,10 +126,10 @@ class SecurityMiddleware(Middleware):
121
126
 
122
127
  def _apply_cors_headers(self, response: Response) -> None:
123
128
  """Apply CORS headers to response"""
124
- if not self.config.cors_configuration:
129
+ if not self.secur_config.cors_configuration:
125
130
  return
126
131
 
127
- cors = self.config.cors_configuration
132
+ cors = self.secur_config.cors_configuration
128
133
  response.headers.update(
129
134
  {
130
135
  "Access-Control-Allow-Origin": ", ".join(cors.allowed_origins),
@@ -137,8 +142,8 @@ class SecurityMiddleware(Middleware):
137
142
 
138
143
  def _apply_security_headers(self, response: Response) -> None:
139
144
  """Apply security headers to response"""
140
- if self.config.security_headers:
141
- response.headers.update(self.config.security_headers)
145
+ if self.secur_config.security_headers:
146
+ response.headers.update(self.secur_config.security_headers)
142
147
 
143
148
  async def before_request(self, request: Request) -> Request | Response:
144
149
  """Process request before handling"""
@@ -147,7 +152,7 @@ class SecurityMiddleware(Middleware):
147
152
  return rate_limit_response
148
153
 
149
154
  # JWT authentication check
150
- if self.config.jwt_auth:
155
+ if self.secur_config.jwt_auth:
151
156
  auth_header = request.headers.get("Authorization")
152
157
  if not auth_header or not auth_header.startswith("Bearer "):
153
158
  raise Unauthorized("Missing or invalid authorization header")
@@ -158,7 +163,7 @@ class SecurityMiddleware(Middleware):
158
163
  return Response(status_code=401, description=str(e))
159
164
 
160
165
  # CSRF protection check
161
- if self.config.csrf_protection and request.method in ["POST", "PUT", "DELETE", "PATCH"]:
166
+ if self.secur_config.csrf_protection and request.method in ["POST", "PUT", "DELETE", "PATCH"]:
162
167
  csrf_token = request.headers.get("X-CSRF-Token")
163
168
  if not csrf_token or not self._validate_csrf_token(csrf_token):
164
169
  raise Forbidden("CSRF token missing or invalid")
hypern/processpool.py CHANGED
@@ -25,11 +25,26 @@ def run_processes(
25
25
  after_request: List[FunctionInfo],
26
26
  response_headers: Dict[str, str],
27
27
  reload: bool = True,
28
+ on_startup: FunctionInfo | None = None,
29
+ on_shutdown: FunctionInfo | None = None,
30
+ auto_compression: bool = False,
28
31
  ) -> List[Process]:
29
32
  socket = SocketHeld(host, port)
30
33
 
31
34
  process_pool = init_processpool(
32
- router, websocket_router, socket, workers, processes, max_blocking_threads, injectables, before_request, after_request, response_headers
35
+ router,
36
+ websocket_router,
37
+ socket,
38
+ workers,
39
+ processes,
40
+ max_blocking_threads,
41
+ injectables,
42
+ before_request,
43
+ after_request,
44
+ response_headers,
45
+ on_startup,
46
+ on_shutdown,
47
+ auto_compression,
33
48
  )
34
49
 
35
50
  def terminating_signal_handler(_sig, _frame):
@@ -79,6 +94,9 @@ def init_processpool(
79
94
  before_request: List[FunctionInfo],
80
95
  after_request: List[FunctionInfo],
81
96
  response_headers: Dict[str, str],
97
+ on_startup: FunctionInfo | None = None,
98
+ on_shutdown: FunctionInfo | None = None,
99
+ auto_compression: bool = False,
82
100
  ) -> List[Process]:
83
101
  process_pool = []
84
102
 
@@ -86,7 +104,20 @@ def init_processpool(
86
104
  copied_socket = socket.try_clone()
87
105
  process = Process(
88
106
  target=spawn_process,
89
- args=(router, websocket_router, copied_socket, workers, max_blocking_threads, injectables, before_request, after_request, response_headers),
107
+ args=(
108
+ router,
109
+ websocket_router,
110
+ copied_socket,
111
+ workers,
112
+ max_blocking_threads,
113
+ injectables,
114
+ before_request,
115
+ after_request,
116
+ response_headers,
117
+ on_startup,
118
+ on_shutdown,
119
+ auto_compression,
120
+ ),
90
121
  )
91
122
  process.start()
92
123
  process_pool.append(process)
@@ -118,6 +149,9 @@ def spawn_process(
118
149
  before_request: List[FunctionInfo],
119
150
  after_request: List[FunctionInfo],
120
151
  response_headers: Dict[str, str],
152
+ on_startup: FunctionInfo | None = None,
153
+ on_shutdown: FunctionInfo | None = None,
154
+ auto_compression: bool = False,
121
155
  ):
122
156
  loop = initialize_event_loop()
123
157
 
@@ -128,7 +162,12 @@ def spawn_process(
128
162
  server.set_before_hooks(hooks=before_request)
129
163
  server.set_after_hooks(hooks=after_request)
130
164
  server.set_response_headers(headers=response_headers)
165
+ server.set_auto_compression(enabled=auto_compression)
131
166
 
167
+ if on_startup:
168
+ server.set_startup_handler(on_startup)
169
+ if on_shutdown:
170
+ server.set_shutdown_handler(on_shutdown)
132
171
  try:
133
172
  server.start(socket, workers, max_blocking_threads)
134
173
  loop = asyncio.get_event_loop()
@@ -1,4 +1,5 @@
1
1
  from .route import Route
2
2
  from .endpoint import HTTPEndpoint
3
+ from .queue import QueuedHTTPEndpoint
3
4
 
4
- __all__ = ["Route", "HTTPEndpoint"]
5
+ __all__ = ["Route", "HTTPEndpoint", "QueuedHTTPEndpoint"]
@@ -0,0 +1,175 @@
1
+ import asyncio
2
+ import time
3
+ from contextlib import asynccontextmanager
4
+ from dataclasses import dataclass, field
5
+ from queue import PriorityQueue
6
+ from typing import Any, Dict
7
+
8
+ from hypern.hypern import Request, Response
9
+ from hypern.response import JSONResponse
10
+ from hypern.routing import HTTPEndpoint
11
+ from hypern.logging import logger
12
+
13
+
14
+ @dataclass(order=True)
15
+ class PrioritizedRequest:
16
+ priority: int
17
+ timestamp: float = field(default_factory=time.time)
18
+ request: Request | None = field(default=None, compare=False)
19
+ future: asyncio.Future = field(compare=False, default_factory=asyncio.Future)
20
+
21
+
22
+ class QueuedHTTPEndpoint(HTTPEndpoint):
23
+ """
24
+ HTTPEndpoint with request queuing capabilities for high-load scenarios.
25
+ """
26
+
27
+ def __init__(self, *args, **kwargs):
28
+ super().__init__(*args, **kwargs)
29
+ # Queue configuration
30
+ self._max_concurrent = kwargs.get("max_concurrent", 100)
31
+ self._queue_size = kwargs.get("queue_size", 1000)
32
+ self._request_timeout = kwargs.get("request_timeout", 30)
33
+
34
+ # Initialize queuing system
35
+ self._request_queue: PriorityQueue = PriorityQueue(maxsize=self._queue_size)
36
+ self._active_requests = 0
37
+ self._lock = None # Will be initialized when needed
38
+ self._request_semaphore = None # Will be initialized when needed
39
+ self._shutdown = False
40
+ self._queue_task = None
41
+ self._initialized = False
42
+
43
+ # Metrics
44
+ self._metrics = {"processed_requests": 0, "queued_requests": 0, "rejected_requests": 0, "avg_wait_time": 0.0}
45
+
46
+ self._fully_message = "Request queue is full"
47
+
48
+ async def _initialize(self):
49
+ """Initialize async components when first request arrives"""
50
+ if not self._initialized:
51
+ self._lock = asyncio.Lock()
52
+ self._request_semaphore = asyncio.Semaphore(self._max_concurrent)
53
+ self._queue_task = asyncio.create_task(self._process_queue())
54
+ self._initialized = True
55
+
56
+ @asynccontextmanager
57
+ async def _queue_context(self, request: Request, priority: int = 10):
58
+ """Context manager for handling request queuing."""
59
+ if self._shutdown:
60
+ raise RuntimeError("Endpoint is shutting down")
61
+
62
+ await self._initialize() # Ensure async components are initialized
63
+
64
+ request_future = asyncio.Future()
65
+ prioritized_request = PrioritizedRequest(priority=priority, timestamp=time.time(), request=request, future=request_future)
66
+
67
+ try:
68
+ if self._request_queue.qsize() >= self._queue_size:
69
+ self._metrics["rejected_requests"] += 1
70
+ raise asyncio.QueueFull(self._fully_message)
71
+
72
+ await self._enqueue_request(prioritized_request)
73
+ yield await asyncio.wait_for(request_future, timeout=self._request_timeout)
74
+
75
+ except asyncio.TimeoutError:
76
+ self._metrics["rejected_requests"] += 1
77
+ raise asyncio.TimeoutError("Request timed out while waiting in queue")
78
+ finally:
79
+ if not request_future.done():
80
+ request_future.cancel()
81
+
82
+ async def _enqueue_request(self, request: PrioritizedRequest):
83
+ """Add request to the queue."""
84
+ try:
85
+ self._request_queue.put_nowait(request)
86
+ self._metrics["queued_requests"] += 1
87
+ except asyncio.QueueFull:
88
+ self._metrics["rejected_requests"] += 1
89
+ raise asyncio.QueueFull(self._fully_message)
90
+
91
+ async def _process_queue(self):
92
+ """Background task to process queued requests."""
93
+ while not self._shutdown:
94
+ try:
95
+ if not self._request_queue.empty():
96
+ async with self._lock:
97
+ if self._active_requests >= self._max_concurrent:
98
+ await asyncio.sleep(0.1)
99
+ continue
100
+
101
+ request = self._request_queue.get_nowait()
102
+ wait_time = time.time() - request.timestamp
103
+ self._metrics["avg_wait_time"] = (self._metrics["avg_wait_time"] * self._metrics["processed_requests"] + wait_time) / (
104
+ self._metrics["processed_requests"] + 1
105
+ )
106
+
107
+ if not request.future.cancelled():
108
+ self._active_requests += 1
109
+ asyncio.create_task(self._handle_request(request))
110
+
111
+ await asyncio.sleep(0.01)
112
+ except Exception as e:
113
+ logger.error(f"Error processing queue: {e}")
114
+ await asyncio.sleep(1)
115
+
116
+ async def _handle_request(self, request: PrioritizedRequest):
117
+ """Handle individual request."""
118
+ try:
119
+ async with self._request_semaphore:
120
+ response = await super().dispatch(request.request, {})
121
+ if not request.future.done():
122
+ request.future.set_result(response)
123
+ except Exception as e:
124
+ if not request.future.done():
125
+ request.future.set_exception(e)
126
+ finally:
127
+ self._active_requests -= 1
128
+ self._metrics["processed_requests"] += 1
129
+ self._metrics["queued_requests"] -= 1
130
+
131
+ async def dispatch(self, request: Request, inject: Dict[str, Any]) -> Response:
132
+ """
133
+ Enhanced dispatch method with request queuing.
134
+ """
135
+ try:
136
+ priority = self._get_request_priority(request)
137
+
138
+ async with self._queue_context(request, priority) as response:
139
+ return response
140
+
141
+ except asyncio.QueueFull:
142
+ return JSONResponse(description={"error": "Server too busy", "message": self._fully_message, "retry_after": 5}, status_code=503)
143
+ except asyncio.TimeoutError:
144
+ return JSONResponse(
145
+ description={
146
+ "error": "Request timeout",
147
+ "message": "Request timed out while waiting in queue",
148
+ },
149
+ status_code=504,
150
+ )
151
+ except Exception as e:
152
+ return JSONResponse(description={"error": "Internal server error", "message": str(e)}, status_code=500)
153
+
154
+ def _get_request_priority(self, request: Request) -> int:
155
+ """
156
+ Determine request priority. Override this method to implement
157
+ custom priority logic.
158
+ """
159
+ if request.method == "GET":
160
+ return 5
161
+ return 10
162
+
163
+ async def shutdown(self):
164
+ """Gracefully shutdown the endpoint."""
165
+ self._shutdown = True
166
+ if self._queue_task and not self._queue_task.done():
167
+ self._queue_task.cancel()
168
+ try:
169
+ await self._queue_task
170
+ except asyncio.CancelledError:
171
+ pass
172
+
173
+ def get_metrics(self) -> Dict[str, Any]:
174
+ """Get current queue metrics."""
175
+ return {**self._metrics, "current_queue_size": self._request_queue.qsize(), "active_requests": self._active_requests}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypern
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -1,20 +1,15 @@
1
- hypern-0.3.1.dist-info/METADATA,sha256=dAKnEF83pJsKjgE50V8YZOD-_dTDlcrM3yc4Gtn5_IE,3754
2
- hypern-0.3.1.dist-info/WHEEL,sha256=SK_cql1gpDHx6aBV-LOSvGbTt4TUC8AJJOzjOP2tdpI,92
3
- hypern-0.3.1.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
4
- hypern/application.py,sha256=D8tye5rpOJzHGQHUelaDpjUL3B9xO40B7xAL9wC3Uno,14328
5
- hypern/args_parser.py,sha256=Crxzr8_uhiIk_AWJvuwJTEfRqEBqU_GfTbg6chg_YiY,1790
1
+ hypern-0.3.2.dist-info/METADATA,sha256=xbMB8V67yK1aLvRGbYrsjqoScFdSOZXs97zXg0yVVQ4,3754
2
+ hypern-0.3.2.dist-info/WHEEL,sha256=SK_cql1gpDHx6aBV-LOSvGbTt4TUC8AJJOzjOP2tdpI,92
3
+ hypern-0.3.2.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
4
+ hypern/application.py,sha256=yNlohWdSG0lpKfuTVX7N9gI3cmXl3IoEvP8WLH-yAiE,16251
5
+ hypern/args_parser.py,sha256=diz3Oq1PDNMBlG7ElqK01iokY5w_X3U1Ky1jikPVXRg,2020
6
6
  hypern/auth/authorization.py,sha256=-NprZsI0np889ZN1fp-MiVFrPoMNzUtatBJaCMtkllM,32
7
7
  hypern/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  hypern/background.py,sha256=xy38nQZSJsYFRXr3-uFJeNW9E1GiXXOC7lSe5pC0eyE,124
9
- hypern/caching/base/backend.py,sha256=nHl5_nH95LSYWBS9RmOvOzsps_p19HcnQsb4h4W7cP8,68
10
- hypern/caching/base/key_maker.py,sha256=-W1r3ywfQ-K6saniiK3aTMaW3iy3aXai2pvQqM8f74I,232
11
- hypern/caching/base/__init__.py,sha256=M-B56YGTkSwCkKW_I6GcV1LSIFfxZ6KuXowB1_aJQpQ,155
12
- hypern/caching/cache_manager.py,sha256=TF6UosJ54950JgsrPVZUS3MH2R8zafAu5PTryNJ0sRs,2053
13
- hypern/caching/cache_tag.py,sha256=bZcjivMNETAzAHAIobuLN0S2wHgPgLLL8Gg4uso_qbk,267
14
- hypern/caching/custom_key_maker.py,sha256=88RIIJjpQYFnv857wOlCKgWWBbK_S23zNHsIrJz_4PY,394
15
- hypern/caching/redis_backend.py,sha256=IgQToCnHYGpKEErq2CNZkR5woo01z456Ef3C-XRPRV8,70
16
- hypern/caching/strategies.py,sha256=S7qJiJOTJrk6KUFDrmjGkHP3XV8mYTRM3Sd4pA4UQKA,4199
17
- hypern/caching/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ hypern/caching/backend.py,sha256=bfSEgJQxS3FDdj08CY4V9b9U3rwf4uy9gS46QmwA3Zc,829
10
+ hypern/caching/redis_backend.py,sha256=SWYlJvr8qi2kS_grhxhqQBLEEATpFremT2XyXkio8h0,5968
11
+ hypern/caching/strategies.py,sha256=qQjqgZLUX7KZjhwPD4SYUaMRewgCgsp6qa1FunK3Y0I,7288
12
+ hypern/caching/__init__.py,sha256=ODO7zMm4iFG8wcvrhKmukryG5wOTW0DnzFvNMfF57Cc,352
18
13
  hypern/cli/commands.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
14
  hypern/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
15
  hypern/config.py,sha256=frZSdXBI8GaM0tkw1Rs-XydZ9-XjGLRPj6DL4d51-y4,4930
@@ -39,20 +34,27 @@ hypern/db/sql/__init__.py,sha256=1UoWQi2CIcUAbQj3FadR-8V0o_b286nI2wYvOsvtbFc,647
39
34
  hypern/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
35
  hypern/enum.py,sha256=KcVziJj7vWvyie0r2rtxhrLzdtkZAsf0DY58oJ4tQl4,360
41
36
  hypern/exceptions.py,sha256=nHTkF0YdNBMKfSiNtjRMHMNKoY3RMUm68YYluuW15us,2428
42
- hypern/hypern.pyi,sha256=DhTRcW9c0qjVS976fx__insErvhk4k7W0NKcV6iZGxw,8038
37
+ hypern/gateway/aggregator.py,sha256=N1onAp9gdzpCR-E5VubkVoUjjEmVNxG8gDZx9rhnbXc,1132
38
+ hypern/gateway/gateway.py,sha256=26K2qvJUR-0JnN4IlhwvSSt7EYcpYrBVDuzZ1ivQQ34,1475
39
+ hypern/gateway/proxy.py,sha256=w1wcTplDnVrfjn7hb0M0yBVth5TGl88irF-MUYHysQQ,2463
40
+ hypern/gateway/service.py,sha256=PkRaM08olqM_j_4wRjEJCR8X8ZysAF2WOcfhWjaX2eo,1701
41
+ hypern/gateway/__init__.py,sha256=TpFWtqnJerW1-jCWq5fjypJcw9Y6ytyrkvkzby1Eg0E,235
42
+ hypern/hypern.pyi,sha256=GiUjtY6tbV8d6KI1PDTTZK5zWyOD4hooahALnFd7Fl4,8197
43
43
  hypern/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  hypern/logging/logger.py,sha256=WACam_IJiCMXX0hGVKMGSxUQpY4DgAXy7M1dD3q-Z9s,3256
45
45
  hypern/logging/__init__.py,sha256=6eVriyncsJ4J73fGYhoejv9MX7aGTkRezTpPxO4DX1I,52
46
- hypern/middleware/base.py,sha256=mJoz-i-7lqw1eDxZ8Bb0t8sz60lx5TE-OjZT4UR75e4,541
47
- hypern/middleware/cors.py,sha256=x90DnCOJSfp4ojm1krttn_EdtlqeDazyUzVg66NES4A,1681
46
+ hypern/middleware/base.py,sha256=3G7vxFXr0tDp28Bz3NeM76eo03qYGppm07N3yWoA_4U,369
47
+ hypern/middleware/cache.py,sha256=HFfghpnhS21DOsXLn6nt-TgRLCNNm6ZqTKPMgoXM0Mw,7583
48
+ hypern/middleware/compress.py,sha256=Zph3pQz15YrYB4dMUMbQnfWIFY8ovysgPMepbY_WV9k,3119
49
+ hypern/middleware/cors.py,sha256=pt5HyTd3J5L9Lvczo2xI8fxLmtntSbJq-CPU0vYXoAI,1800
48
50
  hypern/middleware/i18n.py,sha256=jHzVzjTx1nnjbraZtIVOprrnSaeKMxZB8RuSqRp2I4s,16
49
- hypern/middleware/limit.py,sha256=8hzUxu_mxra2QiDjAghgZtvwN6Dx07irPUiL12dbVhY,8152
50
- hypern/middleware/security.py,sha256=hIbmP1fkZ_KgxK1Wx7s9xdNH7M174mo6ySASiuzMz_A,7273
51
- hypern/middleware/__init__.py,sha256=lXwR3fdmpVK4Z7QWaLsgf3Sazy5NPPFXIOxIEv1xDC8,273
51
+ hypern/middleware/limit.py,sha256=eAYARPjqxq8Ue0TCpnxlVRB5hv7hwBF0PxeD-bG6Sl0,8252
52
+ hypern/middleware/security.py,sha256=d9Qf2UNMN8wz-MLnG2wRb0Vgf55_IGZAje5hbc2T_HQ,7539
53
+ hypern/middleware/__init__.py,sha256=V-Gnv-Jf-14BVuA28z7PN7GBVQ9BBiBdab6-QnTPCfY,493
52
54
  hypern/openapi/schemas.py,sha256=YHfMlPUeP5DzDX5ao3YH8p_25Vvyaf616dh6XDCUZRc,1677
53
55
  hypern/openapi/swagger.py,sha256=naqUY3rFAEYA1ZLIlmDsMYaol0yIm6TVebdkFa5cMTc,64
54
56
  hypern/openapi/__init__.py,sha256=4rEVD8pa0kdSpsy7ZkJ5JY0Z2XF0NGSKDMwYAd7YZpE,141
55
- hypern/processpool.py,sha256=YW7Dg33Hla9D33x3Mf8xsjaTprHaovkLPK-4XnQKiGU,4045
57
+ hypern/processpool.py,sha256=l6_PKRMFQ4GIMPLTMOEbIwWDsxER0NIj_wdxVWqA7zA,5042
56
58
  hypern/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
59
  hypern/reload.py,sha256=nfaZCoChrQetHNtIqN4Xzi-a0v-irxSCMhwCK3bCEq0,1569
58
60
  hypern/response/response.py,sha256=-dnboAraPic8asf503PxwmDuxhNllUO5h97_DGmbER4,4582
@@ -60,8 +62,9 @@ hypern/response/__init__.py,sha256=_w3u3TDNuYx5ejnnN1unqnTY8NlBgUATQi6wepEB_FQ,2
60
62
  hypern/routing/dispatcher.py,sha256=i2wLAAW1ZXgpi5K2heGXhTODnP1WdQzaR5WlUjs1o9c,2368
61
63
  hypern/routing/endpoint.py,sha256=RKVhvqOEGL9IKBXQ3KJgPi9bgJj9gfWC5BdZc5U_atc,1026
62
64
  hypern/routing/parser.py,sha256=R-4lcN9Ha1iMeAjlqDe8HwkjjMVG-c-ubQLZyWKXj6M,3554
65
+ hypern/routing/queue.py,sha256=NtFBbogU22ddyyX-CuQMip1XFDPZdMCVMIeUCQ-CR6Y,7176
63
66
  hypern/routing/route.py,sha256=IUnWU5ra-0R9rrRDpxJiwiw7vaEefn-We2dZ4EocJGw,10403
64
- hypern/routing/__init__.py,sha256=7rw7EAxougCXtmkgJjrmLP3N5RXctIpI_3JmG9FcKVU,101
67
+ hypern/routing/__init__.py,sha256=U4xW5fDRsn03z4cVLT4dJHHGGU6SVxyv2DL86LXodeE,162
65
68
  hypern/scheduler.py,sha256=-k3tW2AGCnHYSthKXk-FOs_SCtWp3yIxQzwzUJMJsbo,67
66
69
  hypern/security.py,sha256=3E86Yp_eOSVa1emUvBrDgoF0Sn6eNX0CfLnt87w5CPI,1773
67
70
  hypern/worker.py,sha256=WQrhY_awR6zjMwY4Q7izXi4E4fFrDqt7jIblUW8Bzcg,924
@@ -72,5 +75,5 @@ hypern/ws/route.py,sha256=fGQ2RC708MPOiiIHPUo8aZ-oK379TTAyQYm4htNA5jM,803
72
75
  hypern/ws/__init__.py,sha256=dhRoRY683_rfPfSPM5qUczfTuyYDeuLOCFxY4hIdKt8,131
73
76
  hypern/ws.py,sha256=F6SA2Z1KVnqTEX8ssvOXqCtudUS4eo30JsiIsvfbHnE,394
74
77
  hypern/__init__.py,sha256=9Ww_aUQ0vJls0tOq7Yw1_TVOCRsa5bHJ-RtnSeComwk,119
75
- hypern/hypern.cp312-win32.pyd,sha256=GClLmHIIv0CKb5uHz_ochUZKY_1wketwqHGB9TUdRPg,5482496
76
- hypern-0.3.1.dist-info/RECORD,,
78
+ hypern/hypern.cp312-win32.pyd,sha256=JxNOpTLC3EaBfc6d3TLY05BbTHHx5ciyj3wSbOKbWIM,6539264
79
+ hypern-0.3.2.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from .backend import BaseBackend
3
- from .key_maker import BaseKeyMaker
4
-
5
- __all__ = [
6
- "BaseKeyMaker",
7
- "BaseBackend",
8
- ]
@@ -1,3 +0,0 @@
1
- from hypern.hypern import BaseBackend
2
-
3
- __all__ = ["BaseBackend"]
@@ -1,8 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from abc import ABC, abstractmethod
3
- from typing import Callable
4
-
5
-
6
- class BaseKeyMaker(ABC):
7
- @abstractmethod
8
- async def make(self, function: Callable, prefix: str, identify_key: str) -> str: ...
@@ -1,56 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from functools import wraps
3
- from typing import Callable, Dict, Type
4
-
5
- from .base import BaseBackend, BaseKeyMaker
6
- from .cache_tag import CacheTag
7
- import orjson
8
-
9
-
10
- class CacheManager:
11
- def __init__(self):
12
- self.backend = None
13
- self.key_maker = None
14
-
15
- def init(self, backend: BaseBackend, key_maker: BaseKeyMaker) -> None:
16
- self.backend = backend
17
- self.key_maker = key_maker
18
-
19
- def cached(self, tag: CacheTag, ttl: int = 60, identify: Dict = {}) -> Type[Callable]:
20
- def _cached(function):
21
- @wraps(function)
22
- async def __cached(*args, **kwargs):
23
- if not self.backend or not self.key_maker:
24
- raise ValueError("Backend or KeyMaker not initialized")
25
-
26
- _identify_key = []
27
- for key, values in identify.items():
28
- _obj = kwargs.get(key, None)
29
- if not _obj:
30
- raise ValueError(f"Caching: Identify key {key} not found in kwargs")
31
- for attr in values:
32
- _identify_key.append(f"{attr}={getattr(_obj, attr)}")
33
- _identify_key = ":".join(_identify_key)
34
-
35
- key = await self.key_maker.make(function=function, prefix=tag.value, identify_key=_identify_key)
36
-
37
- cached_response = self.backend.get(key=key)
38
- if cached_response:
39
- return orjson.loads(cached_response)
40
-
41
- response = await function(*args, **kwargs)
42
- self.backend.set(response=orjson.dumps(response).decode("utf-8"), key=key, ttl=ttl)
43
- return response
44
-
45
- return __cached
46
-
47
- return _cached # type: ignore
48
-
49
- async def remove_by_tag(self, tag: CacheTag) -> None:
50
- await self.backend.delete_startswith(value=tag.value)
51
-
52
- async def remove_by_prefix(self, prefix: str) -> None:
53
- await self.backend.delete_startswith(value=prefix)
54
-
55
-
56
- Cache = CacheManager()
@@ -1,10 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from enum import Enum
3
-
4
-
5
- class CacheTag(Enum):
6
- GET_HEALTH_CHECK = "get_health_check"
7
- GET_USER_INFO = "get_user_info"
8
- GET_CATEGORIES = "get_categories"
9
- GET_HISTORY = "get_chat_history"
10
- GET_QUESTION = "get_question"
@@ -1,11 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from typing import Callable
3
- import inspect
4
-
5
- from hypern.caching.base import BaseKeyMaker
6
-
7
-
8
- class CustomKeyMaker(BaseKeyMaker):
9
- async def make(self, function: Callable, prefix: str, identify_key: str = "") -> str:
10
- path = f"{prefix}:{inspect.getmodule(function).__name__}.{function.__name__}:{identify_key}" # type: ignore
11
- return str(path)
File without changes