aflib 0.0.7__tar.gz → 0.0.8__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 (36) hide show
  1. aflib-0.0.7/README.md → aflib-0.0.8/PKG-INFO +22 -5
  2. aflib-0.0.7/PKG-INFO → aflib-0.0.8/README.md +13 -14
  3. {aflib-0.0.7 → aflib-0.0.8}/pyproject.toml +1 -1
  4. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/__init__.py +1 -1
  5. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/batch.py +17 -2
  6. {aflib-0.0.7 → aflib-0.0.8}/tests/test_batch.py +20 -2
  7. {aflib-0.0.7 → aflib-0.0.8}/.gitignore +0 -0
  8. {aflib-0.0.7 → aflib-0.0.8}/LICENSE +0 -0
  9. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/authz.py +0 -0
  10. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/codec.py +0 -0
  11. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/db.py +0 -0
  12. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/errors.py +0 -0
  13. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/events.py +0 -0
  14. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/functions.py +0 -0
  15. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/identity.py +0 -0
  16. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/logging.py +0 -0
  17. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/params.py +0 -0
  18. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/result.py +0 -0
  19. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/transaction.py +0 -0
  20. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/trigger.py +0 -0
  21. {aflib-0.0.7 → aflib-0.0.8}/src/aflib/workflow.py +0 -0
  22. {aflib-0.0.7 → aflib-0.0.8}/tests/conftest.py +0 -0
  23. {aflib-0.0.7 → aflib-0.0.8}/tests/test_authz.py +0 -0
  24. {aflib-0.0.7 → aflib-0.0.8}/tests/test_codec.py +0 -0
  25. {aflib-0.0.7 → aflib-0.0.8}/tests/test_db.py +0 -0
  26. {aflib-0.0.7 → aflib-0.0.8}/tests/test_errors.py +0 -0
  27. {aflib-0.0.7 → aflib-0.0.8}/tests/test_events.py +0 -0
  28. {aflib-0.0.7 → aflib-0.0.8}/tests/test_functions.py +0 -0
  29. {aflib-0.0.7 → aflib-0.0.8}/tests/test_identity.py +0 -0
  30. {aflib-0.0.7 → aflib-0.0.8}/tests/test_logging.py +0 -0
  31. {aflib-0.0.7 → aflib-0.0.8}/tests/test_params.py +0 -0
  32. {aflib-0.0.7 → aflib-0.0.8}/tests/test_public_api.py +0 -0
  33. {aflib-0.0.7 → aflib-0.0.8}/tests/test_result.py +0 -0
  34. {aflib-0.0.7 → aflib-0.0.8}/tests/test_transaction.py +0 -0
  35. {aflib-0.0.7 → aflib-0.0.8}/tests/test_trigger.py +0 -0
  36. {aflib-0.0.7 → aflib-0.0.8}/tests/test_workflow.py +0 -0
@@ -1,3 +1,12 @@
1
+ Metadata-Version: 2.5
2
+ Name: aflib
3
+ Version: 0.0.8
4
+ Summary: Helper library for business logic inside Airflows plpython function bodies
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
1
10
  # aflib
2
11
 
3
12
  Helper library for business logic inside [Airflows](https://flows.ninja) `plpython3u`
@@ -18,16 +27,24 @@ so low.
18
27
  Airflows resolves Python packages from public PyPI by name and version only; there is no
19
28
  local-import, git or private-index route. Declare it in
20
29
  `models/pythonPackages/pythonPackages.airflows` by pinning **name and version in
21
- separate fields** — never as a single `aflib==0.0.7` string (that form produced
30
+ separate fields** — never as a single `aflib==0.0.8` string (that form produced
22
31
  malformed `name==ver==ver` entries in a real instance's package list):
23
32
 
24
33
  ```
25
34
  name: aflib
26
- version: 0.0.7
35
+ version: 0.0.8
27
36
  ```
28
37
 
29
38
  Then import it from a function body.
30
39
 
40
+ ## What 0.0.8 fixes
41
+
42
+ `Batch.run` accepts a predicate whose column name contains the letters `_t`.
43
+ `id_tuition IS NOT NULL`, `start_time < now()` and `is_test = false` are
44
+ predicates. 0.0.7 refused them, because the guard searched for `_t` as a
45
+ substring. `_t.col` and `"_t".col` are still refused: that alias exists only on
46
+ the outer `UPDATE`.
47
+
31
48
  ## What 0.0.7 gives you
32
49
 
33
50
  Everything in 0.0.6, plus three capabilities that ran on a real instance on
@@ -82,10 +99,10 @@ the 2026-09 platform probes showed, plus two verified fixes:
82
99
  resolve.
83
100
  - **`identity.current_user(plpy)`** — `SELECT current_user`. Not a User id.
84
101
 
85
- Pin **name and version in separate fields** (`aflib` / `0.0.7`), not
86
- `aflib==0.0.7` as one string.
102
+ Pin **name and version in separate fields** (`aflib` / `0.0.8`), not
103
+ `aflib==0.0.8` as one string.
87
104
 
88
- ## Core API (0.0.1–0.0.3, still in 0.0.7)
105
+ ## Core API (0.0.1–0.0.3, still in 0.0.8)
89
106
 
90
107
  ```python
91
108
  from aflib import Db, errors, fail, logging, ok, params
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: aflib
3
- Version: 0.0.7
4
- Summary: Helper library for business logic inside Airflows plpython function bodies
5
- License: MIT
6
- License-File: LICENSE
7
- Requires-Python: >=3.11
8
- Description-Content-Type: text/markdown
9
-
10
1
  # aflib
11
2
 
12
3
  Helper library for business logic inside [Airflows](https://flows.ninja) `plpython3u`
@@ -27,16 +18,24 @@ so low.
27
18
  Airflows resolves Python packages from public PyPI by name and version only; there is no
28
19
  local-import, git or private-index route. Declare it in
29
20
  `models/pythonPackages/pythonPackages.airflows` by pinning **name and version in
30
- separate fields** — never as a single `aflib==0.0.7` string (that form produced
21
+ separate fields** — never as a single `aflib==0.0.8` string (that form produced
31
22
  malformed `name==ver==ver` entries in a real instance's package list):
32
23
 
33
24
  ```
34
25
  name: aflib
35
- version: 0.0.7
26
+ version: 0.0.8
36
27
  ```
37
28
 
38
29
  Then import it from a function body.
39
30
 
31
+ ## What 0.0.8 fixes
32
+
33
+ `Batch.run` accepts a predicate whose column name contains the letters `_t`.
34
+ `id_tuition IS NOT NULL`, `start_time < now()` and `is_test = false` are
35
+ predicates. 0.0.7 refused them, because the guard searched for `_t` as a
36
+ substring. `_t.col` and `"_t".col` are still refused: that alias exists only on
37
+ the outer `UPDATE`.
38
+
40
39
  ## What 0.0.7 gives you
41
40
 
42
41
  Everything in 0.0.6, plus three capabilities that ran on a real instance on
@@ -91,10 +90,10 @@ the 2026-09 platform probes showed, plus two verified fixes:
91
90
  resolve.
92
91
  - **`identity.current_user(plpy)`** — `SELECT current_user`. Not a User id.
93
92
 
94
- Pin **name and version in separate fields** (`aflib` / `0.0.7`), not
95
- `aflib==0.0.7` as one string.
93
+ Pin **name and version in separate fields** (`aflib` / `0.0.8`), not
94
+ `aflib==0.0.8` as one string.
96
95
 
97
- ## Core API (0.0.1–0.0.3, still in 0.0.7)
96
+ ## Core API (0.0.1–0.0.3, still in 0.0.8)
98
97
 
99
98
  ```python
100
99
  from aflib import Db, errors, fail, logging, ok, params
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aflib"
3
- version = "0.0.7"
3
+ version = "0.0.8"
4
4
  description = "Helper library for business logic inside Airflows plpython function bodies"
5
5
  readme = "README.md"
6
6
  # The platform runs Python 3.11.2 (observed, not assumed — see docs/platform-constraints.md).
@@ -56,7 +56,7 @@ from aflib.result import fail, ok
56
56
  # Kept in step with `pyproject.toml` by hand. A deployed body returns this so a run can
57
57
  # prove which aflib it actually imported — a stale package otherwise looks exactly like a
58
58
  # working one.
59
- __version__ = "0.0.7"
59
+ __version__ = "0.0.8"
60
60
 
61
61
  __all__ = [
62
62
  "BadParameter",
@@ -54,6 +54,7 @@ inside the environment's `statement_timeout` — 180 s on PRO, no limit on PRE,
54
54
  cannot tell you whether a chunk fits**.
55
55
  """
56
56
 
57
+ import re
57
58
  import time
58
59
 
59
60
  from aflib.db import Db
@@ -88,6 +89,18 @@ class Progress(object):
88
89
  self.done, self.remaining, self.complete)
89
90
 
90
91
 
92
+ def _alias_as_qualifier(alias, pending):
93
+ """True when `pending` uses `alias` as a qualifier, not as letters inside a name.
94
+
95
+ Searching for the substring `_t` refuses `id_tuition IS NOT NULL`,
96
+ `start_time < now()` and `is_test = false`. Observed on PRE 2026-10-02,
97
+ acceptance probe `aflib-007-accept-r1`, case `batch`. The alias is a problem
98
+ only as `_t.col` or `"_t".col`, with optional space before the dot.
99
+ """
100
+ pattern = r'(?<![\w"])"?{0}"?\s*\.'.format(re.escape(alias))
101
+ return re.search(pattern, pending) is not None
102
+
103
+
91
104
  class Batch(object):
92
105
  """One committed chunk per call. `plpy` is injected, as everywhere in aflib."""
93
106
 
@@ -105,14 +118,16 @@ class Batch(object):
105
118
  the outer `WHERE`, where Postgres resolves it against the update target, and
106
119
  inside the subquery, where it resolves against the subquery's own `FROM`. Both
107
120
  resolutions are what we want, and either qualification breaks one of them. A
108
- predicate mentioning the alias is rejected rather than silently misplaced.
121
+ predicate that qualifies a column with the alias (`_t.col` or `"_t".col`) is
122
+ rejected rather than silently misplaced. A column whose name merely contains
123
+ those letters — `id_tuition`, `start_time`, `is_test` — is not a qualification.
109
124
 
110
125
  `order` defaults to `key`, which makes the work deterministic and the chunks
111
126
  disjoint. Ordering by something non-unique would let two concurrent callers pick
112
127
  overlapping sets; the row-level guard still keeps them correct, but they would
113
128
  waste each other's work.
114
129
  """
115
- if self.ALIAS in pending:
130
+ if _alias_as_qualifier(self.ALIAS, pending):
116
131
  raise ValueError(
117
132
  "`pending` must be unqualified: it is placed both in the outer WHERE and "
118
133
  "inside the subquery, and %r resolves in only one of them." % self.ALIAS)
@@ -66,13 +66,31 @@ class TestThePredicateIsPlacedTwice:
66
66
  where = sql.split("SET")[1].split("AND")[0]
67
67
  assert PENDING in where, "the first predicate must sit in the UPDATE's own WHERE"
68
68
 
69
- def test_a_qualified_predicate_is_refused_rather_than_misplaced(self, make_plpy):
69
+ @pytest.mark.parametrize("pending", [
70
+ "_t.id_status = $1",
71
+ '"_t".estado = \'pending\'',
72
+ '"_t" . estado = \'pending\'',
73
+ "_t . id = 1",
74
+ ])
75
+ def test_a_qualified_predicate_is_refused_rather_than_misplaced(self, make_plpy, pending):
70
76
  # It would resolve in the outer WHERE and break inside the subquery, so the
71
77
  # helper must not accept it and quietly produce a statement that fails at runtime.
78
+ # The quoted form is what the PRE acceptance probe sends.
72
79
  with pytest.raises(ValueError) as exc:
73
- Batch(make_plpy()).run(TABLE, SET, "_t.id_status = $1")
80
+ Batch(make_plpy()).run(TABLE, SET, pending)
74
81
  assert "unqualified" in str(exc.value)
75
82
 
83
+ @pytest.mark.parametrize("pending", [
84
+ "id_tuition IS NOT NULL", "start_time < now()", "is_test = false",
85
+ ])
86
+ def test_a_column_containing_the_alias_text_is_not_a_qualification(self, make_plpy, pending):
87
+ # Failing on 0.0.7: the guard looks for the substring "_t" anywhere, so every
88
+ # column containing it is refused. Observed on PRE 2026-10-02, probe
89
+ # aflib-007-accept-r1, case `batch`: `id_tuition IS NOT NULL` raised ValueError.
90
+ fake = _plpy(make_plpy, 0, 0)
91
+ Batch(fake).run(TABLE, SET, pending)
92
+ assert _sql(fake.executed[0]).count(pending) == 2
93
+
76
94
 
77
95
  class TestChunkBoundaries:
78
96
  def test_limit_bounds_the_subquery_not_the_update(self, make_plpy):
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