half-orm 0.18.8__tar.gz → 0.18.10__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 (37) hide show
  1. {half_orm-0.18.8/half_orm.egg-info → half_orm-0.18.10}/PKG-INFO +1 -1
  2. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/fkey.py +29 -17
  3. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/relation.py +42 -6
  4. half_orm-0.18.10/half_orm/version.txt +1 -0
  5. {half_orm-0.18.8 → half_orm-0.18.10/half_orm.egg-info}/PKG-INFO +1 -1
  6. half_orm-0.18.8/half_orm/version.txt +0 -1
  7. {half_orm-0.18.8 → half_orm-0.18.10}/AUTHORS +0 -0
  8. {half_orm-0.18.8 → half_orm-0.18.10}/LICENSE +0 -0
  9. {half_orm-0.18.8 → half_orm-0.18.10}/README.md +0 -0
  10. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/__init__.py +0 -0
  11. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/__main__.py +0 -0
  12. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/cli.py +0 -0
  13. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/cli_utils.py +0 -0
  14. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/field.py +0 -0
  15. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/field_errors.py +0 -0
  16. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/hotest.py +0 -0
  17. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/model.py +0 -0
  18. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/model_errors.py +0 -0
  19. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/null.py +0 -0
  20. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/pg_meta.py +0 -0
  21. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/relation_errors.py +0 -0
  22. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/relation_factory.py +0 -0
  23. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/sql_adapter.py +0 -0
  24. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/sql_ast.py +0 -0
  25. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/transaction.py +0 -0
  26. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/utils.py +0 -0
  27. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/SOURCES.txt +0 -0
  28. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/dependency_links.txt +0 -0
  29. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/entry_points.txt +0 -0
  30. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/requires.txt +0 -0
  31. {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/top_level.txt +0 -0
  32. {half_orm-0.18.8 → half_orm-0.18.10}/pyproject.toml +0 -0
  33. {half_orm-0.18.8 → half_orm-0.18.10}/setup.cfg +0 -0
  34. {half_orm-0.18.8 → half_orm-0.18.10}/setup.py +0 -0
  35. {half_orm-0.18.8 → half_orm-0.18.10}/test/test_cli.py +0 -0
  36. {half_orm-0.18.8 → half_orm-0.18.10}/test/test_main.py +0 -0
  37. {half_orm-0.18.8 → half_orm-0.18.10}/test/test_sql_ast.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: half_orm
3
- Version: 0.18.8
3
+ Version: 0.18.10
4
4
  Summary: A simple PostgreSQL to Python mapper.
5
5
  Home-page: https://github.com/half-orm/half-orm
6
6
  Author: Joël Maïzi
@@ -119,40 +119,48 @@ class FKey:
119
119
  def values(self):
120
120
  return [list(elt.values()) for elt in self.__to_relation.ho_select(*self.__fk_names)]
121
121
 
122
- def set(self, __to=None):
122
+ def set(self, ho_to_rel=None, **kwargs):
123
123
  """Bind this foreign key to a relation, adding a JOIN condition.
124
124
 
125
- After calling ``.set(other_rel)``, queries on the owning relation
126
- automatically include a JOIN against ``other_rel`` filtered by
127
- ``other_rel``'s constraints.
125
+ After calling ``.set(...)``, queries on the owning relation
126
+ automatically include a JOIN against the related table filtered by
127
+ the given constraints.
128
128
 
129
- Called with no argument, ``.set()`` joins against all rows of the
130
- related table (tautological predicate) — equivalent to
131
- ``.set(RelatedClass())``.
129
+ Three calling forms no import of the related class required:
130
+
131
+ - ``.set()`` — joins all rows (tautological predicate).
132
+ - ``.set(field=value, ...)`` — joins with constraints, instantiating
133
+ the related class internally.
134
+ - ``.set(rel)`` — joins against an already-built
135
+ :class:`~half_orm.relation.Relation` instance.
132
136
 
133
137
  Args:
134
- __to (Relation | None): the relation to join against. If omitted,
135
- the related table is instantiated with no constraints.
138
+ ho_to_rel (Relation | None): an existing relation to join against.
139
+ Mutually exclusive with ``**kwargs``.
140
+ **kwargs: field constraints forwarded to the related class
141
+ constructor. Ignored when ``ho_to_rel`` is provided.
136
142
 
137
143
  Returns:
138
144
  self — for chaining.
139
145
 
140
146
  Raises:
141
- RuntimeError: if ``__to`` is not a :class:`~half_orm.relation.Relation` instance.
147
+ RuntimeError: if ``ho_to_rel`` is not a :class:`~half_orm.relation.Relation` instance.
148
+ RuntimeError: if ``ho_to_rel`` and ``**kwargs`` are both provided.
142
149
  RuntimeError: if setting this FK would create a cycle in the join graph.
143
150
 
144
151
  Example:
145
- is a post whose author's last name starts with 'Mar':
152
+ Without importing the related class:
146
153
  ```python
147
154
  post = Post()
148
- post.author_fk.set(Author(last_name=('like', 'Mar%')))
149
- print(post.ho_count())
155
+ post.author_fk.set() # all authors
156
+ post.author_fk.set(last_name=('like', 'Mar%')) # filtered
150
157
  ```
151
158
 
152
- join with all rows of the related table:
159
+ With an existing relation:
153
160
  ```python
154
161
  post = Post()
155
- post.author_fk.set() # equivalent to post.author_fk.set(Author())
162
+ post.author_fk.set(Author(last_name=('like', 'Mar%')))
163
+ # equivalent to: Author(last_name=('like', 'Mar%')).post_rfk().ho_count()
156
164
  ```
157
165
 
158
166
  *New in version 0.18.6:* raises ``RuntimeError`` if setting this FK would create a cycle in the join graph.
@@ -160,8 +168,12 @@ class FKey:
160
168
  # pylint: disable=import-outside-toplevel
161
169
  from half_orm.relation import Relation
162
170
 
163
- if __to is None:
164
- __to = self.__get_rel(normalize_qrn(self.__fk_fqrn))()
171
+ if ho_to_rel is not None and kwargs:
172
+ raise RuntimeError("FKey.set: cannot pass both a Relation and keyword arguments")
173
+ if ho_to_rel is None:
174
+ __to = self.__get_rel(normalize_qrn(self.__fk_fqrn))(**kwargs)
175
+ else:
176
+ __to = ho_to_rel
165
177
  if not issubclass(__to.__class__, Relation):
166
178
  raise RuntimeError("Fkey.set excepts an argument of type Relation")
167
179
  from_ = self.__relation
@@ -429,9 +429,15 @@ class Relation:
429
429
  def ho_assert_is_singleton(self):
430
430
  """Assert that this predicate identifies exactly one row, without querying the database.
431
431
 
432
- A predicate is a *singleton* when every field of a unique identifier
433
- (primary key or any ``UNIQUE NOT NULL`` constraint) is set with the
434
- ``=`` comparator. The check is purely structural no SQL is executed.
432
+ A predicate is a *singleton* when:
433
+
434
+ * every field of a unique identifier (primary key or any
435
+ ``UNIQUE NOT NULL`` constraint) is set with the ``=`` comparator, **or**
436
+ * a FK join constrains a unique identifier of this relation: the fields
437
+ on *this* side of the join form a PK or UNIQUE NOT NULL, and the
438
+ corresponding fields on the joined relation are all fixed with ``=``.
439
+
440
+ The check is purely structural — no SQL is executed.
435
441
 
436
442
  Returns:
437
443
  self — for chaining before a write operation.
@@ -465,6 +471,22 @@ class Relation:
465
471
  for ukey in self._ho_ukeys:
466
472
  if _fully_set(ukey):
467
473
  return self
474
+ # A FK join uniquely identifies self when:
475
+ # - the fields on self involved in the join form a PK or UNIQUE NOT NULL, AND
476
+ # - the corresponding fields on the joined relation are all fixed with '='.
477
+ def _all_eq(names, rel):
478
+ return all(
479
+ rel._ho_fields.get(n.strip('"')) is not None
480
+ and rel._ho_fields[n.strip('"')].is_set()
481
+ and rel._ho_fields[n.strip('"')]._comp() == '='
482
+ for n in names
483
+ )
484
+ for fkey, fk_rel in self._ho_join_to.items():
485
+ self_fields = frozenset(fkey.names)
486
+ on_pk = bool(self._ho_pkey) and self_fields == frozenset(self._ho_pkey.keys())
487
+ on_ukey = any(self_fields == frozenset(uk.keys()) for uk in self._ho_ukeys)
488
+ if (on_pk or on_ukey) and _all_eq(fkey.fk_names, fk_rel):
489
+ return self
468
490
  if not self._ho_pkey and not self._ho_ukeys:
469
491
  raise relation_errors.NotASingletonError(
470
492
  f"{self.__class__.__name__} has no primary key or unique NOT NULL constraint.")
@@ -896,10 +918,24 @@ Fkeys = {"""
896
918
  def ho_is_set(self):
897
919
  """Return True if one field at least is set or if self has been
898
920
  constrained by at least one of its foreign keys or self is the
899
- result of a combination of Relations (using set operators).
921
+ result of a combination of Relations (using set operators) where
922
+ at least one operand is itself constrained.
900
923
  """
901
- joined_to = bool(self._ho_join_to)
902
- return (joined_to or bool(self._ho_set_operators.operator) or bool(self._ho_neg) or
924
+ joined_to = any(jt_.ho_is_set() for jt_ in self._ho_join_to.values())
925
+ op = self._ho_set_operators.operator
926
+ if op:
927
+ left_set = self._ho_set_operators.left.ho_is_set()
928
+ right = self._ho_set_operators.right
929
+ right_set = right is not None and right.ho_is_set()
930
+ if op == "or":
931
+ # A() | B → all rows if any operand is unconstrained
932
+ set_op_constrained = left_set and right_set
933
+ else:
934
+ # "and" / "and not": unconstrained operand is transparent
935
+ set_op_constrained = left_set or right_set
936
+ else:
937
+ set_op_constrained = False
938
+ return (joined_to or set_op_constrained or bool(self._ho_neg) or
903
939
  bool({field for field in self._ho_fields.values() if field.is_set()}))
904
940
 
905
941
  def __get_set_fields(self):
@@ -0,0 +1 @@
1
+ 0.18.10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: half_orm
3
- Version: 0.18.8
3
+ Version: 0.18.10
4
4
  Summary: A simple PostgreSQL to Python mapper.
5
5
  Home-page: https://github.com/half-orm/half-orm
6
6
  Author: Joël Maïzi
@@ -1 +0,0 @@
1
- 0.18.8
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