ecodev-core 0.0.57__tar.gz → 0.0.59__tar.gz
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.
Potentially problematic release.
This version of ecodev-core might be problematic. Click here for more details.
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/PKG-INFO +1 -1
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/__init__.py +8 -1
- ecodev_core-0.0.59/ecodev_core/db_i18n.py +213 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/db_upsertion.py +189 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/pyproject.toml +1 -1
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/LICENSE.md +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/README.md +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/app_activity.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/app_rights.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/app_user.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/auth_configuration.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/authentication.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/backup.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/check_dependencies.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/custom_equal.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/db_connection.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/db_filters.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/db_insertion.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/db_retrieval.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/deployment.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/email_sender.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/enum_utils.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/es_connection.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/list_utils.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/logger.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/pandas_utils.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/permissions.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/pydantic_utils.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/read_write.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/safe_utils.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/settings.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/sqlmodel_utils.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/token_banlist.py +0 -0
- {ecodev_core-0.0.57 → ecodev_core-0.0.59}/ecodev_core/version.py +0 -0
|
@@ -37,11 +37,17 @@ from ecodev_core.db_connection import engine
|
|
|
37
37
|
from ecodev_core.db_connection import get_session
|
|
38
38
|
from ecodev_core.db_connection import info_message
|
|
39
39
|
from ecodev_core.db_filters import ServerSideFilter
|
|
40
|
+
from ecodev_core.db_i18n import I18nMixin
|
|
41
|
+
from ecodev_core.db_i18n import Lang
|
|
42
|
+
from ecodev_core.db_i18n import get_lang
|
|
43
|
+
from ecodev_core.db_i18n import localized_col
|
|
44
|
+
from ecodev_core.db_i18n import set_lang
|
|
40
45
|
from ecodev_core.db_insertion import generic_insertion
|
|
41
46
|
from ecodev_core.db_insertion import get_raw_df
|
|
42
47
|
from ecodev_core.db_retrieval import count_rows
|
|
43
48
|
from ecodev_core.db_retrieval import get_rows
|
|
44
49
|
from ecodev_core.db_retrieval import ServerSideField
|
|
50
|
+
from ecodev_core.db_upsertion import add_missing_columns
|
|
45
51
|
from ecodev_core.db_upsertion import add_missing_enum_values
|
|
46
52
|
from ecodev_core.db_upsertion import field
|
|
47
53
|
from ecodev_core.db_upsertion import filter_to_sfield_dict
|
|
@@ -111,4 +117,5 @@ __all__ = [
|
|
|
111
117
|
'sort_by_keys', 'sort_by_values', 'Settings', 'load_yaml_file', 'Deployment', 'Version',
|
|
112
118
|
'sfield', 'field', 'upsert_df_data', 'upsert_deletor', 'get_row_versions', 'get_versions',
|
|
113
119
|
'db_to_value', 'upsert_data', 'upsert_selector', 'get_sfield_columns', 'filter_to_sfield_dict',
|
|
114
|
-
'SETTINGS', 'add_missing_enum_values', 'ban_token', 'TokenBanlist', 'is_banned'
|
|
120
|
+
'SETTINGS', 'add_missing_enum_values', 'ban_token', 'TokenBanlist', 'is_banned',
|
|
121
|
+
'get_lang', 'set_lang', 'Lang', 'localized_col', 'I18nMixin', 'add_missing_columns']
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing internationalization (i18n) for sqlmodel
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import contextvars
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import Label
|
|
10
|
+
from sqlalchemy import label
|
|
11
|
+
from sqlmodel import func
|
|
12
|
+
from sqlmodel.main import SQLModelMetaclass
|
|
13
|
+
|
|
14
|
+
from ecodev_core.list_utils import first_func_or_default
|
|
15
|
+
|
|
16
|
+
class Lang(str, Enum):
|
|
17
|
+
"""
|
|
18
|
+
Enum of the languages available for localization.
|
|
19
|
+
"""
|
|
20
|
+
EN = 'en'
|
|
21
|
+
FR = 'fr'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DB_Lang = 'db_Lang'
|
|
25
|
+
CONTEXT_DB_Lang = contextvars.ContextVar(DB_Lang, default=Lang.EN)
|
|
26
|
+
"""Context variables for storing the active database language, defaults to Lang.EN"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_lang(lang: Lang) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Sets the `CONTEXT_DB_Lang` context var.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
lang (Lang): The language to assign to the `CONTEXT_DB_Lang` context var.
|
|
35
|
+
"""
|
|
36
|
+
CONTEXT_DB_Lang.set(lang)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_lang() -> Lang:
|
|
40
|
+
"""
|
|
41
|
+
Fetches the value of `CONTEXT_DB_Lang` context var.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
lang (Lang): The value of `CONTEXT_DB_Lang` context var
|
|
45
|
+
"""
|
|
46
|
+
return Lang(CONTEXT_DB_Lang.get())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class I18nMixin:
|
|
50
|
+
"""
|
|
51
|
+
I18n (localization) mixin for string attributes of pydantic BaseModel classes.
|
|
52
|
+
|
|
53
|
+
Maps arbitrary string attributes of the class to their localized values. Localized fields
|
|
54
|
+
should be defined following the rules below :
|
|
55
|
+
- The field name must be defined as a key of the private attribute `__localized_fields__`
|
|
56
|
+
- Each field defined in `__localized_fields__` must be present as an attribute for \
|
|
57
|
+
each of its localized versions in the following format <field>_<lang>.
|
|
58
|
+
For example :
|
|
59
|
+
```
|
|
60
|
+
__localized_fields__ = {
|
|
61
|
+
'name':[Lang.EN, Lang.FR]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
assumes that the attributes `name_en` and `name_fr` are attribute of the class.
|
|
65
|
+
These attributes must have a type `str`.
|
|
66
|
+
- All localized field must have a localized version using `__fallback_lang__`
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
__localized_fields__ (dict[str, list[Lang]]): Mapping between localized fields and a \
|
|
70
|
+
list of their available localized versions. Defaults to {}.
|
|
71
|
+
__fallback_lang__ (Lang): Fallback locale if the requested localized version of a \
|
|
72
|
+
field is None. Defaults to Lang.EN.
|
|
73
|
+
"""
|
|
74
|
+
__localized_fields__: dict[str, list[Lang]] = {}
|
|
75
|
+
__fallback_lang__: Lang = Lang.EN
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _get_lang_chain(cls, field: str, lang: Optional[Lang] = None) -> list[Lang]:
|
|
79
|
+
"""
|
|
80
|
+
Returns the chain of localized versions of the requested field with the priority given
|
|
81
|
+
to the `lang` argument, followed by the lang returned by
|
|
82
|
+
[get_lang][ecodev_core.db_i18n.get_lang] and finally the lang defined in
|
|
83
|
+
`cls.__fallback_lang__`.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
field (str): Name of the attribute/field to localize
|
|
87
|
+
lang (Optional[Lang]): The requested locale language. If none, then uses that
|
|
88
|
+
returned by [get_lang][ecodev_core.db_i18n.get_lang]. Defaults to None.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
list[Lang]: List of Lang enums to use for generating the name of the localized \
|
|
92
|
+
fields.
|
|
93
|
+
"""
|
|
94
|
+
if field not in cls.__localized_fields__:
|
|
95
|
+
raise AttributeError(f'Field {field!r} is not internationalized.')
|
|
96
|
+
|
|
97
|
+
available_langs = cls.__localized_fields__[field]
|
|
98
|
+
|
|
99
|
+
if cls.__fallback_lang__ not in available_langs:
|
|
100
|
+
raise AttributeError(
|
|
101
|
+
f'Fallback language {cls.__fallback_lang__!r} not available for field {field!r}. '
|
|
102
|
+
f'Available: {available_langs}'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
lang = lang or get_lang()
|
|
106
|
+
if lang not in cls.__localized_fields__[field]:
|
|
107
|
+
raise AttributeError(f'Field {field!r} is not localized to {lang!r}')
|
|
108
|
+
|
|
109
|
+
return list(set([lang, cls.__fallback_lang__]))
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def _get_localized_field_name(cls, field: str, lang: Lang) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Returns the name of the localized version of `field` for the requested `lang` in the
|
|
115
|
+
following format <field>_<lang>.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
field (str): Name of the attribute/field to localize
|
|
119
|
+
lang (Optional[Lang]): The requested locale language.
|
|
120
|
+
Returns:
|
|
121
|
+
str: the name of the localized version of `field` for the requested `lang`
|
|
122
|
+
"""
|
|
123
|
+
return f'{field}_{lang.value}'
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def get_localized_field_chain(cls, field: str, lang: Optional[Lang] = None) -> list[str]:
|
|
127
|
+
"""
|
|
128
|
+
Returns a chain of the localized versions of the requested field with the priority given
|
|
129
|
+
to the `lang` argument, followed by the lang returned by
|
|
130
|
+
[get_lang][ecodev_core.db_i18n.get_lang] and finally
|
|
131
|
+
the lang defined in `cls.__fallback_lang__`
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
field (str): Name of the attribute/field to localize
|
|
135
|
+
lang (Optional[Lang]): The requested locale language. If none, then uses that
|
|
136
|
+
returned by [get_lang][ecodev_core.db_i18n.get_lang]. Defaults to None.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
list[str]: chain of the localized versions of the requested field.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
return [cls._get_localized_field_name(field, lang)
|
|
143
|
+
for lang in cls._get_lang_chain(field, lang)]
|
|
144
|
+
|
|
145
|
+
def _get_localized(self, field: str, lang: Optional[Lang] = None) -> Optional[str]:
|
|
146
|
+
"""
|
|
147
|
+
Returns the localized version of a field.
|
|
148
|
+
|
|
149
|
+
The localized version is returned following the rules defined below :
|
|
150
|
+
- If the requested localized version is not available then the an attempt \
|
|
151
|
+
will be made to localize the field using `__fallback_lang__`
|
|
152
|
+
- The specified language can be passed to `_get_localized`. If it is not passed, \
|
|
153
|
+
the value returned by [get_lang][ecodev_core.db_i18n.get_lang]
|
|
154
|
+
is used instead (Defaults to Lang.EN)
|
|
155
|
+
- If None is returned using the format <field>_<lang> for the language defined in \
|
|
156
|
+
`__localized_fields__` & `__fallback_lang__` is found then returns None
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
field (str): Name of the attribute/field to localize
|
|
160
|
+
lang (Optional[Lang]): Requested locale. If None, then fetched from \
|
|
161
|
+
[get_lang][ecodev_core.db_i18n.get_lang]. Defaults to None.
|
|
162
|
+
|
|
163
|
+
Return:
|
|
164
|
+
localized_field (Optional[str]): localized version of the field
|
|
165
|
+
"""
|
|
166
|
+
lang_chain = self._get_lang_chain(field=field, lang=lang)
|
|
167
|
+
|
|
168
|
+
for lang in lang_chain:
|
|
169
|
+
attr = self._get_localized_field_name(field=field, lang=lang)
|
|
170
|
+
value = getattr(self, attr, None)
|
|
171
|
+
if value:
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def __getattr__(self, item: str) -> Optional[str]:
|
|
177
|
+
"""
|
|
178
|
+
Overrides __getattr__ to get the localized value of a item if it figures in
|
|
179
|
+
`__localized_fields__`.
|
|
180
|
+
"""
|
|
181
|
+
if item in self.__localized_fields__:
|
|
182
|
+
return self._get_localized(item)
|
|
183
|
+
raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {item!r}')
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def localized_col(
|
|
187
|
+
field: str,
|
|
188
|
+
db_schema: SQLModelMetaclass,
|
|
189
|
+
lang: Optional[Lang] = None,
|
|
190
|
+
) -> Label:
|
|
191
|
+
"""
|
|
192
|
+
Returns the localized version of `field` for the requested `lang` of a
|
|
193
|
+
given SqlModel class. If `lang` is not specified, then fetches the active
|
|
194
|
+
locale from [get_lang][ecodev_core.db_i18n.get_lang].
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
field (str): Name of the field to localize
|
|
198
|
+
db_schema (SQLModelMetaclass): SQLModelMetaclass instance from which the localized \
|
|
199
|
+
fields will be fetched
|
|
200
|
+
lang (Optional[Lang]): Requested locale language. If None, then fetches language \
|
|
201
|
+
from [get_lang][ecodev_core.db_i18n.get_lang] Defaults to None.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
localized_field (Label): Localized version of the requested `field` wrapped in label \
|
|
205
|
+
with the name of `field`.
|
|
206
|
+
"""
|
|
207
|
+
if not issubclass(db_schema, I18nMixin):
|
|
208
|
+
raise TypeError(f"{db_schema.__name__} does not inherit from I18nMixin")
|
|
209
|
+
|
|
210
|
+
localized_fields_chain = db_schema.get_localized_field_chain(field, lang)
|
|
211
|
+
coalesce_fields = [getattr(db_schema, field_name) for field_name in localized_fields_chain]
|
|
212
|
+
|
|
213
|
+
return label(field, func.coalesce(*coalesce_fields))
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Module handling CRUD and version operations
|
|
3
3
|
"""
|
|
4
|
+
import enum
|
|
5
|
+
import json
|
|
6
|
+
import types
|
|
4
7
|
from datetime import datetime
|
|
5
8
|
from enum import EnumType
|
|
6
9
|
from functools import partial
|
|
7
10
|
from typing import Any
|
|
11
|
+
from typing import get_args
|
|
12
|
+
from typing import get_origin
|
|
13
|
+
from typing import Iterator
|
|
8
14
|
from typing import Union
|
|
9
15
|
|
|
10
16
|
import pandas as pd
|
|
11
17
|
import progressbar
|
|
18
|
+
from pydantic_core._pydantic_core import PydanticUndefined
|
|
12
19
|
from sqlmodel import and_
|
|
13
20
|
from sqlmodel import Field
|
|
14
21
|
from sqlmodel import inspect
|
|
@@ -191,3 +198,185 @@ def filter_to_sfield_dict(row: dict | SQLModelMetaclass,
|
|
|
191
198
|
"""
|
|
192
199
|
return {pk: getattr(row, pk)
|
|
193
200
|
for pk in get_sfield_columns(db_schema or row.__class__)}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def add_missing_columns(model: Any, session: Session) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Create all columns corresponding to fields in the passed model that are not yet columns in the
|
|
206
|
+
corresponding db table.
|
|
207
|
+
|
|
208
|
+
NB: The ORM not permitting to create new columns, we unfortunately have to rely on sqlalchemy
|
|
209
|
+
text sql statements.
|
|
210
|
+
|
|
211
|
+
NB2: As of 2025/10/01, handle the creation of int, float, str, bool, bytes, JSONB, Enum columns
|
|
212
|
+
|
|
213
|
+
NB3: possible to index columns, and to add foreign key.
|
|
214
|
+
|
|
215
|
+
NB4: Possible to have a non NULL default value
|
|
216
|
+
"""
|
|
217
|
+
table = model.__tablename__
|
|
218
|
+
current_cols, = get_existing_columns(table, session),
|
|
219
|
+
for col, py_type, fld in [(c, p, f) for c, p, f in _get_cols(model) if c not in current_cols]:
|
|
220
|
+
is_null = _is_type_nullable(py_type)
|
|
221
|
+
default = _get_default_value(fld, is_null)
|
|
222
|
+
_add_column(table, col, _py_type_to_sql(_clean_py_type(py_type)), default, is_null, session)
|
|
223
|
+
if getattr(fld, 'index', False):
|
|
224
|
+
_add_index(table, col, session)
|
|
225
|
+
if isinstance((fk := getattr(fld, 'foreign_key', None)), str) and fk.strip():
|
|
226
|
+
_add_foreign_key(f"{fk.split('.')[0]}(id)", table, col, session)
|
|
227
|
+
session.commit()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _get_default_value(fld: Any, nullable: bool) -> Any:
|
|
231
|
+
"""
|
|
232
|
+
Find if any the field default value
|
|
233
|
+
"""
|
|
234
|
+
if not nullable and hasattr(fld, 'default') and fld.default is not None:
|
|
235
|
+
return fld.default
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _add_column(table: str,
|
|
240
|
+
col: str,
|
|
241
|
+
sql_type: str,
|
|
242
|
+
default: Any,
|
|
243
|
+
nullable: bool,
|
|
244
|
+
session: Session
|
|
245
|
+
) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Add the new column with sql_type to the passed table
|
|
248
|
+
"""
|
|
249
|
+
session.execute(text(f'ALTER TABLE {table} ADD COLUMN {col} {sql_type} '
|
|
250
|
+
f'{_get_additional_request(col, sql_type, default, nullable)}'))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _get_additional_request(col: str, sql_type: str, default_value: Any, nullable: bool) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Add if any the default value for the passed col.
|
|
256
|
+
"""
|
|
257
|
+
if nullable:
|
|
258
|
+
return 'NULL'
|
|
259
|
+
|
|
260
|
+
if default_value is not None:
|
|
261
|
+
if (default_sql := _python_default_to_sql(default_value, sql_type)) == 'NULL':
|
|
262
|
+
raise ValueError(f'Non-nullable column {col} requires a default_value')
|
|
263
|
+
return f'DEFAULT {default_sql} NOT NULL'
|
|
264
|
+
|
|
265
|
+
raise ValueError(f'Non-nullable column {col} requires a default_value')
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _add_index(table: str, col: str, session: Session):
|
|
269
|
+
"""
|
|
270
|
+
Index the new table column
|
|
271
|
+
"""
|
|
272
|
+
session.execute(text(f'CREATE INDEX IF NOT EXISTS ix_{table}_{col} ON {table} ({col})'))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _add_foreign_key(fk: str, table: str, col: str, session: Session):
|
|
276
|
+
"""
|
|
277
|
+
Add a fk foreign key on the passed table column
|
|
278
|
+
"""
|
|
279
|
+
session.execute(text(
|
|
280
|
+
f'ALTER TABLE {table} ADD CONSTRAINT fk_{table}_{col} FOREIGN KEY ({col}) REFERENCES {fk}'))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _get_cols(model: Any) -> Iterator[tuple[str, Any, Any]]:
|
|
284
|
+
"""
|
|
285
|
+
Retrieve all fields and their corresponding sql types from the passed model
|
|
286
|
+
"""
|
|
287
|
+
for col, field in model.model_fields.items():
|
|
288
|
+
if (col_type := getattr(field, 'annotation', None)) is not None:
|
|
289
|
+
yield col, col_type, field
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_existing_columns(table_name: str, session: Session) -> set[str]:
|
|
293
|
+
"""
|
|
294
|
+
Retrieve all column names from the passed table
|
|
295
|
+
"""
|
|
296
|
+
result = session.execute(text('SELECT column_name FROM information_schema.columns WHERE '
|
|
297
|
+
'table_name = :table_name'), {'table_name': table_name})
|
|
298
|
+
return {r[0] for r in result}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _clean_py_type(col_type: Any) -> Any:
|
|
302
|
+
"""
|
|
303
|
+
Convert union and optional types to their non-None types, return directly passed type otherwise.
|
|
304
|
+
- Handle Python 3.10+ UnionType (aka X | Y)
|
|
305
|
+
- Unpack Optional types (Union[X, NoneType])
|
|
306
|
+
"""
|
|
307
|
+
if isinstance(col_type, types.UnionType):
|
|
308
|
+
if len((args := [t for t in col_type.__args__ if t is not type(None)])) == 1:
|
|
309
|
+
return args[0]
|
|
310
|
+
|
|
311
|
+
if get_origin(col_type) is Union:
|
|
312
|
+
if len((args := [t for t in get_args(col_type) if t is not type(None)])) == 1:
|
|
313
|
+
return args[0]
|
|
314
|
+
|
|
315
|
+
return col_type
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _is_type_nullable(col_type: Any) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Return True if col_type is Optional or Union[..., None].
|
|
321
|
+
"""
|
|
322
|
+
if isinstance(col_type, types.UnionType):
|
|
323
|
+
return type(None) in col_type.__args__
|
|
324
|
+
|
|
325
|
+
if get_origin(col_type) is Union:
|
|
326
|
+
return type(None) in get_args(col_type)
|
|
327
|
+
|
|
328
|
+
return col_type is type(None)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _python_default_to_sql(value: Any, sql_type: str) -> str:
|
|
332
|
+
"""
|
|
333
|
+
Convert Python default to SQL literal, handling common types.
|
|
334
|
+
"""
|
|
335
|
+
if value is None or value == PydanticUndefined:
|
|
336
|
+
return 'NULL'
|
|
337
|
+
if sql_type in ('VARCHAR', 'TEXT', 'CHAR'):
|
|
338
|
+
safe_value = value.replace("'", "''")
|
|
339
|
+
return f"'{safe_value}'"
|
|
340
|
+
if sql_type in ('INTEGER', 'FLOAT', 'NUMERIC', 'DOUBLE PRECISION'):
|
|
341
|
+
return str(value)
|
|
342
|
+
if sql_type == 'BOOLEAN':
|
|
343
|
+
return 'TRUE' if value else 'FALSE'
|
|
344
|
+
if sql_type == 'BYTEA':
|
|
345
|
+
if isinstance(value, bytes):
|
|
346
|
+
return f"decode('{value.hex()}', 'hex')"
|
|
347
|
+
raise ValueError('Default for BYTEA must be bytes')
|
|
348
|
+
if sql_type == 'JSONB':
|
|
349
|
+
json_str = json.dumps(value).replace("'", "''")
|
|
350
|
+
return f"'{json_str}'::jsonb"
|
|
351
|
+
if isinstance(value, enum.Enum):
|
|
352
|
+
return f"'{str(value.name)}'"
|
|
353
|
+
return str(value)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _py_type_to_sql(col_type: type) -> str:
|
|
357
|
+
"""
|
|
358
|
+
Convert a python type to a sql one. Only working for (as of 2025/10/01):
|
|
359
|
+
- int
|
|
360
|
+
- float
|
|
361
|
+
- str
|
|
362
|
+
- bool
|
|
363
|
+
- bytes
|
|
364
|
+
- jsonB
|
|
365
|
+
- Enum
|
|
366
|
+
NB: for enum, assumes type is already created in DB
|
|
367
|
+
"""
|
|
368
|
+
if col_type is str:
|
|
369
|
+
return 'VARCHAR'
|
|
370
|
+
if col_type is int:
|
|
371
|
+
return 'INTEGER'
|
|
372
|
+
if col_type is float:
|
|
373
|
+
return 'FLOAT'
|
|
374
|
+
if col_type is bool:
|
|
375
|
+
return 'BOOLEAN'
|
|
376
|
+
if col_type is bytes:
|
|
377
|
+
return 'BYTEA'
|
|
378
|
+
if col_type is dict:
|
|
379
|
+
return 'JSONB'
|
|
380
|
+
if hasattr(col_type, '__members__'):
|
|
381
|
+
return col_type.__name__.lower()
|
|
382
|
+
raise ValueError(f'Unsupported column type: {col_type}')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|