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.
@@ -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.2.8"
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
- kwargs = {}
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
- kwargs.update({
79
- "pool_size": 10,
80
- "max_overflow": 30,
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=lambda: datetime.now(tz.utc))
110
- updated_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(tz.utc))
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
- async with db_config.get_session_maker()() as session:
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
- setattr(record, key, value)
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 = datetime.now(tz.utc)
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.2.8
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.1)
2
+ Generator: setuptools (77.0.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,