hypern 0.3.1__cp311-cp311-win_amd64.whl → 0.3.3__cp311-cp311-win_amd64.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.cp311-win_amd64.pyd +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
@@ -1,101 +1,188 @@
1
- from typing import Any, Optional, Callable, TypeVar
2
- from datetime import datetime
3
1
  import asyncio
2
+ import time
3
+ from abc import ABC, abstractmethod
4
+ from typing import Callable, Generic, Optional, TypeVar
5
+
4
6
  import orjson
5
7
 
6
- from hypern.logging import logger
8
+ from .backend import BaseBackend
7
9
 
8
10
  T = TypeVar("T")
9
11
 
10
12
 
11
- class CacheEntry:
12
- def __init__(self, value: Any, expires_at: int, stale_at: Optional[int] = None):
13
+ class CacheStrategy(ABC, Generic[T]):
14
+ """Base class for cache strategies"""
15
+
16
+ @abstractmethod
17
+ async def get(self, key: str) -> Optional[T]:
18
+ pass
19
+
20
+ @abstractmethod
21
+ async def set(self, key: str, value: T, ttl: Optional[int] = None) -> None:
22
+ pass
23
+
24
+ @abstractmethod
25
+ async def delete(self, key: str) -> None:
26
+ pass
27
+
28
+
29
+ class CacheEntry(Generic[T]):
30
+ """Represents a cached item with metadata"""
31
+
32
+ def __init__(self, value: T, created_at: float, ttl: int, revalidate_after: Optional[int] = None):
13
33
  self.value = value
14
- self.expires_at = expires_at
15
- self.stale_at = stale_at or expires_at
34
+ self.created_at = created_at
35
+ self.ttl = ttl
36
+ self.revalidate_after = revalidate_after
16
37
  self.is_revalidating = False
17
38
 
18
- def to_json(self) -> str:
19
- return orjson.dumps({"value": self.value, "expires_at": self.expires_at, "stale_at": self.stale_at, "is_revalidating": self.is_revalidating})
39
+ def is_stale(self) -> bool:
40
+ """Check if entry is stale and needs revalidation"""
41
+ now = time.time()
42
+ return self.revalidate_after is not None and now > (self.created_at + self.revalidate_after)
43
+
44
+ def is_expired(self) -> bool:
45
+ """Check if entry has completely expired"""
46
+ now = time.time()
47
+ return now > (self.created_at + self.ttl)
48
+
49
+ def to_json(self) -> bytes:
50
+ """Serialize entry to JSON"""
51
+ return orjson.dumps(
52
+ {
53
+ "value": self.value,
54
+ "created_at": self.created_at,
55
+ "ttl": self.ttl,
56
+ "revalidate_after": self.revalidate_after,
57
+ "is_revalidating": self.is_revalidating,
58
+ }
59
+ )
20
60
 
21
61
  @classmethod
22
- def from_json(cls, data: str) -> "CacheEntry":
23
- data_dict = orjson.loads(data)
24
- entry = cls(value=data_dict["value"], expires_at=data_dict["expires_at"], stale_at=data_dict["stale_at"])
25
- entry.is_revalidating = data_dict["is_revalidating"]
26
- return entry
62
+ def from_json(cls, data: bytes) -> "CacheEntry[T]":
63
+ """Deserialize entry from JSON"""
64
+ parsed = orjson.loads(data)
65
+ return cls(value=parsed["value"], created_at=parsed["created_at"], ttl=parsed["ttl"], revalidate_after=parsed["revalidate_after"])
66
+
27
67
 
68
+ class StaleWhileRevalidateStrategy(CacheStrategy[T]):
69
+ """
70
+ Implements stale-while-revalidate caching strategy.
71
+ Allows serving stale content while revalidating in the background.
72
+ """
28
73
 
29
- class CacheStrategy:
30
- def __init__(self, backend: Any):
74
+ def __init__(self, backend: BaseBackend, revalidate_after: int, ttl: int, revalidate_fn: Callable[..., T]):
75
+ """
76
+ Initialize the caching strategy.
77
+
78
+ Args:
79
+ backend (BaseBackend): The backend storage for caching.
80
+ revalidate_after (int): The time in seconds after which the cache should be revalidated.
81
+ ttl (int): The time-to-live for cache entries in seconds.
82
+ revalidate_fn (Callable[..., T]): The function to call for revalidating the cache.
83
+
84
+ Attributes:
85
+ backend (BaseBackend): The backend storage for caching.
86
+ revalidate_after (int): The time in seconds after which the cache should be revalidated.
87
+ ttl (int): The time-to-live for cache entries in seconds.
88
+ revalidate_fn (Callable[..., T]): The function to call for revalidating the cache.
89
+ _revalidation_locks (dict): A dictionary to manage revalidation locks.
90
+ """
31
91
  self.backend = backend
92
+ self.revalidate_after = revalidate_after
93
+ self.ttl = ttl
94
+ self.revalidate_fn = revalidate_fn
95
+ self._revalidation_locks: dict = {}
32
96
 
33
- async def get(self, key: str, loader: Callable[[], T]) -> T:
34
- raise NotImplementedError
97
+ async def get(self, key: str) -> Optional[T]:
98
+ entry = await self.backend.get(key)
99
+ if not entry:
100
+ return None
35
101
 
102
+ if isinstance(entry, bytes):
103
+ entry = CacheEntry.from_json(entry)
36
104
 
37
- class StaleWhileRevalidateStrategy(CacheStrategy):
38
- def __init__(self, backend: Any, stale_ttl: int, cache_ttl: int):
39
- super().__init__(backend)
40
- self.stale_ttl = stale_ttl
41
- self.cache_ttl = cache_ttl
105
+ # If entry is stale but not expired, trigger background revalidation
106
+ if entry.is_stale() and not entry.is_expired():
107
+ if not entry.is_revalidating:
108
+ entry.is_revalidating = True
109
+ await self.backend.set(key, entry.to_json())
110
+ asyncio.create_task(self._revalidate(key))
111
+ return entry.value
42
112
 
43
- async def get(self, key: str, loader: Callable[[], T]) -> T:
44
- now = int(datetime.now().timestamp())
113
+ # If entry is expired, return None
114
+ if entry.is_expired():
115
+ return None
45
116
 
46
- # Try to get from cache
47
- cached_data = await self.backend.get(key)
48
- if cached_data:
49
- entry = CacheEntry.from_json(cached_data)
117
+ return entry.value
50
118
 
51
- if now < entry.stale_at:
52
- # Cache is fresh
53
- return entry.value
119
+ async def set(self, key: str, value: T, ttl: Optional[int] = None) -> None:
120
+ entry = CacheEntry(value=value, created_at=time.time(), ttl=ttl or self.ttl, revalidate_after=self.revalidate_after)
121
+ await self.backend.set(key, entry.to_json(), ttl=ttl)
54
122
 
55
- if now < entry.expires_at and not entry.is_revalidating:
56
- # Cache is stale but usable - trigger background revalidation
57
- entry.is_revalidating = True
58
- await self.backend.set(key, entry.to_json(), self.cache_ttl)
59
- asyncio.create_task(self._revalidate(key, loader))
60
- return entry.value
61
-
62
- # Cache miss or expired - load fresh data
63
- value = await loader()
64
- entry = CacheEntry(value=value, expires_at=now + self.cache_ttl, stale_at=now + (self.cache_ttl - self.stale_ttl))
65
- await self.backend.set(key, entry.to_json(), self.cache_ttl)
66
- return value
123
+ async def delete(self, key: str) -> None:
124
+ await self.backend.delete(key)
67
125
 
68
- async def _revalidate(self, key: str, loader: Callable[[], T]):
126
+ async def _revalidate(self, key: str) -> None:
127
+ """Background revalidation of cached data"""
69
128
  try:
70
- value = await loader()
71
- now = int(datetime.now().timestamp())
72
- entry = CacheEntry(value=value, expires_at=now + self.cache_ttl, stale_at=now + (self.cache_ttl - self.stale_ttl))
73
- await self.backend.set(key, entry.to_json(), self.cache_ttl)
74
- except Exception as e:
75
- logger.error(f"Revalidation failed for key {key}: {e}")
129
+ # Prevent multiple simultaneous revalidations
130
+ if key in self._revalidation_locks:
131
+ return
132
+ self._revalidation_locks[key] = True
76
133
 
134
+ # Get fresh data
135
+ fresh_value = await self.revalidate_fn(key)
77
136
 
78
- class CacheAsideStrategy(CacheStrategy):
79
- def __init__(self, backend: Any, ttl: int):
80
- super().__init__(backend)
81
- self.ttl = ttl
137
+ # Update cache with fresh data
138
+ await self.set(key, fresh_value)
139
+ finally:
140
+ self._revalidation_locks.pop(key, None)
82
141
 
83
- async def get(self, key: str, loader: Callable[[], T]) -> T:
84
- # Try to get from cache
85
- cached_data = await self.backend.get(key)
86
- if cached_data:
87
- entry = CacheEntry.from_json(cached_data)
88
- if entry.expires_at > int(datetime.now().timestamp()):
89
- return entry.value
90
-
91
- # Cache miss or expired - load from source
92
- value = await loader()
93
- entry = CacheEntry(value=value, expires_at=int(datetime.now().timestamp()) + self.ttl)
94
- await self.backend.set(key, entry.to_json(), self.ttl)
142
+
143
+ class CacheAsideStrategy(CacheStrategy[T]):
144
+ """
145
+ Implements cache-aside (lazy loading) strategy.
146
+ Data is loaded into cache only when requested.
147
+ """
148
+
149
+ def __init__(self, backend: BaseBackend, load_fn: Callable[[str], T], ttl: int, write_through: bool = False):
150
+ self.backend = backend
151
+ self.load_fn = load_fn
152
+ self.ttl = ttl
153
+ self.write_through = write_through
154
+
155
+ async def get(self, key: str) -> Optional[T]:
156
+ # Try to get from cache first
157
+ value = await self.backend.get(key)
158
+ if value:
159
+ if isinstance(value, bytes):
160
+ value = orjson.loads(value)
161
+ return value
162
+
163
+ # On cache miss, load from source
164
+ value = await self.load_fn(key)
165
+ if value is not None:
166
+ await self.set(key, value)
95
167
  return value
96
168
 
169
+ async def set(self, key: str, value: T, ttl: Optional[int] = None) -> None:
170
+ await self.backend.set(key, value, ttl or self.ttl)
171
+
172
+ # If write-through is enabled, update the source
173
+ if self.write_through:
174
+ await self._write_to_source(key, value)
175
+
176
+ async def delete(self, key: str) -> None:
177
+ await self.backend.delete(key)
97
178
 
98
- def cache_with_strategy(strategy: CacheStrategy, key_prefix: str = None):
179
+ async def _write_to_source(self, key: str, value: T) -> None:
180
+ """Write to the source in write-through mode"""
181
+ if hasattr(self.load_fn, "write"):
182
+ await self.load_fn.write(key, value)
183
+
184
+
185
+ def cache_with_strategy(strategy: CacheStrategy, key_prefix: str | None = None, ttl: int = 3600):
99
186
  """
100
187
  Decorator for using cache strategies
101
188
  """
@@ -105,10 +192,16 @@ def cache_with_strategy(strategy: CacheStrategy, key_prefix: str = None):
105
192
  # Generate cache key
106
193
  cache_key = f"{key_prefix or func.__name__}:{hash(str(args) + str(kwargs))}"
107
194
 
108
- async def loader():
109
- return await func(*args, **kwargs)
195
+ result = await strategy.get(cache_key)
196
+ if result is not None:
197
+ return result
198
+
199
+ # Execute function and cache result
200
+ result = await func(*args, **kwargs)
201
+ if result is not None:
202
+ await strategy.set(cache_key, result, ttl)
110
203
 
111
- return await strategy.get(cache_key, loader)
204
+ return result
112
205
 
113
206
  return wrapper
114
207
 
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
+ ]