async-easy-model 0.2.8__py3-none-any.whl → 0.3.0__py3-none-any.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.
- async_easy_model/__init__.py +1 -1
- async_easy_model/model.py +104 -15
- {async_easy_model-0.2.8.dist-info → async_easy_model-0.3.0.dist-info}/METADATA +2 -1
- async_easy_model-0.3.0.dist-info/RECORD +11 -0
- {async_easy_model-0.2.8.dist-info → async_easy_model-0.3.0.dist-info}/WHEEL +1 -1
- async_easy_model-0.2.8.dist-info/RECORD +0 -11
- {async_easy_model-0.2.8.dist-info → async_easy_model-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {async_easy_model-0.2.8.dist-info → async_easy_model-0.3.0.dist-info}/top_level.txt +0 -0
async_easy_model/__init__.py
CHANGED
@@ -12,7 +12,7 @@ from typing import Optional, Any
|
|
12
12
|
from .model import EasyModel, init_db, db_config
|
13
13
|
from sqlmodel import Field, Relationship as SQLModelRelationship
|
14
14
|
|
15
|
-
__version__ = "0.
|
15
|
+
__version__ = "0.3.0"
|
16
16
|
__all__ = ["EasyModel", "init_db", "db_config", "Field", "Relationship", "Relation", "enable_auto_relationships", "disable_auto_relationships", "process_auto_relationships", "MigrationManager", "check_and_migrate_models", "ModelVisualizer"]
|
17
17
|
|
18
18
|
# Create a more user-friendly Relationship function
|
async_easy_model/model.py
CHANGED
@@ -14,6 +14,9 @@ import re
|
|
14
14
|
|
15
15
|
T = TypeVar("T", bound="EasyModel")
|
16
16
|
|
17
|
+
# Global database configuration instance (forward declaration)
|
18
|
+
db_config = None
|
19
|
+
|
17
20
|
class DatabaseConfig:
|
18
21
|
_engine = None
|
19
22
|
_session_maker = None
|
@@ -73,15 +76,21 @@ class DatabaseConfig:
|
|
73
76
|
def get_engine(self):
|
74
77
|
"""Get or create the SQLAlchemy engine."""
|
75
78
|
if DatabaseConfig._engine is None:
|
76
|
-
|
79
|
+
# Apply connection pool configuration to all database types
|
80
|
+
# to prevent connection leaks and ensure proper resource management
|
81
|
+
kwargs = {
|
82
|
+
"pool_size": 10, # Base number of connections in the pool
|
83
|
+
"max_overflow": 30, # Additional connections allowed beyond pool_size
|
84
|
+
"pool_timeout": 30, # Timeout in seconds for getting connection from pool
|
85
|
+
"pool_recycle": 1800, # Recycle connections after 30 minutes
|
86
|
+
"pool_pre_ping": True, # Verify connections before use
|
87
|
+
}
|
88
|
+
|
89
|
+
# PostgreSQL-specific optimizations (if needed in the future)
|
77
90
|
if self.db_type == "postgresql":
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
"pool_timeout": 30,
|
82
|
-
"pool_recycle": 1800,
|
83
|
-
"pool_pre_ping": True,
|
84
|
-
})
|
91
|
+
# PostgreSQL already has good defaults above
|
92
|
+
pass
|
93
|
+
|
85
94
|
DatabaseConfig._engine = create_async_engine(
|
86
95
|
self.get_connection_url(),
|
87
96
|
**kwargs
|
@@ -101,13 +110,74 @@ class DatabaseConfig:
|
|
101
110
|
# Global database configuration instance.
|
102
111
|
db_config = DatabaseConfig()
|
103
112
|
|
113
|
+
def _normalize_datetime_for_db(value: Any) -> Any:
|
114
|
+
"""
|
115
|
+
Normalize datetime values for database compatibility.
|
116
|
+
|
117
|
+
For PostgreSQL with TIMESTAMP WITHOUT TIME ZONE columns, converts timezone-aware
|
118
|
+
datetimes to timezone-naive UTC datetimes. For other databases, returns the value unchanged.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
value: The value to potentially normalize
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
The normalized value
|
125
|
+
"""
|
126
|
+
global db_config
|
127
|
+
|
128
|
+
# Only process datetime objects
|
129
|
+
if not isinstance(value, datetime):
|
130
|
+
return value
|
131
|
+
|
132
|
+
# Only normalize for PostgreSQL
|
133
|
+
if db_config and db_config.db_type == "postgresql":
|
134
|
+
# If the datetime is timezone-aware, convert to UTC and make naive
|
135
|
+
if value.tzinfo is not None:
|
136
|
+
return value.astimezone(tz.utc).replace(tzinfo=None)
|
137
|
+
|
138
|
+
return value
|
139
|
+
|
140
|
+
def _normalize_data_for_db(data: Dict[str, Any]) -> Dict[str, Any]:
|
141
|
+
"""
|
142
|
+
Normalize all datetime values in a data dictionary for database compatibility.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
data: Dictionary containing field values
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
Dictionary with normalized datetime values
|
149
|
+
"""
|
150
|
+
normalized_data = {}
|
151
|
+
for key, value in data.items():
|
152
|
+
normalized_data[key] = _normalize_datetime_for_db(value)
|
153
|
+
return normalized_data
|
154
|
+
|
155
|
+
def _get_normalized_datetime() -> datetime:
|
156
|
+
"""
|
157
|
+
Get a datetime that's appropriate for the current database backend.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
For PostgreSQL: timezone-naive UTC datetime
|
161
|
+
For SQLite: timezone-aware UTC datetime (for backward compatibility)
|
162
|
+
"""
|
163
|
+
global db_config
|
164
|
+
|
165
|
+
utc_now = datetime.now(tz.utc)
|
166
|
+
|
167
|
+
# For PostgreSQL, return timezone-naive datetime
|
168
|
+
if db_config and db_config.db_type == "postgresql":
|
169
|
+
return utc_now.replace(tzinfo=None)
|
170
|
+
|
171
|
+
# For SQLite and others, return timezone-aware datetime (backward compatibility)
|
172
|
+
return utc_now
|
173
|
+
|
104
174
|
class EasyModel(SQLModel):
|
105
175
|
"""
|
106
176
|
Base model class providing common async database operations.
|
107
177
|
"""
|
108
178
|
id: Optional[int] = Field(default=None, primary_key=True)
|
109
|
-
created_at: Optional[datetime] = Field(default_factory=
|
110
|
-
updated_at: Optional[datetime] = Field(default_factory=
|
179
|
+
created_at: Optional[datetime] = Field(default_factory=_get_normalized_datetime)
|
180
|
+
updated_at: Optional[datetime] = Field(default_factory=_get_normalized_datetime)
|
111
181
|
|
112
182
|
# Default table args with extend_existing=True to ensure all subclasses can redefine tables
|
113
183
|
__table_args__ = {"extend_existing": True}
|
@@ -115,9 +185,24 @@ class EasyModel(SQLModel):
|
|
115
185
|
@classmethod
|
116
186
|
@contextlib.asynccontextmanager
|
117
187
|
async def get_session(cls):
|
118
|
-
"""Provide a transactional scope for database operations.
|
119
|
-
|
188
|
+
"""Provide a transactional scope for database operations.
|
189
|
+
|
190
|
+
This method ensures proper session cleanup by:
|
191
|
+
- Explicitly rolling back transactions on exceptions
|
192
|
+
- Explicitly closing sessions in all cases
|
193
|
+
- Proper exception propagation
|
194
|
+
"""
|
195
|
+
session = None
|
196
|
+
try:
|
197
|
+
session = db_config.get_session_maker()()
|
120
198
|
yield session
|
199
|
+
except Exception:
|
200
|
+
if session:
|
201
|
+
await session.rollback()
|
202
|
+
raise
|
203
|
+
finally:
|
204
|
+
if session:
|
205
|
+
await session.close()
|
121
206
|
|
122
207
|
@classmethod
|
123
208
|
def _get_relationship_fields(cls) -> List[str]:
|
@@ -416,6 +501,9 @@ class EasyModel(SQLModel):
|
|
416
501
|
try:
|
417
502
|
processed_data = await cls._process_relationships_for_insert(session, data)
|
418
503
|
|
504
|
+
# Normalize datetime values for database compatibility
|
505
|
+
processed_data = _normalize_data_for_db(processed_data)
|
506
|
+
|
419
507
|
# Create the model instance
|
420
508
|
obj = cls(**processed_data)
|
421
509
|
session.add(obj)
|
@@ -748,9 +836,10 @@ class EasyModel(SQLModel):
|
|
748
836
|
if existing:
|
749
837
|
raise ValueError(f"Cannot update {field_name} to '{new_value}': value already exists")
|
750
838
|
|
751
|
-
# Apply the updates
|
839
|
+
# Apply the updates with datetime normalization
|
752
840
|
for key, value in data.items():
|
753
|
-
|
841
|
+
normalized_value = _normalize_datetime_for_db(value)
|
842
|
+
setattr(record, key, normalized_value)
|
754
843
|
|
755
844
|
# Process many-to-many relationships if any
|
756
845
|
for rel_name, rel_data in many_to_many_data.items():
|
@@ -1362,7 +1451,7 @@ class EasyModel(SQLModel):
|
|
1362
1451
|
def _update_updated_at(session, flush_context, instances):
|
1363
1452
|
for instance in session.dirty:
|
1364
1453
|
if isinstance(instance, EasyModel) and hasattr(instance, "updated_at"):
|
1365
|
-
instance.updated_at =
|
1454
|
+
instance.updated_at = _get_normalized_datetime()
|
1366
1455
|
|
1367
1456
|
async def init_db(migrate: bool = True, model_classes: List[Type[SQLModel]] = None):
|
1368
1457
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: async-easy-model
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: A simplified SQLModel-based ORM for async database operations
|
5
5
|
Home-page: https://github.com/puntorigen/easy-model
|
6
6
|
Author: Pablo Schaffner
|
@@ -59,6 +59,7 @@ A simplified SQLModel-based ORM for async database operations in Python. async-e
|
|
59
59
|
- 🛠️ Built on top of SQLModel and SQLAlchemy for robust performance
|
60
60
|
- 📝 Type hints for better IDE support
|
61
61
|
- 🕒 Automatic `id`, `created_at` and `updated_at` fields provided by default
|
62
|
+
- ⏰ **PostgreSQL DateTime Compatibility**: Automatic timezone-aware to timezone-naive datetime conversion for PostgreSQL TIMESTAMP WITHOUT TIME ZONE columns
|
62
63
|
- 🔄 Automatic schema migrations for evolving database models
|
63
64
|
- 📊 Visualization of database schema using Mermaid ER diagrams
|
64
65
|
|
@@ -0,0 +1,11 @@
|
|
1
|
+
async_easy_model/__init__.py,sha256=IXheLaQx-dpGPL6oBEaaIftZDihlFB7bwzEWfMlZ4Ts,1921
|
2
|
+
async_easy_model/auto_relationships.py,sha256=V2LAzNi7y-keFk4C_m-byVRM-k_7nL5HEy9Ig3nEdq8,27756
|
3
|
+
async_easy_model/migrations.py,sha256=rYDGCGlruSugAmPfdIF2-uhyG6UvC_2qbF3BXJ084qI,17803
|
4
|
+
async_easy_model/model.py,sha256=XxlFOYiX8HT1B6PBB3RubuvB5j4aYKEyO-sPA202Y2U,66508
|
5
|
+
async_easy_model/relationships.py,sha256=vR5BsJpGaDcecCcNlg9-ouZfxFXFQv5kOyiXhKp_T7A,3286
|
6
|
+
async_easy_model/visualization.py,sha256=RVCdc8j3uUQe-zy3jXju_yhA13qJ8KWVbQ5fQyjyqkA,29973
|
7
|
+
async_easy_model-0.3.0.dist-info/licenses/LICENSE,sha256=uwDkl6oHbRltW7vYKNc4doJyhtwhyrSNFFlPpKATwLE,1072
|
8
|
+
async_easy_model-0.3.0.dist-info/METADATA,sha256=0PEppZL9WiSl1uKDqlC-f-l9rrrM-x5HVcRWBcRgsJU,13047
|
9
|
+
async_easy_model-0.3.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
10
|
+
async_easy_model-0.3.0.dist-info/top_level.txt,sha256=e5_47sGmJnyxz2msfwU6C316EqmrSd9RGIYwZyWx68E,17
|
11
|
+
async_easy_model-0.3.0.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
async_easy_model/__init__.py,sha256=8wTdiTMrhwZjclRALXktyRIBJ4ONt-F0JG_CK60yz9A,1921
|
2
|
-
async_easy_model/auto_relationships.py,sha256=V2LAzNi7y-keFk4C_m-byVRM-k_7nL5HEy9Ig3nEdq8,27756
|
3
|
-
async_easy_model/migrations.py,sha256=rYDGCGlruSugAmPfdIF2-uhyG6UvC_2qbF3BXJ084qI,17803
|
4
|
-
async_easy_model/model.py,sha256=f0eMcIaDOz9s01A4jpQ-T_VpCgFt67JF1puDYuhpdv4,63290
|
5
|
-
async_easy_model/relationships.py,sha256=vR5BsJpGaDcecCcNlg9-ouZfxFXFQv5kOyiXhKp_T7A,3286
|
6
|
-
async_easy_model/visualization.py,sha256=RVCdc8j3uUQe-zy3jXju_yhA13qJ8KWVbQ5fQyjyqkA,29973
|
7
|
-
async_easy_model-0.2.8.dist-info/licenses/LICENSE,sha256=uwDkl6oHbRltW7vYKNc4doJyhtwhyrSNFFlPpKATwLE,1072
|
8
|
-
async_easy_model-0.2.8.dist-info/METADATA,sha256=29ESU7RvrK94YP0v2iwy_v8f06nFLs96D3hPJJibO1U,12888
|
9
|
-
async_easy_model-0.2.8.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
|
10
|
-
async_easy_model-0.2.8.dist-info/top_level.txt,sha256=e5_47sGmJnyxz2msfwU6C316EqmrSd9RGIYwZyWx68E,17
|
11
|
-
async_easy_model-0.2.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|