half-orm 1.0.0rc13__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.0rc13 → half_orm-1.0.0rc15}/AUTHORS +1 -0
- {half_orm-1.0.0rc13/half_orm.egg-info → half_orm-1.0.0rc15}/PKG-INFO +3 -2
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/cli.py +1 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/field.py +33 -3
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/model.py +112 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/pg_meta.py +8 -2
- half_orm-1.0.0rc15/half_orm/version.txt +1 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15/half_orm.egg-info}/PKG-INFO +3 -2
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/pyproject.toml +1 -1
- half_orm-1.0.0rc13/half_orm/version.txt +0 -1
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/LICENSE +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/README.md +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/__init__.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/__main__.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/cli_utils.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/field_errors.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/fkey.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/hotest.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/migrations/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/migrations/__init__.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/model_errors.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/null.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/py.typed +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/relation.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/relation_errors.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/relation_factory.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/sql_adapter.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/sql_ast.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/testing.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/transaction.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/utils.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/SOURCES.txt +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/dependency_links.txt +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/entry_points.txt +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/requires.txt +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/top_level.txt +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/setup.cfg +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/test/test_cli.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/test/test_main.py +0 -0
- {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/test/test_sql_ast.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: half_orm
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc15
|
|
4
4
|
Summary: A database-first ORM for PostgreSQL
|
|
5
5
|
Author-email: Joël Maïzi <joel.maizi@collorg.org>
|
|
6
6
|
License: GPL-3.0
|
|
@@ -25,6 +25,7 @@ License-File: AUTHORS
|
|
|
25
25
|
Requires-Dist: psycopg[binary]
|
|
26
26
|
Requires-Dist: click
|
|
27
27
|
Requires-Dist: pyyaml
|
|
28
|
+
Dynamic: license-file
|
|
28
29
|
|
|
29
30
|
# halfORM
|
|
30
31
|
|
|
@@ -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
|
|
|
@@ -167,6 +171,20 @@ class Field():
|
|
|
167
171
|
"""
|
|
168
172
|
return bool(self.__metadata['notnull'])
|
|
169
173
|
|
|
174
|
+
@property
|
|
175
|
+
def has_default_value(self):
|
|
176
|
+
"""The default expression for this column, or ``None`` if there is none.
|
|
177
|
+
|
|
178
|
+
Returns the PostgreSQL expression string as stored in ``pg_attrdef``,
|
|
179
|
+
e.g. ``"nextval('seq'::regclass)"``, ``"'active'::text"``, ``"now()"``.
|
|
180
|
+
|
|
181
|
+
Example::
|
|
182
|
+
|
|
183
|
+
Author().id.has_default_value # "nextval('author_id_seq'::regclass)"
|
|
184
|
+
Author().last_name.has_default_value # None
|
|
185
|
+
"""
|
|
186
|
+
return self.__metadata.get('default_expr')
|
|
187
|
+
|
|
170
188
|
def __parse_json_schema(self):
|
|
171
189
|
desc = self.__metadata.get('fielddescription') or ''
|
|
172
190
|
m = re.search(r'@json\s*```yaml\s*(.*?)(?:```|\Z)', desc, re.DOTALL)
|
|
@@ -238,7 +256,7 @@ class Field():
|
|
|
238
256
|
cast = f'::{self.__sql_type}'
|
|
239
257
|
if col_is_array and comp == '=':
|
|
240
258
|
where_repr = f'{comp_str} = ANY({self.__praf(query, ho_id)})'
|
|
241
|
-
elif not self.unaccent:
|
|
259
|
+
elif not self.unaccent or not self._is_text_like:
|
|
242
260
|
where_repr = f"{self.__praf(query, ho_id)} {comp} {comp_str}{cast}"
|
|
243
261
|
else:
|
|
244
262
|
where_repr = f"unaccent({self.__praf(query, ho_id)}) {comp} unaccent({comp_str}{cast})"
|
|
@@ -282,7 +300,7 @@ class Field():
|
|
|
282
300
|
comp=comp,
|
|
283
301
|
placeholder=f'{comp_str}{cast}',
|
|
284
302
|
value=self,
|
|
285
|
-
unaccent=self.unaccent,
|
|
303
|
+
unaccent=self.unaccent and self._is_text_like,
|
|
286
304
|
array_any=is_array_any,
|
|
287
305
|
)
|
|
288
306
|
|
|
@@ -352,7 +370,7 @@ class Field():
|
|
|
352
370
|
self.__comp = '='
|
|
353
371
|
self.__unaccent = False
|
|
354
372
|
return
|
|
355
|
-
self.
|
|
373
|
+
self.unaccent = unaccent
|
|
356
374
|
comp = None
|
|
357
375
|
if isinstance(value, tuple):
|
|
358
376
|
if len(value) != 2:
|
|
@@ -414,10 +432,22 @@ class Field():
|
|
|
414
432
|
"""
|
|
415
433
|
return self.__unaccent
|
|
416
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
|
+
|
|
417
440
|
@unaccent.setter
|
|
418
441
|
def unaccent(self, value):
|
|
419
442
|
if not isinstance(value, bool):
|
|
420
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
|
|
421
451
|
self.__unaccent = value
|
|
422
452
|
|
|
423
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
|
|
|
@@ -111,7 +111,8 @@ SELECT
|
|
|
111
111
|
cn_fk.confrelid AS fkeytableid,
|
|
112
112
|
cn_fk.confkey AS fkeynum,
|
|
113
113
|
cn_fk.confupdtype as fkey_confupdtype,
|
|
114
|
-
cn_fk.confdeltype as fkey_confdeltype
|
|
114
|
+
cn_fk.confdeltype as fkey_confdeltype,
|
|
115
|
+
pg_get_expr(d.adbin, d.adrelid) AS default_expr
|
|
115
116
|
FROM
|
|
116
117
|
pg_class c -- table
|
|
117
118
|
LEFT JOIN pg_description tdesc ON
|
|
@@ -128,6 +129,9 @@ FROM
|
|
|
128
129
|
adesc.objsubid = a.attnum
|
|
129
130
|
JOIN pg_type pt ON
|
|
130
131
|
a.atttypid = pt.oid
|
|
132
|
+
LEFT JOIN pg_attrdef d ON
|
|
133
|
+
d.adrelid = a.attrelid AND
|
|
134
|
+
d.adnum = a.attnum
|
|
131
135
|
LEFT JOIN pg_constraint cn_uniq ON
|
|
132
136
|
cn_uniq.contype = 'u' AND
|
|
133
137
|
cn_uniq.conrelid = a.attrelid AND
|
|
@@ -175,7 +179,9 @@ GROUP BY
|
|
|
175
179
|
cn_fk.confrelid,
|
|
176
180
|
cn_fk.confkey,
|
|
177
181
|
cn_fk.confupdtype,
|
|
178
|
-
cn_fk.confdeltype
|
|
182
|
+
cn_fk.confdeltype,
|
|
183
|
+
d.adbin,
|
|
184
|
+
d.adrelid
|
|
179
185
|
ORDER BY
|
|
180
186
|
n.nspname, c.relname, a.attnum
|
|
181
187
|
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0rc15
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: half_orm
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc15
|
|
4
4
|
Summary: A database-first ORM for PostgreSQL
|
|
5
5
|
Author-email: Joël Maïzi <joel.maizi@collorg.org>
|
|
6
6
|
License: GPL-3.0
|
|
@@ -25,6 +25,7 @@ License-File: AUTHORS
|
|
|
25
25
|
Requires-Dist: psycopg[binary]
|
|
26
26
|
Requires-Dist: click
|
|
27
27
|
Requires-Dist: pyyaml
|
|
28
|
+
Dynamic: license-file
|
|
28
29
|
|
|
29
30
|
# halfORM
|
|
30
31
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0rc13
|
|
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
|