half-orm 1.0.0rc14__tar.gz → 1.0.0rc15__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.
- {half_orm-1.0.0rc14/half_orm.egg-info → half_orm-1.0.0rc15}/PKG-INFO +1 -1
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/cli.py +1 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/field.py +19 -3
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/model.py +112 -0
- half_orm-1.0.0rc15/half_orm/version.txt +1 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15/half_orm.egg-info}/PKG-INFO +1 -1
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/pyproject.toml +1 -1
- half_orm-1.0.0rc14/half_orm/version.txt +0 -1
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/AUTHORS +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/LICENSE +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/README.md +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/__init__.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/__main__.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/cli_utils.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/field_errors.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/fkey.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/hotest.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/migrations/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/migrations/__init__.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/model_errors.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/null.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/pg_meta.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/py.typed +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/relation.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/relation_errors.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/relation_factory.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/sql_adapter.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/sql_ast.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/testing.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/transaction.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm/utils.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm.egg-info/SOURCES.txt +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm.egg-info/dependency_links.txt +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm.egg-info/entry_points.txt +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm.egg-info/requires.txt +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/half_orm.egg-info/top_level.txt +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/setup.cfg +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/test/test_cli.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/test/test_main.py +0 -0
- {half_orm-1.0.0rc14 → half_orm-1.0.0rc15}/test/test_sql_ast.py +0 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import re
|
|
7
7
|
import sys
|
|
8
8
|
import typing
|
|
9
|
+
import warnings
|
|
9
10
|
import yaml
|
|
10
11
|
from collections.abc import Iterable
|
|
11
12
|
from half_orm.null import NULL
|
|
@@ -13,6 +14,9 @@ from half_orm.sql_adapter import SQL_ADAPTER
|
|
|
13
14
|
from half_orm.sql_ast import FieldExpr
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
_TEXT_LIKE_TYPES = {'text', 'varchar', 'character varying', 'char', 'bpchar', 'name', 'citext'}
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
class Expr:
|
|
17
21
|
"""A raw SQL expression for use with :meth:`Field.set`.
|
|
18
22
|
|
|
@@ -252,7 +256,7 @@ class Field():
|
|
|
252
256
|
cast = f'::{self.__sql_type}'
|
|
253
257
|
if col_is_array and comp == '=':
|
|
254
258
|
where_repr = f'{comp_str} = ANY({self.__praf(query, ho_id)})'
|
|
255
|
-
elif not self.unaccent:
|
|
259
|
+
elif not self.unaccent or not self._is_text_like:
|
|
256
260
|
where_repr = f"{self.__praf(query, ho_id)} {comp} {comp_str}{cast}"
|
|
257
261
|
else:
|
|
258
262
|
where_repr = f"unaccent({self.__praf(query, ho_id)}) {comp} unaccent({comp_str}{cast})"
|
|
@@ -296,7 +300,7 @@ class Field():
|
|
|
296
300
|
comp=comp,
|
|
297
301
|
placeholder=f'{comp_str}{cast}',
|
|
298
302
|
value=self,
|
|
299
|
-
unaccent=self.unaccent,
|
|
303
|
+
unaccent=self.unaccent and self._is_text_like,
|
|
300
304
|
array_any=is_array_any,
|
|
301
305
|
)
|
|
302
306
|
|
|
@@ -366,7 +370,7 @@ class Field():
|
|
|
366
370
|
self.__comp = '='
|
|
367
371
|
self.__unaccent = False
|
|
368
372
|
return
|
|
369
|
-
self.
|
|
373
|
+
self.unaccent = unaccent
|
|
370
374
|
comp = None
|
|
371
375
|
if isinstance(value, tuple):
|
|
372
376
|
if len(value) != 2:
|
|
@@ -428,10 +432,22 @@ class Field():
|
|
|
428
432
|
"""
|
|
429
433
|
return self.__unaccent
|
|
430
434
|
|
|
435
|
+
@property
|
|
436
|
+
def _is_text_like(self):
|
|
437
|
+
base_type = self.__sql_type.lstrip('_')
|
|
438
|
+
return base_type in _TEXT_LIKE_TYPES
|
|
439
|
+
|
|
431
440
|
@unaccent.setter
|
|
432
441
|
def unaccent(self, value):
|
|
433
442
|
if not isinstance(value, bool):
|
|
434
443
|
raise RuntimeError('unaccent value must be True or False!')
|
|
444
|
+
if value and not self._is_text_like:
|
|
445
|
+
warnings.warn(
|
|
446
|
+
f"unaccent ignored: field '{self.__name}' has non-text type '{self.__sql_type}'",
|
|
447
|
+
UserWarning,
|
|
448
|
+
stacklevel=2,
|
|
449
|
+
)
|
|
450
|
+
value = False
|
|
435
451
|
self.__unaccent = value
|
|
436
452
|
|
|
437
453
|
def _comp(self):
|
|
@@ -41,6 +41,18 @@ CONF_DIR = os.path.abspath(environ.get('HALFORM_CONF_DIR', '/etc/half_orm'))
|
|
|
41
41
|
|
|
42
42
|
# UUID is natively supported in psycopg 3
|
|
43
43
|
|
|
44
|
+
_SQL_TO_JSON = {
|
|
45
|
+
'uuid': 'string', 'text': 'string', 'varchar': 'string', 'bpchar': 'string',
|
|
46
|
+
'int4': 'integer', 'int8': 'integer', 'int2': 'integer',
|
|
47
|
+
'float4': 'number', 'float8': 'number', 'numeric': 'number',
|
|
48
|
+
'bool': 'boolean',
|
|
49
|
+
'date': 'date', 'timestamp': 'datetime', 'timestamptz': 'datetime',
|
|
50
|
+
'jsonb': 'json', 'json': 'json',
|
|
51
|
+
}
|
|
52
|
+
def _sql_to_json_type(sql_type: str) -> str:
|
|
53
|
+
base = sql_type.lstrip('_')
|
|
54
|
+
return _SQL_TO_JSON.get(base, 'string')
|
|
55
|
+
|
|
44
56
|
register = register_class
|
|
45
57
|
|
|
46
58
|
class Model:
|
|
@@ -167,6 +179,106 @@ class Model:
|
|
|
167
179
|
|
|
168
180
|
reconnect = __connect
|
|
169
181
|
|
|
182
|
+
def ho_meta(self) -> dict:
|
|
183
|
+
"""Return a structured description of all relations visible in the database scope.
|
|
184
|
+
|
|
185
|
+
For each relation (table, view, materialized view…) the returned dict maps
|
|
186
|
+
``'<schema>/<table>'`` keys to a metadata dict with the following structure::
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
'schema': str, # schema name
|
|
190
|
+
'table': str, # relation name
|
|
191
|
+
'kind': str, # 'r' table, 'v' view, 'm' matview, 'p' partition
|
|
192
|
+
'pk_fields': list[str], # primary-key column names
|
|
193
|
+
'fields': [
|
|
194
|
+
{
|
|
195
|
+
'name': str, # column name
|
|
196
|
+
'sql_type': str, # PostgreSQL type (e.g. 'text', 'int4')
|
|
197
|
+
'json_type': str, # JSON schema type (e.g. 'string', 'integer')
|
|
198
|
+
'is_pk': bool,
|
|
199
|
+
'not_null': bool,
|
|
200
|
+
'has_default': bool,
|
|
201
|
+
},
|
|
202
|
+
...
|
|
203
|
+
],
|
|
204
|
+
'fk_deps': [ # outgoing foreign keys
|
|
205
|
+
{
|
|
206
|
+
'local_fields': list[str],
|
|
207
|
+
'remote_schema': str,
|
|
208
|
+
'remote_table': str,
|
|
209
|
+
'remote_fields': list[str],
|
|
210
|
+
},
|
|
211
|
+
...
|
|
212
|
+
],
|
|
213
|
+
'reverse_fks': [ # incoming foreign keys
|
|
214
|
+
{
|
|
215
|
+
'local_fields': list[str],
|
|
216
|
+
'remote_schema': str,
|
|
217
|
+
'remote_table': str,
|
|
218
|
+
'remote_fields': list[str],
|
|
219
|
+
'is_singleton': bool,
|
|
220
|
+
},
|
|
221
|
+
...
|
|
222
|
+
],
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
dict: mapping ``'<schema>/<table>'`` → metadata dict (see above).
|
|
227
|
+
|
|
228
|
+
Example::
|
|
229
|
+
|
|
230
|
+
model = Model('halftest')
|
|
231
|
+
meta = model.ho_meta()
|
|
232
|
+
person = meta['actor/person']
|
|
233
|
+
print(person['kind']) # 'r'
|
|
234
|
+
print(person['pk_fields']) # ['id']
|
|
235
|
+
for f in person['fields']:
|
|
236
|
+
print(f['name'], f['sql_type'])
|
|
237
|
+
"""
|
|
238
|
+
result = {}
|
|
239
|
+
for kind, sfqrn, _ in self.desc():
|
|
240
|
+
dbname, schema, table = sfqrn
|
|
241
|
+
key = f'{schema}/{table}'
|
|
242
|
+
fields_meta = self._fields_metadata(sfqrn)
|
|
243
|
+
fkeys_meta = self._fkeys_metadata(sfqrn)
|
|
244
|
+
pk_fields = self._pkey_constraint(sfqrn)
|
|
245
|
+
|
|
246
|
+
fields = []
|
|
247
|
+
for fname, fdata in fields_meta.items():
|
|
248
|
+
fields.append({
|
|
249
|
+
'name': fname,
|
|
250
|
+
'sql_type': fdata['fieldtype'],
|
|
251
|
+
'json_type': _sql_to_json_type(fdata['fieldtype']), # helper à ajouter
|
|
252
|
+
'is_pk': bool(fdata.get('pkey')),
|
|
253
|
+
'not_null': bool(fdata.get('notnull')),
|
|
254
|
+
'has_default': fdata.get('default_expr') is not None,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
fk_deps, reverse_fks = [], []
|
|
258
|
+
for fk_name, fk_data in fkeys_meta.items():
|
|
259
|
+
ftable_key, ffields, local_fields, upd, del_, is_reverse, is_singleton = fk_data
|
|
260
|
+
_, r_schema, r_table = ftable_key
|
|
261
|
+
entry = {
|
|
262
|
+
'local_fields': local_fields,
|
|
263
|
+
'remote_schema': r_schema,
|
|
264
|
+
'remote_table': r_table,
|
|
265
|
+
'remote_fields': ffields,
|
|
266
|
+
}
|
|
267
|
+
if is_reverse:
|
|
268
|
+
entry['is_singleton'] = is_singleton
|
|
269
|
+
reverse_fks.append(entry)
|
|
270
|
+
else:
|
|
271
|
+
fk_deps.append(entry)
|
|
272
|
+
|
|
273
|
+
result[key] = {
|
|
274
|
+
'schema': schema, 'table': table, 'kind': kind,
|
|
275
|
+
'pk_fields': pk_fields,
|
|
276
|
+
'fields': fields,
|
|
277
|
+
'fk_deps': fk_deps,
|
|
278
|
+
'reverse_fks': reverse_fks,
|
|
279
|
+
}
|
|
280
|
+
return result
|
|
281
|
+
|
|
170
282
|
def get_relation_class(self, relation_name: str, fields_aliases: typing.Dict=None): # -> Relation
|
|
171
283
|
"""Generate a :class:`~half_orm.relation.Relation` subclass for a table or view.
|
|
172
284
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0rc15
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0rc14
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|