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.
Files changed (40) hide show
  1. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/AUTHORS +1 -0
  2. {half_orm-1.0.0rc13/half_orm.egg-info → half_orm-1.0.0rc15}/PKG-INFO +3 -2
  3. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/cli.py +1 -0
  4. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/field.py +33 -3
  5. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/model.py +112 -0
  6. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/pg_meta.py +8 -2
  7. half_orm-1.0.0rc15/half_orm/version.txt +1 -0
  8. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15/half_orm.egg-info}/PKG-INFO +3 -2
  9. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/pyproject.toml +1 -1
  10. half_orm-1.0.0rc13/half_orm/version.txt +0 -1
  11. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/LICENSE +0 -0
  12. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/README.md +0 -0
  13. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/__init__.py +0 -0
  14. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/__main__.py +0 -0
  15. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/cli_utils.py +0 -0
  16. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/field_errors.py +0 -0
  17. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/fkey.py +0 -0
  18. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/hotest.py +0 -0
  19. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/migrations/BREAKING_CHANGES-1.0.0.md +0 -0
  20. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/migrations/__init__.py +0 -0
  21. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/model_errors.py +0 -0
  22. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/null.py +0 -0
  23. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/py.typed +0 -0
  24. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/relation.py +0 -0
  25. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/relation_errors.py +0 -0
  26. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/relation_factory.py +0 -0
  27. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/sql_adapter.py +0 -0
  28. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/sql_ast.py +0 -0
  29. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/testing.py +0 -0
  30. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/transaction.py +0 -0
  31. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm/utils.py +0 -0
  32. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/SOURCES.txt +0 -0
  33. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/dependency_links.txt +0 -0
  34. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/entry_points.txt +0 -0
  35. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/requires.txt +0 -0
  36. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/half_orm.egg-info/top_level.txt +0 -0
  37. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/setup.cfg +0 -0
  38. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/test/test_cli.py +0 -0
  39. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/test/test_main.py +0 -0
  40. {half_orm-1.0.0rc13 → half_orm-1.0.0rc15}/test/test_sql_ast.py +0 -0
@@ -1,2 +1,3 @@
1
1
  # This is the list of half_orm's significant contributors.
2
2
  # Hopefully, it will become larger...
3
+ Joël Maizi
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: half_orm
3
- Version: 1.0.0rc13
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
 
@@ -98,6 +98,7 @@ OFFICIAL_EXTENSIONS = {
98
98
  'half_orm_test_extension',
99
99
  'half_orm_inspect',
100
100
  'half_orm_dev',
101
+ 'half_orm_gen'
101
102
  }
102
103
 
103
104
  def get_config_file():
@@ -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.__unaccent = unaccent
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
1
+ Metadata-Version: 2.4
2
2
  Name: half_orm
3
- Version: 1.0.0rc13
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__"
4
4
 
5
5
  [project]
6
6
  name = "half_orm"
7
- version = "1.0.0rc13"
7
+ version = "1.0.0rc15"
8
8
  description = "A database-first ORM for PostgreSQL"
9
9
  readme = "README.md"
10
10
  license = { text = "GPL-3.0" }
@@ -1 +0,0 @@
1
- 1.0.0rc13
File without changes
File without changes
File without changes