hypern 0.3.7__cp312-cp312-win32.whl → 0.3.9__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.
- hypern/__init__.py +21 -1
- hypern/application.py +29 -36
- hypern/args_parser.py +4 -23
- hypern/database/sqlalchemy/__init__.py +4 -0
- hypern/database/{addons/sqlalchemy/__init__.py → sqlalchemy/config.py} +0 -5
- hypern/database/{sql → sqlx}/query.py +1 -1
- hypern/datastructures.py +2 -2
- hypern/hypern.cp312-win32.pyd +0 -0
- hypern/hypern.pyi +3 -5
- hypern/worker.py +265 -21
- {hypern-0.3.7.dist-info → hypern-0.3.9.dist-info}/METADATA +16 -14
- {hypern-0.3.7.dist-info → hypern-0.3.9.dist-info}/RECORD +19 -36
- {hypern-0.3.7.dist-info → hypern-0.3.9.dist-info}/WHEEL +1 -1
- hypern/database/addons/__init__.py +0 -5
- hypern/database/addons/sqlalchemy/fields/__init__.py +0 -14
- hypern/database/addons/sqlalchemy/fields/color.py +0 -16
- hypern/database/addons/sqlalchemy/fields/daterange.py +0 -23
- hypern/database/addons/sqlalchemy/fields/datetime.py +0 -22
- hypern/database/addons/sqlalchemy/fields/encrypted.py +0 -58
- hypern/database/addons/sqlalchemy/fields/password.py +0 -171
- hypern/database/addons/sqlalchemy/fields/ts_vector.py +0 -46
- hypern/database/addons/sqlalchemy/fields/unicode.py +0 -15
- hypern/database/nosql/__init__.py +0 -25
- hypern/database/nosql/addons/__init__.py +0 -4
- hypern/database/nosql/addons/color.py +0 -16
- hypern/database/nosql/addons/daterange.py +0 -30
- hypern/database/nosql/addons/encrypted.py +0 -53
- hypern/database/nosql/addons/password.py +0 -134
- hypern/database/nosql/addons/unicode.py +0 -10
- hypern/security.py +0 -44
- hypern/ws.py +0 -16
- /hypern/database/{addons/sqlalchemy → sqlalchemy}/repository.py +0 -0
- /hypern/database/{sql → sqlx}/__init__.py +0 -0
- /hypern/database/{sql → sqlx}/field.py +0 -0
- /hypern/database/{sql → sqlx}/migrate.py +0 -0
- /hypern/database/{sql → sqlx}/model.py +0 -0
- {hypern-0.3.7.dist-info → hypern-0.3.9.dist-info}/licenses/LICENSE +0 -0
hypern/__init__.py
CHANGED
@@ -1,4 +1,24 @@
|
|
1
|
+
from hypern.logging import logger
|
2
|
+
from hypern.routing import HTTPEndpoint, QueuedHTTPEndpoint, Route
|
3
|
+
from hypern.ws import WebsocketRoute, WebSocketSession
|
4
|
+
|
1
5
|
from .application import Hypern
|
2
6
|
from .hypern import Request, Response
|
7
|
+
from .response import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse
|
3
8
|
|
4
|
-
__all__ = [
|
9
|
+
__all__ = [
|
10
|
+
"Hypern",
|
11
|
+
"Request",
|
12
|
+
"Response",
|
13
|
+
"Route",
|
14
|
+
"HTTPEndpoint",
|
15
|
+
"QueuedHTTPEndpoint",
|
16
|
+
"WebsocketRoute",
|
17
|
+
"WebSocketSession",
|
18
|
+
"FileResponse",
|
19
|
+
"HTMLResponse",
|
20
|
+
"JSONResponse",
|
21
|
+
"PlainTextResponse",
|
22
|
+
"RedirectResponse",
|
23
|
+
"logger",
|
24
|
+
]
|
hypern/application.py
CHANGED
@@ -83,6 +83,14 @@ class Hypern:
|
|
83
83
|
"""
|
84
84
|
),
|
85
85
|
] = None,
|
86
|
+
dependencies: Annotated[
|
87
|
+
dict[str, Any] | None,
|
88
|
+
Doc(
|
89
|
+
"""
|
90
|
+
A dictionary of global dependencies that can be accessed by all routes.
|
91
|
+
"""
|
92
|
+
),
|
93
|
+
] = None,
|
86
94
|
title: Annotated[
|
87
95
|
str,
|
88
96
|
Doc(
|
@@ -209,22 +217,6 @@ class Hypern:
|
|
209
217
|
"""
|
210
218
|
),
|
211
219
|
] = None,
|
212
|
-
default_injectables: Annotated[
|
213
|
-
dict[str, Any] | None,
|
214
|
-
Doc(
|
215
|
-
"""
|
216
|
-
A dictionary of default injectables to be passed to all routes.
|
217
|
-
"""
|
218
|
-
),
|
219
|
-
] = None,
|
220
|
-
auto_compression: Annotated[
|
221
|
-
bool,
|
222
|
-
Doc(
|
223
|
-
"""
|
224
|
-
Enable automatic compression of responses.
|
225
|
-
"""
|
226
|
-
),
|
227
|
-
] = False,
|
228
220
|
database_config: Annotated[
|
229
221
|
DatabaseConfig | None,
|
230
222
|
Doc(
|
@@ -239,15 +231,14 @@ class Hypern:
|
|
239
231
|
super().__init__(*args, **kwargs)
|
240
232
|
self.router = Router(path="/")
|
241
233
|
self.websocket_router = WebsocketRouter(path="/")
|
234
|
+
self.dependencies = dependencies or {}
|
242
235
|
self.scheduler = scheduler
|
243
|
-
self.injectables = default_injectables or {}
|
244
236
|
self.middleware_before_request = []
|
245
237
|
self.middleware_after_request = []
|
246
238
|
self.response_headers = {}
|
247
239
|
self.args = ArgsConfig()
|
248
240
|
self.start_up_handler = None
|
249
241
|
self.shutdown_handler = None
|
250
|
-
self.auto_compression = auto_compression
|
251
242
|
self.database_config = database_config
|
252
243
|
self.thread_config = ThreadConfigurator().get_config()
|
253
244
|
|
@@ -255,7 +246,8 @@ class Hypern:
|
|
255
246
|
self.router.extend_route(route(app=self).routes)
|
256
247
|
|
257
248
|
for websocket_route in websockets or []:
|
258
|
-
|
249
|
+
for route in websocket_route.routes:
|
250
|
+
self.websocket_router.add_route(route)
|
259
251
|
|
260
252
|
if openapi_url and docs_url:
|
261
253
|
self.__add_openapi(
|
@@ -313,6 +305,20 @@ class Hypern:
|
|
313
305
|
self.add_route(HTTPMethod.GET, openapi_url, schema)
|
314
306
|
self.add_route(HTTPMethod.GET, docs_url, template_render)
|
315
307
|
|
308
|
+
def inject(self, key: str, value: Any):
|
309
|
+
"""
|
310
|
+
Injects a key-value pair into the injectables dictionary.
|
311
|
+
|
312
|
+
Args:
|
313
|
+
key (str): The key to be added to the injectables dictionary.
|
314
|
+
value (Any): The value to be associated with the key.
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
self: Returns the instance of the class to allow method chaining.
|
318
|
+
"""
|
319
|
+
self.dependencies[key] = value
|
320
|
+
return self
|
321
|
+
|
316
322
|
def add_response_header(self, key: str, value: str):
|
317
323
|
"""
|
318
324
|
Adds a response header to the response headers dictionary.
|
@@ -366,20 +372,6 @@ class Hypern:
|
|
366
372
|
|
367
373
|
return decorator
|
368
374
|
|
369
|
-
def inject(self, key: str, value: Any):
|
370
|
-
"""
|
371
|
-
Injects a key-value pair into the injectables dictionary.
|
372
|
-
|
373
|
-
Args:
|
374
|
-
key (str): The key to be added to the injectables dictionary.
|
375
|
-
value (Any): The value to be associated with the key.
|
376
|
-
|
377
|
-
Returns:
|
378
|
-
self: Returns the instance of the class to allow method chaining.
|
379
|
-
"""
|
380
|
-
self.injectables[key] = value
|
381
|
-
return self
|
382
|
-
|
383
375
|
def add_middleware(self, middleware: Middleware):
|
384
376
|
"""
|
385
377
|
Adds middleware to the application.
|
@@ -429,12 +421,10 @@ class Hypern:
|
|
429
421
|
server = Server()
|
430
422
|
server.set_router(router=self.router)
|
431
423
|
server.set_websocket_router(websocket_router=self.websocket_router)
|
432
|
-
server.
|
424
|
+
server.set_dependencies(dependencies=self.dependencies)
|
433
425
|
server.set_before_hooks(hooks=self.middleware_before_request)
|
434
426
|
server.set_after_hooks(hooks=self.middleware_after_request)
|
435
427
|
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
428
|
|
439
429
|
if self.database_config:
|
440
430
|
server.set_database_config(config=self.database_config)
|
@@ -447,6 +437,9 @@ class Hypern:
|
|
447
437
|
self.args.workers = self.thread_config.workers
|
448
438
|
self.args.max_blocking_threads = self.thread_config.max_blocking_threads
|
449
439
|
|
440
|
+
if self.args.http2:
|
441
|
+
server.enable_http2()
|
442
|
+
|
450
443
|
run_processes(
|
451
444
|
server=server,
|
452
445
|
host=self.args.host,
|
hypern/args_parser.py
CHANGED
@@ -49,13 +49,6 @@ 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
52
|
parser.add_argument(
|
60
53
|
"--auto-workers",
|
61
54
|
action="store_true",
|
@@ -63,19 +56,9 @@ class ArgsConfig:
|
|
63
56
|
)
|
64
57
|
|
65
58
|
parser.add_argument(
|
66
|
-
"--
|
67
|
-
|
68
|
-
|
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]",
|
59
|
+
"--http2",
|
60
|
+
action="store_true",
|
61
|
+
help="Enable HTTP/2 support.",
|
79
62
|
)
|
80
63
|
|
81
64
|
args, _ = parser.parse_known_args()
|
@@ -86,7 +69,5 @@ class ArgsConfig:
|
|
86
69
|
self.processes = args.processes or 1
|
87
70
|
self.workers = args.workers or 1
|
88
71
|
self.reload = args.reload or False
|
89
|
-
self.auto_compression = args.auto_compression
|
90
72
|
self.auto_workers = args.auto_workers
|
91
|
-
self.
|
92
|
-
self.max_capacity = args.max_capacity
|
73
|
+
self.http2 = args.http2 or False
|
@@ -7,8 +7,6 @@ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_scoped_sessi
|
|
7
7
|
from sqlalchemy.orm import Session, sessionmaker
|
8
8
|
from sqlalchemy.sql.expression import Delete, Insert, Update
|
9
9
|
|
10
|
-
from .repository import Model, PostgresRepository
|
11
|
-
|
12
10
|
|
13
11
|
class SqlalchemyConfig:
|
14
12
|
def __init__(self, default_engine: AsyncEngine | None = None, reader_engine: AsyncEngine | None = None, writer_engine: AsyncEngine | None = None):
|
@@ -66,6 +64,3 @@ class SqlalchemyConfig:
|
|
66
64
|
|
67
65
|
def init_app(self, app):
|
68
66
|
app.inject("get_session", self.get_session)
|
69
|
-
|
70
|
-
|
71
|
-
__all__ = ["Model", "PostgresRepository", "SqlalchemyConfig"]
|
hypern/datastructures.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from typing import Optional
|
2
2
|
from enum import Enum
|
3
|
-
from pydantic import BaseModel, AnyUrl
|
3
|
+
from pydantic import BaseModel, AnyUrl
|
4
4
|
|
5
5
|
|
6
6
|
class BaseModelWithConfig(BaseModel):
|
@@ -10,7 +10,7 @@ class BaseModelWithConfig(BaseModel):
|
|
10
10
|
class Contact(BaseModelWithConfig):
|
11
11
|
name: Optional[str] = None
|
12
12
|
url: Optional[AnyUrl] = None
|
13
|
-
email: Optional[
|
13
|
+
email: Optional[str] = None
|
14
14
|
|
15
15
|
|
16
16
|
class License(BaseModelWithConfig):
|
hypern/hypern.cp312-win32.pyd
CHANGED
Binary file
|
hypern/hypern.pyi
CHANGED
@@ -181,16 +181,14 @@ class Server:
|
|
181
181
|
def set_router(self, router: Router) -> None: ...
|
182
182
|
def set_websocket_router(self, websocket_router: WebsocketRouter) -> None: ...
|
183
183
|
def start(self, socket: SocketHeld, worker: int, max_blocking_threads: int) -> None: ...
|
184
|
-
def inject(self, key: str, value: Any) -> None: ...
|
185
|
-
def set_injected(self, injected: Dict[str, Any]) -> None: ...
|
186
184
|
def set_before_hooks(self, hooks: List[FunctionInfo]) -> None: ...
|
187
185
|
def set_after_hooks(self, hooks: List[FunctionInfo]) -> None: ...
|
188
186
|
def set_response_headers(self, headers: Dict[str, str]) -> None: ...
|
189
187
|
def set_startup_handler(self, on_startup: FunctionInfo) -> None: ...
|
190
188
|
def set_shutdown_handler(self, on_shutdown: FunctionInfo) -> None: ...
|
191
|
-
def set_auto_compression(self, enabled: bool) -> None: ...
|
192
189
|
def set_database_config(self, config: DatabaseConfig) -> None: ...
|
193
|
-
def
|
190
|
+
def set_dependencies(self, dependencies: Dict[str, Any]) -> None: ...
|
191
|
+
def enable_http2(self) -> None: ...
|
194
192
|
|
195
193
|
class Route:
|
196
194
|
path: str
|
@@ -278,7 +276,7 @@ class UploadedFile:
|
|
278
276
|
path: str
|
279
277
|
size: int
|
280
278
|
content: bytes
|
281
|
-
|
279
|
+
file_name: str
|
282
280
|
|
283
281
|
@dataclass
|
284
282
|
class BodyData:
|
hypern/worker.py
CHANGED
@@ -1,30 +1,274 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import time
|
5
|
+
import traceback
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
7
|
+
from functools import partial, wraps
|
8
|
+
from typing import Callable, Dict, List
|
9
|
+
|
3
10
|
from celery import Celery
|
4
|
-
from
|
11
|
+
from celery.result import AsyncResult
|
12
|
+
from celery.signals import (
|
13
|
+
after_setup_logger,
|
14
|
+
after_setup_task_logger,
|
15
|
+
task_failure,
|
16
|
+
task_postrun,
|
17
|
+
task_prerun,
|
18
|
+
worker_ready,
|
19
|
+
worker_shutdown,
|
20
|
+
)
|
21
|
+
from kombu import Exchange, Queue
|
22
|
+
|
23
|
+
|
24
|
+
class Worker(Celery):
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
main: str = None,
|
28
|
+
broker_url: str = None,
|
29
|
+
result_backend: str = "rpc://",
|
30
|
+
queues: Dict[str, Dict] = None,
|
31
|
+
task_routes: Dict[str, str] = None,
|
32
|
+
imports: List[str] = None,
|
33
|
+
**kwargs,
|
34
|
+
):
|
35
|
+
super().__init__(main, **kwargs)
|
36
|
+
|
37
|
+
self._executor = ThreadPoolExecutor()
|
38
|
+
self._task_timings = {}
|
39
|
+
|
40
|
+
self.default_exchange = Exchange("default", type="direct")
|
41
|
+
self.priority_exchange = Exchange("priority", type="direct")
|
42
|
+
|
43
|
+
default_queues = {
|
44
|
+
"default": {"exchange": self.default_exchange, "routing_key": "default"},
|
45
|
+
"high_priority": {"exchange": self.priority_exchange, "routing_key": "high"},
|
46
|
+
"low_priority": {"exchange": self.priority_exchange, "routing_key": "low"},
|
47
|
+
}
|
48
|
+
if queues:
|
49
|
+
default_queues.update(queues)
|
50
|
+
|
51
|
+
self._queues = {
|
52
|
+
name: Queue(
|
53
|
+
name,
|
54
|
+
exchange=config.get("exchange", self.default_exchange),
|
55
|
+
routing_key=config.get("routing_key", name),
|
56
|
+
queue_arguments=config.get("arguments", {}),
|
57
|
+
)
|
58
|
+
for name, config in default_queues.items()
|
59
|
+
}
|
60
|
+
|
61
|
+
self.conf.update(
|
62
|
+
broker_url=broker_url,
|
63
|
+
result_backend=result_backend,
|
64
|
+
# Worker Pool Configuration
|
65
|
+
worker_pool="solo",
|
66
|
+
worker_pool_restarts=True,
|
67
|
+
broker_connection_retry_on_startup=True,
|
68
|
+
# Worker Configuration
|
69
|
+
worker_prefetch_multiplier=1,
|
70
|
+
worker_max_tasks_per_child=1000,
|
71
|
+
worker_concurrency=os.cpu_count(),
|
72
|
+
# Task Settings
|
73
|
+
task_acks_late=True,
|
74
|
+
task_reject_on_worker_lost=True,
|
75
|
+
task_time_limit=3600,
|
76
|
+
task_soft_time_limit=3000,
|
77
|
+
task_default_retry_delay=300,
|
78
|
+
task_max_retries=3,
|
79
|
+
# Memory Management
|
80
|
+
worker_max_memory_per_child=200000, # 200MB
|
81
|
+
# Task Routing
|
82
|
+
task_routes=task_routes,
|
83
|
+
task_queues=list(self._queues.values()),
|
84
|
+
# Performance Settings
|
85
|
+
task_compression="gzip",
|
86
|
+
result_compression="gzip",
|
87
|
+
task_serializer="json",
|
88
|
+
result_serializer="json",
|
89
|
+
accept_content=["json"],
|
90
|
+
imports=imports,
|
91
|
+
task_default_exchange=self.default_exchange.name,
|
92
|
+
task_default_routing_key="default",
|
93
|
+
)
|
94
|
+
|
95
|
+
self._setup_signals()
|
96
|
+
|
97
|
+
def _setup_signals(self):
|
98
|
+
@worker_ready.connect
|
99
|
+
def on_worker_ready(sender, **kwargs):
|
100
|
+
self.logger.info(f"Worker {sender.hostname} is ready")
|
101
|
+
|
102
|
+
@worker_shutdown.connect
|
103
|
+
def on_worker_shutdown(sender, **kwargs):
|
104
|
+
self.logger.info(f"Worker {sender.hostname} is shutting down")
|
105
|
+
self._executor.shutdown(wait=True)
|
106
|
+
|
107
|
+
@task_prerun.connect
|
108
|
+
def task_prerun_handler(task_id, task, *args, **kwargs):
|
109
|
+
self._task_timings[task_id] = {"start": time.time()}
|
110
|
+
self.logger.info(f"Task {task.name}[{task_id}] started")
|
111
|
+
|
112
|
+
@task_postrun.connect
|
113
|
+
def task_postrun_handler(task_id, task, *args, retval=None, **kwargs):
|
114
|
+
if task_id in self._task_timings:
|
115
|
+
start_time = self._task_timings[task_id]["start"]
|
116
|
+
duration = time.time() - start_time
|
117
|
+
self.logger.info(f"Task {task.name}[{task_id}] completed in {duration:.2f}s")
|
118
|
+
del self._task_timings[task_id]
|
119
|
+
|
120
|
+
@task_failure.connect
|
121
|
+
def task_failure_handler(task_id, exc, task, *args, **kwargs):
|
122
|
+
self.logger.error(f"Task {task.name}[{task_id}] failed: {exc}\n{traceback.format_exc()}")
|
123
|
+
|
124
|
+
@after_setup_logger.connect
|
125
|
+
def setup_celery_logger(logger, *args, **kwargs):
|
126
|
+
existing_logger = logging.getLogger("hypern")
|
127
|
+
logger.handlers = existing_logger.handlers
|
128
|
+
logger.filters = existing_logger.filters
|
129
|
+
logger.level = existing_logger.level
|
130
|
+
|
131
|
+
@after_setup_task_logger.connect
|
132
|
+
def setup_task_logger(logger, *args, **kwargs):
|
133
|
+
existing_logger = logging.getLogger("hypern")
|
134
|
+
logger.handlers = existing_logger.handlers
|
135
|
+
logger.filters = existing_logger.filters
|
136
|
+
logger.level = existing_logger.level
|
137
|
+
|
138
|
+
def add_task_routes(self, routes: Dict[str, str]) -> None:
|
139
|
+
"""
|
140
|
+
Example:
|
141
|
+
app.add_task_routes({
|
142
|
+
'tasks.email.*': 'email_queue',
|
143
|
+
'tasks.payment.process': 'payment_queue',
|
144
|
+
'tasks.high_priority.*': 'high_priority'
|
145
|
+
})
|
146
|
+
"""
|
147
|
+
for task_pattern, queue in routes.items():
|
148
|
+
self.add_task_route(task_pattern, queue)
|
149
|
+
|
150
|
+
def add_task_route(self, task_pattern: str, queue: str) -> None:
|
151
|
+
"""
|
152
|
+
Add a task route to the Celery app
|
153
|
+
|
154
|
+
Example:
|
155
|
+
app.add_task_route('tasks.email.send', 'email_queue')
|
156
|
+
app.add_task_route('tasks.payment.*', 'payment_queue')
|
157
|
+
"""
|
158
|
+
if queue not in self._queues:
|
159
|
+
raise ValueError(f"Queue '{queue}' does not exist. Create it first using create_queue()")
|
160
|
+
|
161
|
+
self._task_route_mapping[task_pattern] = queue
|
162
|
+
|
163
|
+
# Update Celery task routes
|
164
|
+
routes = self.conf.task_routes or {}
|
165
|
+
routes[task_pattern] = {"queue": queue}
|
166
|
+
self.conf.task_routes = routes
|
167
|
+
|
168
|
+
self.logger.info(f"Added route: {task_pattern} -> {queue}")
|
169
|
+
|
170
|
+
def task(self, *args, **opts):
|
171
|
+
"""
|
172
|
+
Decorator modified to support sync and async functions
|
173
|
+
"""
|
174
|
+
base_task = Celery.task.__get__(self)
|
175
|
+
|
176
|
+
def decorator(func):
|
177
|
+
is_async = asyncio.iscoroutinefunction(func)
|
178
|
+
|
179
|
+
if is_async:
|
180
|
+
|
181
|
+
@wraps(func)
|
182
|
+
async def async_wrapper(*fargs, **fkwargs):
|
183
|
+
return await func(*fargs, **fkwargs)
|
184
|
+
|
185
|
+
@base_task(*args, **opts)
|
186
|
+
def wrapped(*fargs, **fkwargs):
|
187
|
+
loop = asyncio.new_event_loop()
|
188
|
+
asyncio.set_event_loop(loop)
|
189
|
+
try:
|
190
|
+
return loop.run_until_complete(async_wrapper(*fargs, **fkwargs))
|
191
|
+
finally:
|
192
|
+
loop.close()
|
193
|
+
|
194
|
+
return wrapped
|
195
|
+
else:
|
196
|
+
return base_task(*args, **opts)(func)
|
197
|
+
|
198
|
+
return decorator
|
199
|
+
|
200
|
+
async def async_send_task(self, task_name: str, *args, **kwargs) -> AsyncResult:
|
201
|
+
"""
|
202
|
+
Version of send_task() that is async
|
203
|
+
"""
|
204
|
+
loop = asyncio.get_event_loop()
|
205
|
+
return await loop.run_in_executor(self._executor, partial(self.send_task, task_name, args=args, kwargs=kwargs))
|
206
|
+
|
207
|
+
async def async_result(self, task_id: str) -> Dict:
|
208
|
+
"""
|
209
|
+
Get the result of a task asynchronously
|
210
|
+
"""
|
211
|
+
async_result = self.AsyncResult(task_id)
|
212
|
+
loop = asyncio.get_event_loop()
|
213
|
+
|
214
|
+
result = await loop.run_in_executor(
|
215
|
+
self._executor,
|
216
|
+
lambda: {
|
217
|
+
"task_id": task_id,
|
218
|
+
"status": async_result.status,
|
219
|
+
"result": async_result.result,
|
220
|
+
"traceback": async_result.traceback,
|
221
|
+
"date_done": async_result.date_done,
|
222
|
+
},
|
223
|
+
)
|
224
|
+
return result
|
225
|
+
|
226
|
+
def get_queue_length(self, queue_name: str) -> int:
|
227
|
+
"""
|
228
|
+
Get the number of messages in a queue
|
229
|
+
"""
|
230
|
+
with self.connection_or_acquire() as conn:
|
231
|
+
channel = conn.channel()
|
232
|
+
queue = Queue(queue_name, channel=channel)
|
233
|
+
return queue.queue_declare(passive=True).message_count
|
5
234
|
|
235
|
+
async def chain_tasks(self, tasks: list) -> AsyncResult:
|
236
|
+
"""
|
237
|
+
Function to chain multiple tasks together
|
238
|
+
"""
|
239
|
+
chain = tasks[0]
|
240
|
+
for task in tasks[1:]:
|
241
|
+
chain = chain | task
|
242
|
+
return await self.adelay_task(chain)
|
6
243
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
return cls.instance
|
244
|
+
def register_task_middleware(self, middleware: Callable):
|
245
|
+
"""
|
246
|
+
Register a middleware function to be called before each task
|
247
|
+
"""
|
12
248
|
|
13
|
-
|
14
|
-
|
15
|
-
|
249
|
+
def task_middleware(task):
|
250
|
+
@wraps(task)
|
251
|
+
def _wrapped(*args, **kwargs):
|
252
|
+
return middleware(task, *args, **kwargs)
|
16
253
|
|
17
|
-
|
18
|
-
TaskBase = self.Task
|
254
|
+
return _wrapped
|
19
255
|
|
20
|
-
|
21
|
-
abstract = True
|
256
|
+
self.task = task_middleware(self.task)
|
22
257
|
|
23
|
-
|
24
|
-
|
25
|
-
|
258
|
+
def monitor_task(self, task_id: str) -> dict:
|
259
|
+
"""
|
260
|
+
Get monitoring data for a task
|
261
|
+
"""
|
262
|
+
result = self.AsyncResult(task_id)
|
263
|
+
timing_info = self._task_timings.get(task_id, {})
|
26
264
|
|
27
|
-
|
28
|
-
|
265
|
+
monitoring_data = {
|
266
|
+
"task_id": task_id,
|
267
|
+
"status": result.status,
|
268
|
+
"start_time": timing_info.get("start"),
|
269
|
+
"duration": time.time() - timing_info["start"] if timing_info.get("start") else None,
|
270
|
+
"result": result.result if result.ready() else None,
|
271
|
+
"traceback": result.traceback,
|
272
|
+
}
|
29
273
|
|
30
|
-
|
274
|
+
return monitoring_data
|
@@ -1,32 +1,24 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hypern
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.9
|
4
4
|
Classifier: Programming Language :: Rust
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
7
7
|
Requires-Dist: sqlalchemy[asyncio] ==2.0.31
|
8
|
-
Requires-Dist: pydantic[email] ==2.8.2
|
9
|
-
Requires-Dist: passlib ==1.7.4
|
10
8
|
Requires-Dist: pyjwt ==2.8.0
|
11
9
|
Requires-Dist: pydash ==8.0.3
|
12
10
|
Requires-Dist: sentry-sdk ==2.11.0
|
13
|
-
Requires-Dist: pydantic-settings ==2.3.4
|
14
11
|
Requires-Dist: celery ==5.4.0
|
15
|
-
Requires-Dist: asgiref ==3.8.1
|
16
12
|
Requires-Dist: psycopg ==3.2.3
|
17
13
|
Requires-Dist: pyyaml ==6.0.2
|
18
|
-
Requires-Dist: mongoengine ==0.29.1
|
19
|
-
Requires-Dist: argon2-cffi ==23.1.0
|
20
|
-
Requires-Dist: bcrypt ==4.2.0
|
21
14
|
Requires-Dist: orjson ==3.10.11
|
22
15
|
Requires-Dist: multiprocess ==0.70.17
|
23
16
|
Requires-Dist: uvloop ==0.21.0 ; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'
|
24
|
-
Requires-Dist: cryptography ==43.0.3
|
25
17
|
Requires-Dist: watchdog ==6.0.0
|
26
|
-
Requires-Dist: jsonschema ==4.23.0
|
27
18
|
Requires-Dist: psutil ==6.1.0
|
28
19
|
Requires-Dist: msgpack ==1.1.0
|
29
20
|
Requires-Dist: redis ==5.2.1
|
21
|
+
Requires-Dist: pydantic ==2.10.4
|
30
22
|
License-File: LICENSE
|
31
23
|
Summary: A Fast Async Python backend with a Rust runtime.
|
32
24
|
Author-email: Martin Dang <vannghiem848@gmail.com>
|
@@ -38,7 +30,7 @@ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
38
30
|
|
39
31
|
Hypern: A Versatile Python and Rust Framework
|
40
32
|
|
41
|
-
Hypern is a flexible, open-source framework built on the [
|
33
|
+
Hypern is a flexible, open-source framework built on the [Rust](https://github.com/rust-lang/rust), designed to jumpstart your high-performance web development endeavors. By providing a pre-configured structure and essential components, Hypern empowers you to rapidly develop custom web applications that leverage the combined power of Python and Rust.
|
42
34
|
|
43
35
|
With Hypern, you can seamlessly integrate asynchronous features and build scalable solutions for RESTful APIs and dynamic web applications. Its intuitive design and robust tooling allow developers to focus on creating high-quality code while maximizing performance. Embrace the synergy of Python and Rust to elevate your web development experience.
|
44
36
|
|
@@ -92,7 +84,7 @@ routing = [
|
|
92
84
|
app = Hypern(routing)
|
93
85
|
|
94
86
|
if __name__ == "__main__":
|
95
|
-
app.start(
|
87
|
+
app.start()
|
96
88
|
```
|
97
89
|
|
98
90
|
```
|
@@ -100,6 +92,16 @@ $ python3 main.py
|
|
100
92
|
```
|
101
93
|
You can open swagger UI at path `/docs`
|
102
94
|
|
95
|
+
## CLI
|
96
|
+
|
97
|
+
- host (str): The host address to bind to. Defaults to '127.0.0.1'.
|
98
|
+
- port (int): The port number to bind to. Defaults to 5000.
|
99
|
+
- processes (int): The number of processes to use. Defaults to 1.
|
100
|
+
- workers (int): The number of worker threads to use. Defaults to 1.
|
101
|
+
- max_blocking_threads (int): The maximum number of blocking threads. Defaults to 32.
|
102
|
+
- reload (bool): If True, the server will restart on file changes.
|
103
|
+
- auto_workers (bool): If True, sets the number of workers and max-blocking-threads automatically.
|
104
|
+
|
103
105
|
|
104
106
|
## 💡 Features
|
105
107
|
|
@@ -107,7 +109,7 @@ You can open swagger UI at path `/docs`
|
|
107
109
|
- Rust-powered core with Python flexibility
|
108
110
|
- Multi-process architecture for optimal CPU utilization
|
109
111
|
- Async/await support for non-blocking operations
|
110
|
-
- Built on top of production-ready
|
112
|
+
- Built on top of production-ready Rust language
|
111
113
|
|
112
114
|
### 🛠 Development Experience
|
113
115
|
- Type hints and IDE support
|
@@ -118,7 +120,7 @@ You can open swagger UI at path `/docs`
|
|
118
120
|
### 🔌 Integration & Extensions
|
119
121
|
- Easy dependency injection
|
120
122
|
- Middleware support (before/after request hooks)
|
121
|
-
- WebSocket support
|
123
|
+
- WebSocket support
|
122
124
|
- Background task scheduling
|
123
125
|
- File upload handling
|
124
126
|
|