hypern 0.3.2__cp312-cp312-win32.whl → 0.3.4__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/application.py CHANGED
@@ -2,26 +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, MiddlewareConfig
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
- from hypern.logging import logger
21
24
 
22
25
  AppType = TypeVar("AppType", bound="Hypern")
23
26
 
24
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
+
25
51
  class Hypern:
26
52
  def __init__(
27
53
  self: AppType,
@@ -199,6 +225,14 @@ class Hypern:
199
225
  """
200
226
  ),
201
227
  ] = False,
228
+ database_config: Annotated[
229
+ DatabaseConfig | None,
230
+ Doc(
231
+ """
232
+ The database configuration for the application.
233
+ """
234
+ ),
235
+ ] = None,
202
236
  *args: Any,
203
237
  **kwargs: Any,
204
238
  ) -> None:
@@ -214,6 +248,8 @@ class Hypern:
214
248
  self.start_up_handler = None
215
249
  self.shutdown_handler = None
216
250
  self.auto_compression = auto_compression
251
+ self.database_config = database_config
252
+ self.thread_config = ThreadConfigurator().get_config()
217
253
 
218
254
  for route in routes or []:
219
255
  self.router.extend_route(route(app=self).routes)
@@ -369,6 +405,15 @@ class Hypern:
369
405
  after_request = FunctionInfo(handler=after_request, is_async=is_async)
370
406
  self.middleware_after_request.append((after_request, middleware.config))
371
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
416
+
372
417
  def start(
373
418
  self,
374
419
  ):
@@ -381,22 +426,35 @@ class Hypern:
381
426
  if self.scheduler:
382
427
  self.scheduler.start()
383
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
+ if self.database_config:
440
+ server.set_database_config(config=self.database_config)
441
+ if self.start_up_handler:
442
+ server.set_startup_handler(self.start_up_handler)
443
+ if self.shutdown_handler:
444
+ server.set_shutdown_handler(self.shutdown_handler)
445
+
446
+ if self.args.auto_workers:
447
+ self.args.workers = self.thread_config.workers
448
+ self.args.max_blocking_threads = self.thread_config.max_blocking_threads
449
+
384
450
  run_processes(
451
+ server=server,
385
452
  host=self.args.host,
386
453
  port=self.args.port,
387
454
  workers=self.args.workers,
388
455
  processes=self.args.processes,
389
456
  max_blocking_threads=self.args.max_blocking_threads,
390
- router=self.router,
391
- websocket_router=self.websocket_router,
392
- injectables=self.injectables,
393
- before_request=self.middleware_before_request,
394
- after_request=self.middleware_after_request,
395
- response_headers=self.response_headers,
396
457
  reload=self.args.reload,
397
- on_startup=self.start_up_handler,
398
- on_shutdown=self.shutdown_handler,
399
- auto_compression=self.args.auto_compression or self.auto_compression,
400
458
  )
401
459
 
402
460
  def add_route(self, method: HTTPMethod, endpoint: str, handler: Callable[..., Any]):
hypern/args_parser.py CHANGED
@@ -55,12 +55,38 @@ class ArgsConfig:
55
55
  action="store_true",
56
56
  help="It compresses the response automatically.",
57
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
+
58
81
  args, _ = parser.parse_known_args()
59
82
 
60
83
  self.host = args.host or "127.0.0.1"
61
84
  self.port = args.port or 5000
62
- self.max_blocking_threads = args.max_blocking_threads or 100
85
+ self.max_blocking_threads = args.max_blocking_threads or 32
63
86
  self.processes = args.processes or 1
64
87
  self.workers = args.workers or 1
65
88
  self.reload = args.reload or False
66
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
hypern/config.py CHANGED
@@ -1,9 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+
5
+ # -*- coding: utf-8 -*-
6
+ import threading
4
7
  import typing
5
8
  import warnings
9
+ from contextvars import ContextVar
10
+ from datetime import datetime
6
11
  from pathlib import Path
12
+ from typing import Dict, Optional
7
13
 
8
14
  """
9
15
 
@@ -147,3 +153,94 @@ class Config:
147
153
  return cast(value)
148
154
  except (TypeError, ValueError):
149
155
  raise ValueError(f"Config '{key}' has value '{value}'. Not a valid {cast.__name__}.")
156
+
157
+
158
+ class ContextStore:
159
+ def __init__(self, cleanup_interval: int = 300, max_age: int = 3600):
160
+ """
161
+ Initialize ContextStore with automatic session cleanup.
162
+
163
+ :param cleanup_interval: Interval between cleanup checks (in seconds)
164
+ :param max_age: Maximum age of a session before it's considered expired (in seconds)
165
+ """
166
+ self._session_times: Dict[str, datetime] = {}
167
+ self.session_var = ContextVar("session_id", default=None)
168
+
169
+ self._max_age = max_age
170
+ self._cleanup_interval = cleanup_interval
171
+ self._cleanup_thread: Optional[threading.Thread] = None
172
+ self._stop_event = threading.Event()
173
+
174
+ # Start the cleanup thread
175
+ self._start_cleanup_thread()
176
+
177
+ def _start_cleanup_thread(self):
178
+ """Start a background thread for periodic session cleanup."""
179
+
180
+ def cleanup_worker():
181
+ while not self._stop_event.is_set():
182
+ self._perform_cleanup()
183
+ self._stop_event.wait(self._cleanup_interval)
184
+
185
+ self._cleanup_thread = threading.Thread(
186
+ target=cleanup_worker,
187
+ daemon=True, # Allows the thread to be automatically terminated when the main program exits
188
+ )
189
+ self._cleanup_thread.start()
190
+
191
+ def _perform_cleanup(self):
192
+ """Perform cleanup of expired sessions."""
193
+ current_time = datetime.now()
194
+ expired_sessions = [
195
+ session_id for session_id, timestamp in list(self._session_times.items()) if (current_time - timestamp).total_seconds() > self._max_age
196
+ ]
197
+
198
+ for session_id in expired_sessions:
199
+ self.remove_session(session_id)
200
+
201
+ def remove_session(self, session_id: str):
202
+ """Remove a specific session."""
203
+ self._session_times.pop(session_id, None)
204
+
205
+ def set_context(self, session_id: str):
206
+ """
207
+ Context manager for setting and resetting session context.
208
+
209
+ :param session_id: Unique identifier for the session
210
+ :return: Context manager for session
211
+ """
212
+ self.session_var.set(session_id)
213
+ self._session_times[session_id] = datetime.now()
214
+
215
+ def get_context(self) -> str:
216
+ """
217
+ Get the current session context.
218
+
219
+ :return: Current session ID
220
+ :raises RuntimeError: If no session context is available
221
+ """
222
+ return self.session_var.get()
223
+
224
+ def reset_context(self):
225
+ """Reset the session context."""
226
+ token = self.get_context()
227
+ if token is not None:
228
+ self.session_var.reset(token)
229
+
230
+ def stop_cleanup(self):
231
+ """
232
+ Stop the cleanup thread.
233
+ Useful for graceful shutdown of the application.
234
+ """
235
+ self._stop_event.set()
236
+ if self._cleanup_thread:
237
+ self._cleanup_thread.join()
238
+
239
+ def __del__(self):
240
+ """
241
+ Ensure cleanup thread is stopped when the object is deleted.
242
+ """
243
+ self.stop_cleanup()
244
+
245
+
246
+ context_store = ContextStore()
@@ -0,0 +1,5 @@
1
+ from . import sqlalchemy
2
+
3
+ __all__ = [
4
+ "sqlalchemy",
5
+ ]
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import traceback
4
+ from contextlib import asynccontextmanager
5
+
6
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_scoped_session
7
+ from sqlalchemy.orm import Session, sessionmaker
8
+ from sqlalchemy.sql.expression import Delete, Insert, Update
9
+
10
+ from .repository import Model, PostgresRepository
11
+
12
+
13
+ class SqlalchemyConfig:
14
+ def __init__(self, default_engine: AsyncEngine | None = None, reader_engine: AsyncEngine | None = None, writer_engine: AsyncEngine | None = None):
15
+ """
16
+ Initialize the SQL configuration.
17
+ You can provide a default engine, a reader engine, and a writer engine.
18
+ If only one engine is provided (default_engine), it will be used for both reading and writing.
19
+ If both reader and writer engines are provided, they will be used for reading and writing respectively.
20
+ Note: The reader and writer engines must be different.
21
+ """
22
+
23
+ assert default_engine or reader_engine or writer_engine, "At least one engine must be provided."
24
+ assert not (reader_engine and writer_engine and id(reader_engine) == id(writer_engine)), "Reader and writer engines must be different."
25
+
26
+ engines = {
27
+ "writer": writer_engine or default_engine,
28
+ "reader": reader_engine or default_engine,
29
+ }
30
+
31
+ class RoutingSession(Session):
32
+ def get_bind(this, mapper=None, clause=None, **kwargs):
33
+ if this._flushing or isinstance(clause, (Update, Delete, Insert)):
34
+ return engines["writer"].sync_engine
35
+ return engines["reader"].sync_engine
36
+
37
+ async_session_factory = sessionmaker(
38
+ class_=AsyncSession,
39
+ sync_session_class=RoutingSession,
40
+ expire_on_commit=False,
41
+ )
42
+
43
+ session_scope: AsyncSession | async_scoped_session = async_scoped_session(
44
+ session_factory=async_session_factory,
45
+ scopefunc=asyncio.current_task,
46
+ )
47
+
48
+ @asynccontextmanager
49
+ async def get_session():
50
+ """
51
+ Get the database session.
52
+ This can be used for dependency injection.
53
+
54
+ :return: The database session.
55
+ """
56
+ try:
57
+ yield session_scope
58
+ except Exception:
59
+ traceback.print_exc()
60
+ await session_scope.rollback()
61
+ finally:
62
+ await session_scope.remove()
63
+ await session_scope.close()
64
+
65
+ self.get_session = get_session
66
+
67
+ def init_app(self, app):
68
+ app.inject("get_session", self.get_session)
69
+
70
+
71
+ __all__ = ["Model", "PostgresRepository", "SqlalchemyConfig"]
hypern/db/sql/__init__.py CHANGED
@@ -1,179 +1,13 @@
1
- # -*- coding: utf-8 -*-
2
- import asyncio
3
- import threading
4
- import traceback
5
- from contextlib import asynccontextmanager
6
- from contextvars import ContextVar, Token
7
- from datetime import datetime
8
- from typing import Dict, Optional, Union
9
- from uuid import uuid4
10
-
11
- from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_scoped_session
12
- from sqlalchemy.orm import Session, sessionmaker
13
- from sqlalchemy.sql.expression import Delete, Insert, Update
14
-
15
- from hypern.hypern import Request, Response
16
-
17
- from .repository import Model, PostgresRepository
18
-
19
-
20
- class ContextStore:
21
- def __init__(self, cleanup_interval: int = 300, max_age: int = 3600):
22
- """
23
- Initialize ContextStore with automatic session cleanup.
24
-
25
- :param cleanup_interval: Interval between cleanup checks (in seconds)
26
- :param max_age: Maximum age of a session before it's considered expired (in seconds)
27
- """
28
- self._session_times: Dict[str, datetime] = {}
29
- self.session_var = ContextVar("session_id", default=None)
30
-
31
- self._max_age = max_age
32
- self._cleanup_interval = cleanup_interval
33
- self._cleanup_thread: Optional[threading.Thread] = None
34
- self._stop_event = threading.Event()
35
-
36
- # Start the cleanup thread
37
- self._start_cleanup_thread()
38
-
39
- def _start_cleanup_thread(self):
40
- """Start a background thread for periodic session cleanup."""
41
-
42
- def cleanup_worker():
43
- while not self._stop_event.is_set():
44
- self._perform_cleanup()
45
- self._stop_event.wait(self._cleanup_interval)
46
-
47
- self._cleanup_thread = threading.Thread(
48
- target=cleanup_worker,
49
- daemon=True, # Allows the thread to be automatically terminated when the main program exits
50
- )
51
- self._cleanup_thread.start()
52
-
53
- def _perform_cleanup(self):
54
- """Perform cleanup of expired sessions."""
55
- current_time = datetime.now()
56
- expired_sessions = [
57
- session_id for session_id, timestamp in list(self._session_times.items()) if (current_time - timestamp).total_seconds() > self._max_age
58
- ]
59
-
60
- for session_id in expired_sessions:
61
- self.remove_session(session_id)
62
-
63
- def remove_session(self, session_id: str):
64
- """Remove a specific session."""
65
- self._session_times.pop(session_id, None)
66
-
67
- def set_context(self, session_id: str):
68
- """
69
- Context manager for setting and resetting session context.
70
-
71
- :param session_id: Unique identifier for the session
72
- :return: Context manager for session
73
- """
74
- self.session_var.set(session_id)
75
- self._session_times[session_id] = datetime.now()
76
-
77
- def get_context(self) -> str:
78
- """
79
- Get the current session context.
80
-
81
- :return: Current session ID
82
- :raises RuntimeError: If no session context is available
83
- """
84
- return self.session_var.get()
85
-
86
- def reset_context(self):
87
- """Reset the session context."""
88
- token = self.get_context()
89
- if token is not None:
90
- self.session_var.reset(token)
91
-
92
- def stop_cleanup(self):
93
- """
94
- Stop the cleanup thread.
95
- Useful for graceful shutdown of the application.
96
- """
97
- self._stop_event.set()
98
- if self._cleanup_thread:
99
- self._cleanup_thread.join()
100
-
101
- def __del__(self):
102
- """
103
- Ensure cleanup thread is stopped when the object is deleted.
104
- """
105
- self.stop_cleanup()
106
-
107
-
108
- class SqlConfig:
109
- def __init__(self, default_engine: AsyncEngine | None = None, reader_engine: AsyncEngine | None = None, writer_engine: AsyncEngine | None = None):
110
- """
111
- Initialize the SQL configuration.
112
- You can provide a default engine, a reader engine, and a writer engine.
113
- If only one engine is provided (default_engine), it will be used for both reading and writing.
114
- If both reader and writer engines are provided, they will be used for reading and writing respectively.
115
- Note: The reader and writer engines must be different.
116
- """
117
-
118
- assert default_engine or reader_engine or writer_engine, "At least one engine must be provided."
119
- assert not (reader_engine and writer_engine and id(reader_engine) == id(writer_engine)), "Reader and writer engines must be different."
120
-
121
- engines = {
122
- "writer": writer_engine or default_engine,
123
- "reader": reader_engine or default_engine,
124
- }
125
- self.session_store = ContextStore()
126
-
127
- class RoutingSession(Session):
128
- def get_bind(this, mapper=None, clause=None, **kwargs):
129
- if this._flushing or isinstance(clause, (Update, Delete, Insert)):
130
- return engines["writer"].sync_engine
131
- return engines["reader"].sync_engine
132
-
133
- async_session_factory = sessionmaker(
134
- class_=AsyncSession,
135
- sync_session_class=RoutingSession,
136
- expire_on_commit=False,
137
- )
138
-
139
- session_scope: Union[AsyncSession, async_scoped_session] = async_scoped_session(
140
- session_factory=async_session_factory,
141
- scopefunc=asyncio.current_task,
142
- )
143
-
144
- @asynccontextmanager
145
- async def get_session():
146
- """
147
- Get the database session.
148
- This can be used for dependency injection.
149
-
150
- :return: The database session.
151
- """
152
- try:
153
- yield session_scope
154
- except Exception:
155
- traceback.print_exc()
156
- await session_scope.rollback()
157
- finally:
158
- await session_scope.remove()
159
- await session_scope.close()
160
-
161
- self.get_session = get_session
162
- self._context_token: Optional[Token] = None
163
-
164
- def before_request(self, request: Request):
165
- token = str(uuid4())
166
- self.session_store.set_context(token)
167
- return request
168
-
169
- def after_request(self, response: Response):
170
- self.session_store.reset_context()
171
- return response
172
-
173
- def init_app(self, app):
174
- app.inject("get_session", self.get_session)
175
- app.before_request()(self.before_request)
176
- app.after_request()(self.after_request)
177
-
178
-
179
- __all__ = ["Model", "PostgresRepository", "SqlConfig"]
1
+ # from .context import SqlConfig, DatabaseType
2
+ from .field import CharField, IntegerField
3
+ from .model import Model
4
+ from .query import F, Q, QuerySet
5
+
6
+ __all__ = [
7
+ "CharField",
8
+ "IntegerField",
9
+ "Model",
10
+ "Q",
11
+ "F",
12
+ "QuerySet",
13
+ ]