hypern 0.3.1__cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl → 0.3.3__cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.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 (52) hide show
  1. hypern/application.py +115 -16
  2. hypern/args_parser.py +34 -1
  3. hypern/caching/__init__.py +6 -0
  4. hypern/caching/backend.py +31 -0
  5. hypern/caching/redis_backend.py +200 -2
  6. hypern/caching/strategies.py +164 -71
  7. hypern/config.py +97 -0
  8. hypern/db/addons/__init__.py +5 -0
  9. hypern/db/addons/sqlalchemy/__init__.py +71 -0
  10. hypern/db/sql/__init__.py +13 -179
  11. hypern/db/sql/field.py +606 -0
  12. hypern/db/sql/model.py +116 -0
  13. hypern/db/sql/query.py +879 -0
  14. hypern/exceptions.py +10 -0
  15. hypern/gateway/__init__.py +6 -0
  16. hypern/gateway/aggregator.py +32 -0
  17. hypern/gateway/gateway.py +41 -0
  18. hypern/gateway/proxy.py +60 -0
  19. hypern/gateway/service.py +52 -0
  20. hypern/hypern.cpython-311-i386-linux-gnu.so +0 -0
  21. hypern/hypern.pyi +53 -18
  22. hypern/middleware/__init__.py +14 -2
  23. hypern/middleware/base.py +9 -14
  24. hypern/middleware/cache.py +177 -0
  25. hypern/middleware/compress.py +78 -0
  26. hypern/middleware/cors.py +6 -3
  27. hypern/middleware/limit.py +5 -4
  28. hypern/middleware/security.py +21 -16
  29. hypern/processpool.py +16 -32
  30. hypern/routing/__init__.py +2 -1
  31. hypern/routing/dispatcher.py +4 -0
  32. hypern/routing/queue.py +175 -0
  33. {hypern-0.3.1.dist-info → hypern-0.3.3.dist-info}/METADATA +3 -1
  34. hypern-0.3.3.dist-info/RECORD +84 -0
  35. hypern/caching/base/__init__.py +0 -8
  36. hypern/caching/base/backend.py +0 -3
  37. hypern/caching/base/key_maker.py +0 -8
  38. hypern/caching/cache_manager.py +0 -56
  39. hypern/caching/cache_tag.py +0 -10
  40. hypern/caching/custom_key_maker.py +0 -11
  41. hypern-0.3.1.dist-info/RECORD +0 -76
  42. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/__init__.py +0 -0
  43. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/color.py +0 -0
  44. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/daterange.py +0 -0
  45. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/datetime.py +0 -0
  46. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/encrypted.py +0 -0
  47. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/password.py +0 -0
  48. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/ts_vector.py +0 -0
  49. /hypern/db/{sql/addons → addons/sqlalchemy/fields}/unicode.py +0 -0
  50. /hypern/db/{sql → addons/sqlalchemy}/repository.py +0 -0
  51. {hypern-0.3.1.dist-info → hypern-0.3.3.dist-info}/WHEEL +0 -0
  52. {hypern-0.3.1.dist-info → hypern-0.3.3.dist-info}/licenses/LICENSE +0 -0
hypern/application.py CHANGED
@@ -2,25 +2,52 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
5
+ from dataclasses import dataclass
5
6
  from typing import Any, Callable, List, TypeVar
6
7
 
7
8
  import orjson
9
+ import psutil
8
10
  from typing_extensions import Annotated, Doc
9
11
 
12
+ from hypern.args_parser import ArgsConfig
10
13
  from hypern.datastructures import Contact, HTTPMethod, Info, License
11
- from hypern.hypern import FunctionInfo, Router, Route as InternalRoute, WebsocketRouter
14
+ from hypern.hypern import DatabaseConfig, FunctionInfo, MiddlewareConfig, Router, Server, WebsocketRouter
15
+ from hypern.hypern import Route as InternalRoute
16
+ from hypern.logging import logger
17
+ from hypern.middleware import Middleware
12
18
  from hypern.openapi import SchemaGenerator, SwaggerUI
13
19
  from hypern.processpool import run_processes
14
20
  from hypern.response import HTMLResponse, JSONResponse
15
21
  from hypern.routing import Route
16
22
  from hypern.scheduler import Scheduler
17
- from hypern.middleware import Middleware
18
- from hypern.args_parser import ArgsConfig
19
23
  from hypern.ws import WebsocketRoute
20
24
 
21
25
  AppType = TypeVar("AppType", bound="Hypern")
22
26
 
23
27
 
28
+ @dataclass
29
+ class ThreadConfig:
30
+ workers: int
31
+ max_blocking_threads: int
32
+
33
+
34
+ class ThreadConfigurator:
35
+ def __init__(self):
36
+ self._cpu_count = psutil.cpu_count(logical=True)
37
+ self._memory_gb = psutil.virtual_memory().total / (1024**3)
38
+
39
+ def get_config(self, concurrent_requests: int = None) -> ThreadConfig:
40
+ """Calculate optimal thread configuration based on system resources."""
41
+ workers = max(2, self._cpu_count)
42
+
43
+ if concurrent_requests:
44
+ max_blocking = min(max(32, concurrent_requests * 2), workers * 4, int(self._memory_gb * 8))
45
+ else:
46
+ max_blocking = min(workers * 4, int(self._memory_gb * 8), 256)
47
+
48
+ return ThreadConfig(workers=workers, max_blocking_threads=max_blocking)
49
+
50
+
24
51
  class Hypern:
25
52
  def __init__(
26
53
  self: AppType,
@@ -190,6 +217,22 @@ class Hypern:
190
217
  """
191
218
  ),
192
219
  ] = None,
220
+ auto_compression: Annotated[
221
+ bool,
222
+ Doc(
223
+ """
224
+ Enable automatic compression of responses.
225
+ """
226
+ ),
227
+ ] = False,
228
+ database_config: Annotated[
229
+ DatabaseConfig | None,
230
+ Doc(
231
+ """
232
+ The database configuration for the application.
233
+ """
234
+ ),
235
+ ] = None,
193
236
  *args: Any,
194
237
  **kwargs: Any,
195
238
  ) -> None:
@@ -202,6 +245,11 @@ class Hypern:
202
245
  self.middleware_after_request = []
203
246
  self.response_headers = {}
204
247
  self.args = ArgsConfig()
248
+ self.start_up_handler = None
249
+ self.shutdown_handler = None
250
+ self.auto_compression = auto_compression
251
+ self.database_config = database_config
252
+ self.thread_config = ThreadConfigurator().get_config()
205
253
 
206
254
  for route in routes or []:
207
255
  self.router.extend_route(route(app=self).routes)
@@ -287,10 +335,12 @@ class Hypern:
287
335
  function: The decorator function that registers the middleware.
288
336
  """
289
337
 
338
+ logger.warning("This functin will be deprecated in version 0.4.0. Please use the middleware class instead.")
339
+
290
340
  def decorator(func):
291
341
  is_async = asyncio.iscoroutinefunction(func)
292
342
  func_info = FunctionInfo(handler=func, is_async=is_async)
293
- self.middleware_before_request.append(func_info)
343
+ self.middleware_before_request.append((func_info, MiddlewareConfig.default()))
294
344
  return func
295
345
 
296
346
  return decorator
@@ -306,11 +356,12 @@ class Hypern:
306
356
  Returns:
307
357
  function: The decorator function that registers the given function.
308
358
  """
359
+ logger.warning("This functin will be deprecated in version 0.4.0. Please use the middleware class instead.")
309
360
 
310
361
  def decorator(func):
311
362
  is_async = asyncio.iscoroutinefunction(func)
312
363
  func_info = FunctionInfo(handler=func, is_async=is_async)
313
- self.middleware_after_request.append(func_info)
364
+ self.middleware_after_request.append((func_info, MiddlewareConfig.default()))
314
365
  return func
315
366
 
316
367
  return decorator
@@ -346,11 +397,22 @@ class Hypern:
346
397
  before_request = getattr(middleware, "before_request", None)
347
398
  after_request = getattr(middleware, "after_request", None)
348
399
 
349
- if before_request:
350
- self.before_request()(before_request)
351
- if after_request:
352
- self.after_request()(after_request)
353
- return self
400
+ is_async = asyncio.iscoroutinefunction(before_request)
401
+ before_request = FunctionInfo(handler=before_request, is_async=is_async)
402
+ self.middleware_before_request.append((before_request, middleware.config))
403
+
404
+ is_async = asyncio.iscoroutinefunction(after_request)
405
+ after_request = FunctionInfo(handler=after_request, is_async=is_async)
406
+ self.middleware_after_request.append((after_request, middleware.config))
407
+
408
+ def set_database_config(self, config: DatabaseConfig):
409
+ """
410
+ Sets the database configuration for the application.
411
+
412
+ Args:
413
+ config (DatabaseConfig): The database configuration to be set.
414
+ """
415
+ self.database_config = config
354
416
 
355
417
  def start(
356
418
  self,
@@ -364,18 +426,36 @@ class Hypern:
364
426
  if self.scheduler:
365
427
  self.scheduler.start()
366
428
 
429
+ server = Server()
430
+ server.set_router(router=self.router)
431
+ server.set_websocket_router(websocket_router=self.websocket_router)
432
+ server.set_injected(injected=self.injectables)
433
+ server.set_before_hooks(hooks=self.middleware_before_request)
434
+ server.set_after_hooks(hooks=self.middleware_after_request)
435
+ server.set_response_headers(headers=self.response_headers)
436
+ server.set_auto_compression(enabled=self.auto_compression)
437
+ server.set_mem_pool_capacity(min_capacity=self.args.min_capacity, max_capacity=self.args.max_capacity)
438
+
439
+ server.optimize_routes()
440
+
441
+ if self.database_config:
442
+ server.set_database_config(config=self.database_config)
443
+ if self.start_up_handler:
444
+ server.set_startup_handler(self.start_up_handler)
445
+ if self.shutdown_handler:
446
+ server.set_shutdown_handler(self.shutdown_handler)
447
+
448
+ if self.args.auto_workers:
449
+ self.args.workers = self.thread_config.workers
450
+ self.args.max_blocking_threads = self.thread_config.max_blocking_threads
451
+
367
452
  run_processes(
453
+ server=server,
368
454
  host=self.args.host,
369
455
  port=self.args.port,
370
456
  workers=self.args.workers,
371
457
  processes=self.args.processes,
372
458
  max_blocking_threads=self.args.max_blocking_threads,
373
- router=self.router,
374
- websocket_router=self.websocket_router,
375
- injectables=self.injectables,
376
- before_request=self.middleware_before_request,
377
- after_request=self.middleware_after_request,
378
- response_headers=self.response_headers,
379
459
  reload=self.args.reload,
380
460
  )
381
461
 
@@ -403,3 +483,22 @@ class Hypern:
403
483
  """
404
484
  for route in ws_route.routes:
405
485
  self.websocket_router.add_route(route=route)
486
+
487
+ def on_startup(self, handler: Callable[..., Any]):
488
+ """
489
+ Registers a function to be executed on application startup.
490
+
491
+ Args:
492
+ handler (Callable[..., Any]): The function to be executed on application startup.
493
+ """
494
+ # decorator
495
+ self.start_up_handler = FunctionInfo(handler=handler, is_async=asyncio.iscoroutinefunction(handler))
496
+
497
+ def on_shutdown(self, handler: Callable[..., Any]):
498
+ """
499
+ Registers a function to be executed on application shutdown.
500
+
501
+ Args:
502
+ handler (Callable[..., Any]): The function to be executed on application shutdown.
503
+ """
504
+ self.shutdown_handler = FunctionInfo(handler=handler, is_async=asyncio.iscoroutinefunction(handler))
hypern/args_parser.py CHANGED
@@ -49,11 +49,44 @@ class ArgsConfig:
49
49
  action="store_true",
50
50
  help="It restarts the server based on file changes.",
51
51
  )
52
+
53
+ parser.add_argument(
54
+ "--auto-compression",
55
+ action="store_true",
56
+ help="It compresses the response automatically.",
57
+ )
58
+
59
+ parser.add_argument(
60
+ "--auto-workers",
61
+ action="store_true",
62
+ help="It sets the number of workers and max-blocking-threads automatically.",
63
+ )
64
+
65
+ parser.add_argument(
66
+ "--min-capacity",
67
+ type=int,
68
+ default=1,
69
+ required=False,
70
+ help="Choose the minimum memory pool capacity. [Default: 1]",
71
+ )
72
+
73
+ parser.add_argument(
74
+ "--max-capacity",
75
+ type=int,
76
+ default=100,
77
+ required=False,
78
+ help="Choose the maximum memory pool capacity. [Default: 100]",
79
+ )
80
+
52
81
  args, _ = parser.parse_known_args()
53
82
 
54
83
  self.host = args.host or "127.0.0.1"
55
84
  self.port = args.port or 5000
56
- self.max_blocking_threads = args.max_blocking_threads or 100
85
+ self.max_blocking_threads = args.max_blocking_threads or 32
57
86
  self.processes = args.processes or 1
58
87
  self.workers = args.workers or 1
59
88
  self.reload = args.reload or False
89
+ self.auto_compression = args.auto_compression
90
+ self.auto_workers = args.auto_workers
91
+ self.min_capacity = args.min_capacity
92
+ self.max_capacity = args.max_capacity
@@ -0,0 +1,6 @@
1
+ from .backend import BaseBackend
2
+ from .redis_backend import RedisBackend
3
+
4
+ from .strategies import CacheAsideStrategy, CacheEntry, CacheStrategy, StaleWhileRevalidateStrategy, cache_with_strategy
5
+
6
+ __all__ = ["BaseBackend", "RedisBackend", "CacheAsideStrategy", "CacheEntry", "CacheStrategy", "StaleWhileRevalidateStrategy", "cache_with_strategy"]
@@ -0,0 +1,31 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Optional
3
+
4
+
5
+ class BaseBackend(ABC):
6
+ @abstractmethod
7
+ async def get(self, key: str) -> Any: ...
8
+
9
+ @abstractmethod
10
+ async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: ...
11
+
12
+ @abstractmethod
13
+ async def delete_pattern(self, pattern: str) -> None: ...
14
+
15
+ @abstractmethod
16
+ async def delete(self, key: str) -> None: ...
17
+
18
+ @abstractmethod
19
+ async def exists(self, key: str) -> bool: ...
20
+
21
+ @abstractmethod
22
+ async def set_nx(self, key: str, value: Any, ttl: Optional[int] = None) -> bool: ...
23
+
24
+ @abstractmethod
25
+ async def ttl(self, key: str) -> int: ...
26
+
27
+ @abstractmethod
28
+ async def incr(self, key: str) -> int: ...
29
+
30
+ @abstractmethod
31
+ async def clear(self) -> None: ...
@@ -1,3 +1,201 @@
1
- from hypern.hypern import RedisBackend
1
+ # src/hypern/cache/backends/redis.py
2
+ import pickle
3
+ from typing import Any, Optional
2
4
 
3
- __all__ = ["RedisBackend"]
5
+ from redis import asyncio as aioredis
6
+
7
+ from hypern.logging import logger
8
+
9
+ from .backend import BaseBackend
10
+
11
+
12
+ class RedisBackend(BaseBackend):
13
+ def __init__(self, url: str = "redis://localhost:6379", encoding: str = "utf-8", decode_responses: bool = False, **kwargs):
14
+ """
15
+ Initialize Redis backend with aioredis
16
+
17
+ Args:
18
+ url: Redis connection URL
19
+ encoding: Character encoding to use
20
+ decode_responses: Whether to decode response bytes to strings
21
+ **kwargs: Additional arguments passed to aioredis.from_url
22
+ """
23
+ self.redis = aioredis.from_url(url, encoding=encoding, decode_responses=decode_responses, **kwargs)
24
+ self._encoding = encoding
25
+
26
+ async def get(self, key: str) -> Optional[Any]:
27
+ """
28
+ Get value from Redis
29
+
30
+ Args:
31
+ key: Cache key
32
+
33
+ Returns:
34
+ Deserialized Python object or None if key doesn't exist
35
+ """
36
+ try:
37
+ value = await self.redis.get(key)
38
+ if value is not None:
39
+ return pickle.loads(value)
40
+ return None
41
+ except Exception as e:
42
+ logger.error(f"Error getting cache key {key}: {e}")
43
+ return None
44
+
45
+ async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
46
+ """
47
+ Set value in Redis with optional TTL
48
+
49
+ Args:
50
+ key: Cache key
51
+ value: Python object to store
52
+ ttl: Time to live in seconds
53
+
54
+ Returns:
55
+ bool: True if successful, False otherwise
56
+ """
57
+ try:
58
+ serialized = pickle.dumps(value)
59
+ if ttl is not None:
60
+ await self.redis.setex(key, ttl, serialized)
61
+ else:
62
+ await self.redis.set(key, serialized)
63
+ return True
64
+ except Exception as e:
65
+ logger.error(f"Error setting cache key {key}: {e}")
66
+ return False
67
+
68
+ async def delete(self, key: str) -> bool:
69
+ """
70
+ Delete key from Redis
71
+
72
+ Args:
73
+ key: Cache key to delete
74
+
75
+ Returns:
76
+ bool: True if key was deleted, False otherwise
77
+ """
78
+ try:
79
+ return bool(await self.redis.delete(key))
80
+ except Exception as e:
81
+ logger.error(f"Error deleting cache key {key}: {e}")
82
+ return False
83
+
84
+ async def delete_pattern(self, pattern: str) -> int:
85
+ """
86
+ Delete all keys matching pattern
87
+
88
+ Args:
89
+ pattern: Redis key pattern to match
90
+
91
+ Returns:
92
+ int: Number of keys deleted
93
+ """
94
+ try:
95
+ keys = await self.redis.keys(pattern)
96
+ if keys:
97
+ return await self.redis.delete(*keys)
98
+ return 0
99
+ except Exception as e:
100
+ logger.error(f"Error deleting keys matching {pattern}: {e}")
101
+ return 0
102
+
103
+ async def exists(self, key: str) -> bool:
104
+ """
105
+ Check if key exists
106
+
107
+ Args:
108
+ key: Cache key to check
109
+
110
+ Returns:
111
+ bool: True if key exists, False otherwise
112
+ """
113
+ try:
114
+ return bool(await self.redis.exists(key))
115
+ except Exception as e:
116
+ logger.error(f"Error checking existence of key {key}: {e}")
117
+ return False
118
+
119
+ async def ttl(self, key: str) -> int:
120
+ """
121
+ Get TTL of key in seconds
122
+
123
+ Args:
124
+ key: Cache key
125
+
126
+ Returns:
127
+ int: TTL in seconds, -2 if key doesn't exist, -1 if key has no TTL
128
+ """
129
+ try:
130
+ return await self.redis.ttl(key)
131
+ except Exception as e:
132
+ logger.error(f"Error getting TTL for key {key}: {e}")
133
+ return -2
134
+
135
+ async def incr(self, key: str, amount: int = 1) -> Optional[int]:
136
+ """
137
+ Increment value by amount
138
+
139
+ Args:
140
+ key: Cache key
141
+ amount: Amount to increment by
142
+
143
+ Returns:
144
+ int: New value after increment or None on error
145
+ """
146
+ try:
147
+ return await self.redis.incrby(key, amount)
148
+ except Exception as e:
149
+ logger.error(f"Error incrementing key {key}: {e}")
150
+ return None
151
+
152
+ async def set_nx(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
153
+ """
154
+ Set key only if it doesn't exist (SET NX operation)
155
+
156
+ Args:
157
+ key: Cache key
158
+ value: Value to set
159
+ ttl: Optional TTL in seconds
160
+
161
+ Returns:
162
+ bool: True if key was set, False otherwise
163
+ """
164
+ try:
165
+ serialized = pickle.dumps(value)
166
+ if ttl is not None:
167
+ return await self.redis.set(key, serialized, nx=True, ex=ttl)
168
+ return await self.redis.set(key, serialized, nx=True)
169
+ except Exception as e:
170
+ logger.error(f"Error setting NX for key {key}: {e}")
171
+ return False
172
+
173
+ async def clear(self) -> bool:
174
+ """
175
+ Clear all keys from the current database
176
+
177
+ Returns:
178
+ bool: True if successful, False otherwise
179
+ """
180
+ try:
181
+ await self.redis.flushdb()
182
+ return True
183
+ except Exception as e:
184
+ logger.error(f"Error clearing cache: {e}")
185
+ return False
186
+
187
+ async def close(self) -> None:
188
+ """Close Redis connection"""
189
+ await self.redis.close()
190
+
191
+ async def ping(self) -> bool:
192
+ """
193
+ Check Redis connection
194
+
195
+ Returns:
196
+ bool: True if connection is alive, False otherwise
197
+ """
198
+ try:
199
+ return await self.redis.ping()
200
+ except Exception:
201
+ return False