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.
- aflib-0.0.7/README.md → aflib-0.0.8/PKG-INFO +22 -5
- aflib-0.0.7/PKG-INFO → aflib-0.0.8/README.md +13 -14
- {aflib-0.0.7 → aflib-0.0.8}/pyproject.toml +1 -1
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/__init__.py +1 -1
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/batch.py +17 -2
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_batch.py +20 -2
- {aflib-0.0.7 → aflib-0.0.8}/.gitignore +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/LICENSE +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/authz.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/codec.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/db.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/errors.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/events.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/functions.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/identity.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/logging.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/params.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/result.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/transaction.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/trigger.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/src/aflib/workflow.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/conftest.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_authz.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_codec.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_db.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_errors.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_events.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_functions.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_identity.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_logging.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_params.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_public_api.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_result.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_transaction.py +0 -0
- {aflib-0.0.7 → aflib-0.0.8}/tests/test_trigger.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
86
|
-
`aflib==0.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
95
|
-
`aflib==0.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|