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.
- async_easy_model/__init__.py +1 -1
- async_easy_model/auto_relationships.py +6 -6
- async_easy_model/migrations.py +17 -1
- async_easy_model/model.py +76 -8
- async_easy_model/visualization.py +3 -3
- {async_easy_model-0.2.9.dist-info → async_easy_model-0.3.1.dist-info}/METADATA +2 -1
- async_easy_model-0.3.1.dist-info/RECORD +11 -0
- async_easy_model-0.2.9.dist-info/RECORD +0 -11
- {async_easy_model-0.2.9.dist-info → async_easy_model-0.3.1.dist-info}/WHEEL +0 -0
- {async_easy_model-0.2.9.dist-info → async_easy_model-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {async_easy_model-0.2.9.dist-info → async_easy_model-0.3.1.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.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
|
123
|
-
if hasattr(model_cls, "
|
124
|
-
logger.info(f"Using
|
125
|
-
for field_name, field_info in model_cls.
|
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, "
|
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, '
|
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
|
|
async_easy_model/migrations.py
CHANGED
@@ -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
|
-
*
|
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=
|
116
|
-
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)
|
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.
|
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.
|
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.
|
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
|
-
|
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 =
|
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
|
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, "
|
106
|
-
for field_name in model_class.
|
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.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|