velocity-python 0.4.0__py3-none-any.whl → 0.4.1__py3-none-any.whl

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.
velocity/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.4.0"
1
+ __version__ = version = "0.4.1"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -288,8 +288,15 @@ def act_relations(
288
288
  if kind == "hold":
289
289
  hold = tx.table(engine.HOLD).find({"sys_id": ident})
290
290
  if hold is not None and hold["docket_id"] == docket["sys_id"]:
291
+ # the relation only; the engine decides the hold's own
292
+ # release_permission with the actor's bound authz
291
293
  if engine.hold_owner_of(
292
- actor, route, dict(hold.to_dict()), docket.get("entity")
294
+ actor,
295
+ route,
296
+ dict(hold.to_dict()),
297
+ docket.get("entity"),
298
+ docket=docket,
299
+ check_permission=False,
293
300
  ):
294
301
  out.add(HOLD_OWNER)
295
302
  return frozenset(out)
@@ -891,10 +891,15 @@ def may_release(actor: Actor, route: Route, hold, docket, effect) -> bool:
891
891
  return False
892
892
  if actor.kind != "person" or not in_scope(actor, docket.get("entity")):
893
893
  return False
894
+ held = hold.get("release_permission")
895
+ if held and not actor.may(HOLD_RELEASE, held, docket):
896
+ return False # the hold's own permission binds every way out
894
897
  permission = effect.get("release_permission")
895
898
  if permission:
896
899
  return actor.may(HOLD_RELEASE, permission, docket)
897
- return hold_owner_of(actor, route, hold, docket.get("entity"))
900
+ return hold_owner_of(
901
+ actor, route, hold, docket.get("entity"), docket=docket, direct=False
902
+ )
898
903
 
899
904
 
900
905
  def _release_effects(item: Pass):
@@ -1659,16 +1664,43 @@ def problem_owner_of(actor: Actor, route: Route, position) -> bool:
1659
1664
  )
1660
1665
 
1661
1666
 
1662
- def hold_owner_of(actor: Actor, route: Route, hold, entity) -> bool:
1667
+ RELEASE_ANY, RELEASE_VIA_EFFECT = "any", "effect"
1668
+
1669
+
1670
+ def hold_owner_of(
1671
+ actor: Actor,
1672
+ route: Route,
1673
+ hold,
1674
+ entity,
1675
+ *,
1676
+ docket=None,
1677
+ direct=True,
1678
+ check_permission=True,
1679
+ ) -> bool:
1663
1680
  """Who may release a manual hold on a docket of ``entity``: its owner
1664
- role, its placer, a route owner — inside their entities."""
1681
+ role, its placer, a route owner, an authorized override — inside their
1682
+ entities, and subject to the hold's own release controls:
1683
+
1684
+ * ``release_via="effect"``: never released directly (the
1685
+ ``hold:<id>:release`` action, ``direct=True``), only by a pass effect;
1686
+ * ``release_permission``: whoever releases it must also be allowed that
1687
+ permission through authz (:data:`HOLD_RELEASE`, with the docket's
1688
+ entity scope) — a route owner or an override included. Needs
1689
+ ``docket``; ``check_permission=False`` is only for computing an authz
1690
+ relation, the engine then decides with the actor's own ``allows``."""
1665
1691
  if actor.kind != "person" or str(hold.get("release_rule") or "") not in (
1666
1692
  "",
1667
1693
  "manual",
1668
1694
  ):
1669
1695
  return False
1696
+ if direct and (hold.get("release_via") or RELEASE_ANY) == RELEASE_VIA_EFFECT:
1697
+ return False
1670
1698
  if not in_scope(actor, entity):
1671
1699
  return False
1700
+ permission = hold.get("release_permission")
1701
+ if permission and check_permission:
1702
+ if docket is None or not actor.may(HOLD_RELEASE, permission, docket):
1703
+ return False
1672
1704
  if actor.override:
1673
1705
  return True
1674
1706
  return (
@@ -1777,7 +1809,7 @@ def actions_for(tx, docket, route: Route, actor: Actor, *, positions=None, now=N
1777
1809
  }
1778
1810
  )
1779
1811
  for hold in holds_of(tx, docket["sys_id"]):
1780
- if hold_owner_of(actor, route, hold, docket.get("entity")):
1812
+ if hold_owner_of(actor, route, hold, docket.get("entity"), docket=docket):
1781
1813
  out.append(
1782
1814
  _action(
1783
1815
  f"hold:{hold['sys_id']}:release",
@@ -2217,11 +2249,28 @@ def place_hold(
2217
2249
  blocks=None,
2218
2250
  releases_when=None,
2219
2251
  label=None,
2252
+ release_permission=None,
2253
+ release_via=RELEASE_ANY,
2220
2254
  now=None,
2221
2255
  ) -> int:
2222
2256
  """Place a manual hold; returns its id. ``blocks``: the pass codes it
2223
- blocks (default every forward, branch and rejoin pass)."""
2257
+ blocks (default every forward, branch and rejoin pass).
2258
+
2259
+ Release controls: ``release_permission`` — whoever releases it (owner,
2260
+ route owner, override, or a pass effect) must be allowed that permission
2261
+ through authz (``workflow.hold.release``); ``release_via="effect"`` —
2262
+ only a pass's ``release_hold`` effect releases it, so its pass's guards
2263
+ always apply: the direct ``hold:<id>:release`` action is never offered
2264
+ and is refused if sent."""
2224
2265
  hooks.check_code(kind, "hold kind")
2266
+ if release_via not in (RELEASE_ANY, RELEASE_VIA_EFFECT):
2267
+ raise WorkflowError(
2268
+ f"release_via must be {RELEASE_ANY!r} or {RELEASE_VIA_EFFECT!r}"
2269
+ )
2270
+ if release_permission is not None and (
2271
+ not isinstance(release_permission, str) or not release_permission.strip()
2272
+ ):
2273
+ raise WorkflowError("release_permission must be a permission name")
2225
2274
  ctx = _Ctx(tx, now)
2226
2275
  docket = _docket(tx, docket_id, lock=True)
2227
2276
  row = tx.table(HOLD).new(
@@ -2236,6 +2285,8 @@ def place_hold(
2236
2285
  "release_rule": "manual",
2237
2286
  "releases_when": releases_when,
2238
2287
  "blocks": list(blocks) if blocks else None,
2288
+ "release_permission": release_permission,
2289
+ "release_via": release_via,
2239
2290
  }
2240
2291
  )
2241
2292
  _mark(ctx, docket, actor, "hold", outcome=kind, note=str(reason or kind))
@@ -317,6 +317,10 @@ SCHEMA = {
317
317
  "released_by": "TEXT",
318
318
  "released_at": TS,
319
319
  "blocks": "JSONB",
320
+ # release controls: a permission any release needs, and
321
+ # "effect" = only a pass effect may release it (no direct action)
322
+ "release_permission": "TEXT",
323
+ "release_via": "TEXT",
320
324
  "link_id": "BIGINT",
321
325
  "rule_pack": "TEXT",
322
326
  "rule_version": "INTEGER",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
- velocity/__init__.py,sha256=MxII3QHZeqUP7s3RLjEAxRW17ILOJ6d5n8RiVCGfUsw,362
1
+ velocity/__init__.py,sha256=cKKghorIQ6KmDenkl1684EwlR7g6o3H-aBxwT9VLk5o,362
2
2
  velocity/cache.py,sha256=pthvw7gDVEXRzkTcQtA5YVf4Q1IxAamWsba4UReT0RE,8933
3
3
  velocity/hooks.py,sha256=jY_qtAapNHvptmryFSD-Zc7RvLhV6e5nJEH3x2xTgCw,20575
4
4
  velocity/logging.py,sha256=MAsZap1cDSdOBNRc_9ezQxup5dHOS0ARxU9NI_-UK3E,4037
@@ -239,18 +239,18 @@ velocity/rules/storage.py,sha256=dCWhN6g5_kQCTWy66CgpyNmIwrTY1VsVsMPXKoaiO6U,130
239
239
  velocity/testing/__init__.py,sha256=WtYMDHgCLxOq6mlemcOYV5Y9HqF_2QQgSfuAjFRg69Q,731
240
240
  velocity/testing/fake_db.py,sha256=phgdulk5owf3noUDpEfI7yFsVcKxRGZ82f2BJPYnaiU,53226
241
241
  velocity/workflow/__init__.py,sha256=yVB00hLDhKmQnUGIwEHtwUcwk9MS79M3S0l5SGI9icc,3704
242
- velocity/workflow/access.py,sha256=GIc5I0McBGpf24cHKAkImKgmlSUGi4dd4a8WKXPemP8,12391
242
+ velocity/workflow/access.py,sha256=OPmcFTKvGP6mQnQTzPZjbPAi6TfrYUu1e4UkI8EqWy4,12668
243
243
  velocity/workflow/contract.py,sha256=PoWfQ8lseAZU0WLJiNOvgyJ2ZDDq7e6Ji2r497oap6w,38904
244
244
  velocity/workflow/definitions.py,sha256=2UXxd7dQLdBi8X5H0_W1Qo4cmXJVlDpzLhpBdsbbUqs,31979
245
- velocity/workflow/engine.py,sha256=wOxcceGYZuY3Kwi8hsoIzVhq-f93aQMwPv9WXjN0Rp8,121731
245
+ velocity/workflow/engine.py,sha256=XGnEfBIGKAWMgq5pJsbSoYy86vYj2yaiLqRZq7v-lhg,123922
246
246
  velocity/workflow/hooks.py,sha256=0lDMuAE-OwL1_M8wnylU46zBgOooJomVErPUaqdfR1c,14995
247
247
  velocity/workflow/notices.py,sha256=hTCVKyZn7ScJNPSgPYaa14QTRxKH5VHwAgNnV6xHW8E,13825
248
248
  velocity/workflow/outbox.py,sha256=R-DmdeVETYZc0u__OXxhKyeLgM1Cwq555-RqAFTW7h0,9412
249
249
  velocity/workflow/points.py,sha256=g_vQ3cdHdx_JloVCS5dVjo_jJoSolO92K5p3suDzxlE,10654
250
- velocity/workflow/schema.py,sha256=ZhL7MiKi0zMyH0Uc2qjCGnOS7JIqlFr8X3omWF67_Ko,20238
251
- velocity_python-0.4.0.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
252
- velocity_python-0.4.0.dist-info/METADATA,sha256=_u6t02AcR7Dx_mcAwcolHJgrJruVyzkCrt30IOUyE70,17820
253
- velocity_python-0.4.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
254
- velocity_python-0.4.0.dist-info/entry_points.txt,sha256=nXUKJHkOGQCSPoagdmMNd_qycrle1LVU-GhYknLGM1M,56
255
- velocity_python-0.4.0.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
256
- velocity_python-0.4.0.dist-info/RECORD,,
250
+ velocity/workflow/schema.py,sha256=AY1VkRFRuddL9egJHh8-cdrh8-tDsg2MLDs0ihEKskc,20461
251
+ velocity_python-0.4.1.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
252
+ velocity_python-0.4.1.dist-info/METADATA,sha256=1P5cdnKxBa7go3LyXCa-_ywsXyoTOz5W1woGwpeo5Og,17820
253
+ velocity_python-0.4.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
254
+ velocity_python-0.4.1.dist-info/entry_points.txt,sha256=nXUKJHkOGQCSPoagdmMNd_qycrle1LVU-GhYknLGM1M,56
255
+ velocity_python-0.4.1.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
256
+ velocity_python-0.4.1.dist-info/RECORD,,