async-easy-model 0.2.9__py3-none-any.whl → 0.3.1__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.9"
15
+ __version__ = "0.3.1"
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
@@ -119,10 +119,10 @@ def get_foreign_keys_from_model(model_cls: Type[SQLModel]) -> Dict[str, str]:
119
119
 
120
120
  foreign_keys = {}
121
121
 
122
- # First method: Check SQLModel's __fields__ dictionary
123
- if hasattr(model_cls, "__fields__"):
124
- logger.info(f"Using __fields__ to find foreign keys in {model_cls.__name__}")
125
- for field_name, field_info in model_cls.__fields__.items():
122
+ # First method: Check SQLModel's model_fields dictionary (Pydantic V2)
123
+ if hasattr(model_cls, "model_fields"):
124
+ logger.info(f"Using model_fields to find foreign keys in {model_cls.__name__}")
125
+ for field_name, field_info in model_cls.model_fields.items():
126
126
  # Check if the field has a foreign_key attribute
127
127
  if hasattr(field_info, "foreign_key") and field_info.foreign_key:
128
128
  foreign_key = field_info.foreign_key
@@ -156,7 +156,7 @@ def get_foreign_keys_from_model(model_cls: Type[SQLModel]) -> Dict[str, str]:
156
156
  # Second method: Try to infer foreign keys from field names
157
157
  if not foreign_keys:
158
158
  logger.info(f"No foreign keys found in model {model_cls.__name__} metadata, trying to detect from field names")
159
- for field_name in getattr(model_cls, "__fields__", {}):
159
+ for field_name in getattr(model_cls, "model_fields", {}):
160
160
  if field_name.endswith("_id"):
161
161
  # Infer the referenced model from the field name
162
162
  model_name = field_name[:-3] # Remove _id suffix
@@ -593,7 +593,7 @@ def is_junction_table(model_cls: Type[SQLModel]) -> bool:
593
593
 
594
594
  # Check if all non-standard fields are foreign keys
595
595
  standard_fields = {'id', 'created_at', 'updated_at'}
596
- model_fields = set(getattr(model_cls, '__fields__', {}).keys())
596
+ model_fields = set(getattr(model_cls, 'model_fields', {}).keys())
597
597
  non_standard_fields = model_fields - standard_fields
598
598
  foreign_key_fields = set(foreign_keys.keys())
599
599
 
@@ -64,10 +64,26 @@ async def _create_table_without_indexes(table, connection):
64
64
  """
65
65
  # Create a copy of the table without indexes
66
66
  metadata = MetaData()
67
+ # Manually create columns instead of using deprecated copy() method
68
+ new_columns = []
69
+ for col in table.columns:
70
+ new_col = Column(
71
+ col.name,
72
+ col.type,
73
+ nullable=col.nullable,
74
+ default=col.default,
75
+ server_default=col.server_default,
76
+ primary_key=col.primary_key,
77
+ unique=col.unique,
78
+ autoincrement=col.autoincrement,
79
+ comment=col.comment
80
+ )
81
+ new_columns.append(new_col)
82
+
67
83
  new_table = Table(
68
84
  table.name,
69
85
  metadata,
70
- *[c.copy() for c in table.columns],
86
+ *new_columns,
71
87
  schema=table.schema
72
88
  )
73
89
 
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
@@ -107,13 +110,74 @@ class DatabaseConfig:
107
110
  # Global database configuration instance.
108
111
  db_config = DatabaseConfig()
109
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
+
110
174
  class EasyModel(SQLModel):
111
175
  """
112
176
  Base model class providing common async database operations.
113
177
  """
114
178
  id: Optional[int] = Field(default=None, primary_key=True)
115
- created_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(tz.utc))
116
- 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)
117
181
 
118
182
  # Default table args with extend_existing=True to ensure all subclasses can redefine tables
119
183
  __table_args__ = {"extend_existing": True}
@@ -331,7 +395,7 @@ class EasyModel(SQLModel):
331
395
  List of field names that have unique constraints
332
396
  """
333
397
  unique_fields = []
334
- for name, field in cls.__fields__.items():
398
+ for name, field in cls.model_fields.items():
335
399
  if name != 'id' and hasattr(field, "field_info") and field.field_info.extra.get('unique', False):
336
400
  unique_fields.append(name)
337
401
  return unique_fields
@@ -437,6 +501,9 @@ class EasyModel(SQLModel):
437
501
  try:
438
502
  processed_data = await cls._process_relationships_for_insert(session, data)
439
503
 
504
+ # Normalize datetime values for database compatibility
505
+ processed_data = _normalize_data_for_db(processed_data)
506
+
440
507
  # Create the model instance
441
508
  obj = cls(**processed_data)
442
509
  session.add(obj)
@@ -609,7 +676,7 @@ class EasyModel(SQLModel):
609
676
  """
610
677
  # Look for unique fields in the related model to use for searching
611
678
  unique_fields = []
612
- for name, field in related_model.__fields__.items():
679
+ for name, field in related_model.model_fields.items():
613
680
  if (hasattr(field, "field_info") and
614
681
  field.field_info.extra.get('unique', False)):
615
682
  unique_fields.append(name)
@@ -755,7 +822,7 @@ class EasyModel(SQLModel):
755
822
  # Check for unique constraints before updating
756
823
  for field_name, new_value in data.items():
757
824
  if field_name != 'id' and hasattr(cls, field_name):
758
- field = getattr(cls.__fields__.get(field_name), 'field_info', None)
825
+ field = getattr(cls.model_fields.get(field_name), 'field_info', None)
759
826
  if field and field.extra.get('unique', False):
760
827
  # Check if the new value would conflict with an existing record
761
828
  check_statement = select(cls).where(
@@ -769,9 +836,10 @@ class EasyModel(SQLModel):
769
836
  if existing:
770
837
  raise ValueError(f"Cannot update {field_name} to '{new_value}': value already exists")
771
838
 
772
- # Apply the updates
839
+ # Apply the updates with datetime normalization
773
840
  for key, value in data.items():
774
- setattr(record, key, value)
841
+ normalized_value = _normalize_datetime_for_db(value)
842
+ setattr(record, key, normalized_value)
775
843
 
776
844
  # Process many-to-many relationships if any
777
845
  for rel_name, rel_data in many_to_many_data.items():
@@ -1383,7 +1451,7 @@ class EasyModel(SQLModel):
1383
1451
  def _update_updated_at(session, flush_context, instances):
1384
1452
  for instance in session.dirty:
1385
1453
  if isinstance(instance, EasyModel) and hasattr(instance, "updated_at"):
1386
- instance.updated_at = datetime.now(tz.utc)
1454
+ instance.updated_at = _get_normalized_datetime()
1387
1455
 
1388
1456
  async def init_db(migrate: bool = True, model_classes: List[Type[SQLModel]] = None):
1389
1457
  """
@@ -80,7 +80,7 @@ class ModelVisualizer:
80
80
  foreign_keys = {}
81
81
 
82
82
  try:
83
- # Check model annotations and __fields__ for foreign keys
83
+ # Check model annotations and model_fields for foreign keys
84
84
  if hasattr(model_class, "__annotations__"):
85
85
  for field_name, field_type in model_class.__annotations__.items():
86
86
  if hasattr(model_class, field_name):
@@ -102,8 +102,8 @@ class ModelVisualizer:
102
102
  pass
103
103
 
104
104
  # Try to infer foreign keys from field names ending with _id
105
- if hasattr(model_class, "__fields__"):
106
- for field_name in model_class.__fields__:
105
+ if hasattr(model_class, "model_fields"):
106
+ for field_name in model_class.model_fields:
107
107
  if field_name.endswith("_id") and field_name not in foreign_keys:
108
108
  related_name = field_name[:-3] # Remove _id suffix
109
109
  # Check if there's a model with this name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-easy-model
3
- Version: 0.2.9
3
+ Version: 0.3.1
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=jZ6t8zToAbMXdEbFcOh15WlOa9uHYrO4QmoqxuZImCI,1921
2
+ async_easy_model/auto_relationships.py,sha256=ZYM3IdNBLJ5Q0jz9W9UuL379m0Y3ZVOCbngz0Xa2I_U,27782
3
+ async_easy_model/migrations.py,sha256=o0q2LjzGE8abjjpWDpB7G6EJt-nZOQ_oM2lm56GvS0E,18289
4
+ async_easy_model/model.py,sha256=FviTK-RQ0GMCQNiR8G1cACcrSY1jQMl7bPEg1mcD5WI,66514
5
+ async_easy_model/relationships.py,sha256=vR5BsJpGaDcecCcNlg9-ouZfxFXFQv5kOyiXhKp_T7A,3286
6
+ async_easy_model/visualization.py,sha256=rL3J_KhstR3UI-DxIvyjaDd60YueCnlfG_7E2Cf9i_E,29979
7
+ async_easy_model-0.3.1.dist-info/licenses/LICENSE,sha256=uwDkl6oHbRltW7vYKNc4doJyhtwhyrSNFFlPpKATwLE,1072
8
+ async_easy_model-0.3.1.dist-info/METADATA,sha256=YKgL1jBWsxPh7JrlKwhUOb0Jm-N-3kQaxtoO44Gtfus,13047
9
+ async_easy_model-0.3.1.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
10
+ async_easy_model-0.3.1.dist-info/top_level.txt,sha256=e5_47sGmJnyxz2msfwU6C316EqmrSd9RGIYwZyWx68E,17
11
+ async_easy_model-0.3.1.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- async_easy_model/__init__.py,sha256=eGTq3OypupyVvLe2Zyn3ZFBeuU4v8EXW-XQ047vUMoM,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=pBWRAVhazZuI_rBOnILPttiCF_ZEomWBJGDmfdFp8Nk,64243
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.9.dist-info/licenses/LICENSE,sha256=uwDkl6oHbRltW7vYKNc4doJyhtwhyrSNFFlPpKATwLE,1072
8
- async_easy_model-0.2.9.dist-info/METADATA,sha256=iVRM1qe6DfdFUSa5FYYES-oXgkKScm7m2BVClhlH-k8,12888
9
- async_easy_model-0.2.9.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
10
- async_easy_model-0.2.9.dist-info/top_level.txt,sha256=e5_47sGmJnyxz2msfwU6C316EqmrSd9RGIYwZyWx68E,17
11
- async_easy_model-0.2.9.dist-info/RECORD,,