algovoi-keystone-httpx 0.1.1__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.
- algovoi_keystone_httpx-0.1.1/NOTICE +8 -0
- algovoi_keystone_httpx-0.1.1/PKG-INFO +47 -0
- algovoi_keystone_httpx-0.1.1/README.md +27 -0
- algovoi_keystone_httpx-0.1.1/algovoi_keystone_httpx/__init__.py +23 -0
- algovoi_keystone_httpx-0.1.1/algovoi_keystone_httpx.egg-info/PKG-INFO +47 -0
- algovoi_keystone_httpx-0.1.1/algovoi_keystone_httpx.egg-info/SOURCES.txt +11 -0
- algovoi_keystone_httpx-0.1.1/algovoi_keystone_httpx.egg-info/dependency_links.txt +1 -0
- algovoi_keystone_httpx-0.1.1/algovoi_keystone_httpx.egg-info/requires.txt +1 -0
- algovoi_keystone_httpx-0.1.1/algovoi_keystone_httpx.egg-info/top_level.txt +1 -0
- algovoi_keystone_httpx-0.1.1/pyproject.toml +35 -0
- algovoi_keystone_httpx-0.1.1/setup.cfg +4 -0
- algovoi_keystone_httpx-0.1.1/setup.py +3 -0
- algovoi_keystone_httpx-0.1.1/tests/test_keystone_httpx.py +135 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
algovoi-keystone-httpx
|
|
2
|
+
Copyright 2026 AlgoVoi
|
|
3
|
+
|
|
4
|
+
This product includes software developed by AlgoVoi.
|
|
5
|
+
Licensed under the Apache License, Version 2.0.
|
|
6
|
+
|
|
7
|
+
Built on the AlgoVoi substrate: RFC 8785 (JCS) canonicalisation + SHA-256,
|
|
8
|
+
producing content-addressed execution_ref evidence recomputable offline.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: algovoi-keystone-httpx
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Bind outbound HTTP calls to the AlgoVoi keystone -- wrap any httpx.Client or requests.Session and every POST / PUT / PATCH / DELETE gets a content-addressed execution_ref (keystone-only edition, Apache-2.0)
|
|
5
|
+
Author-email: AlgoVoi <chopmob@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://docs.algovoi.co.uk/keystone-connectors
|
|
8
|
+
Project-URL: Documentation, https://docs.algovoi.co.uk/keystone-connectors
|
|
9
|
+
Project-URL: Repository, https://github.com/chopmob-cloud/AlgoVoi-Keystone-Connectors
|
|
10
|
+
Project-URL: Issues, https://github.com/chopmob-cloud/AlgoVoi-Keystone-Connectors/issues
|
|
11
|
+
Keywords: http,httpx,requests,outbound,webhook,client,keystone,execution_ref,jcs,rfc8785,algovoi
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: NOTICE
|
|
18
|
+
Requires-Dist: algovoi-execution-ref>=0.1.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# algovoi-keystone-httpx
|
|
22
|
+
|
|
23
|
+
Bind **outbound** HTTP calls to the AlgoVoi keystone. The `asgi` connector binds the requests a
|
|
24
|
+
service *receives*; this one binds the calls a service *makes*. Wrap any sync `httpx.Client` or
|
|
25
|
+
`requests.Session` and every state-changing call (POST / PUT / PATCH / DELETE) is bound to the
|
|
26
|
+
`decision_ref` that authorised it, emitting a content-addressed `execution_ref` any party can
|
|
27
|
+
recompute offline:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
execution_ref = "sha256:" + SHA-256(JCS({ decision_ref, action_type, scope, outcome, executed_at_ms }))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`action_type` is `http:<method>`; `scope` is the destination `host/path` (query string dropped for
|
|
34
|
+
stability). Outcome comes from the response status: 2xx/3xx COMMITTED, 4xx SKIPPED, 5xx/exception
|
|
35
|
+
FAILED. Reads (GET / HEAD / OPTIONS) pass through unbound. No httpx or requests dependency.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from algovoi_keystone_httpx import keystone_http_client
|
|
39
|
+
|
|
40
|
+
client = keystone_http_client(httpx.Client(), decision_ref=decision_ref)
|
|
41
|
+
client.post("https://api.partner.com/v1/pay", json=payload) # bound
|
|
42
|
+
ref = client.execution_ref
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Records emitted on `client.executions` drop straight into `algovoi-keystone-validate`.
|
|
46
|
+
|
|
47
|
+
Keystone-only edition, Apache-2.0. Built on the AlgoVoi substrate (RFC 8785 JCS + SHA-256).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# algovoi-keystone-httpx
|
|
2
|
+
|
|
3
|
+
Bind **outbound** HTTP calls to the AlgoVoi keystone. The `asgi` connector binds the requests a
|
|
4
|
+
service *receives*; this one binds the calls a service *makes*. Wrap any sync `httpx.Client` or
|
|
5
|
+
`requests.Session` and every state-changing call (POST / PUT / PATCH / DELETE) is bound to the
|
|
6
|
+
`decision_ref` that authorised it, emitting a content-addressed `execution_ref` any party can
|
|
7
|
+
recompute offline:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
execution_ref = "sha256:" + SHA-256(JCS({ decision_ref, action_type, scope, outcome, executed_at_ms }))
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`action_type` is `http:<method>`; `scope` is the destination `host/path` (query string dropped for
|
|
14
|
+
stability). Outcome comes from the response status: 2xx/3xx COMMITTED, 4xx SKIPPED, 5xx/exception
|
|
15
|
+
FAILED. Reads (GET / HEAD / OPTIONS) pass through unbound. No httpx or requests dependency.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from algovoi_keystone_httpx import keystone_http_client
|
|
19
|
+
|
|
20
|
+
client = keystone_http_client(httpx.Client(), decision_ref=decision_ref)
|
|
21
|
+
client.post("https://api.partner.com/v1/pay", json=payload) # bound
|
|
22
|
+
ref = client.execution_ref
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Records emitted on `client.executions` drop straight into `algovoi-keystone-validate`.
|
|
26
|
+
|
|
27
|
+
Keystone-only edition, Apache-2.0. Built on the AlgoVoi substrate (RFC 8785 JCS + SHA-256).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""algovoi-keystone-httpx -- bind outbound HTTP calls to the AlgoVoi keystone (httpx / requests)."""
|
|
2
|
+
from .adapter import (
|
|
3
|
+
EXECUTION_OUTCOMES,
|
|
4
|
+
KeystoneHTTPClient,
|
|
5
|
+
KeystoneHTTPError,
|
|
6
|
+
WRITE_METHODS,
|
|
7
|
+
keystone_http_client,
|
|
8
|
+
outcome_for_status,
|
|
9
|
+
request_execution_ref,
|
|
10
|
+
tamper_detected,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"EXECUTION_OUTCOMES",
|
|
15
|
+
"KeystoneHTTPClient",
|
|
16
|
+
"KeystoneHTTPError",
|
|
17
|
+
"WRITE_METHODS",
|
|
18
|
+
"keystone_http_client",
|
|
19
|
+
"outcome_for_status",
|
|
20
|
+
"request_execution_ref",
|
|
21
|
+
"tamper_detected",
|
|
22
|
+
]
|
|
23
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: algovoi-keystone-httpx
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Bind outbound HTTP calls to the AlgoVoi keystone -- wrap any httpx.Client or requests.Session and every POST / PUT / PATCH / DELETE gets a content-addressed execution_ref (keystone-only edition, Apache-2.0)
|
|
5
|
+
Author-email: AlgoVoi <chopmob@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://docs.algovoi.co.uk/keystone-connectors
|
|
8
|
+
Project-URL: Documentation, https://docs.algovoi.co.uk/keystone-connectors
|
|
9
|
+
Project-URL: Repository, https://github.com/chopmob-cloud/AlgoVoi-Keystone-Connectors
|
|
10
|
+
Project-URL: Issues, https://github.com/chopmob-cloud/AlgoVoi-Keystone-Connectors/issues
|
|
11
|
+
Keywords: http,httpx,requests,outbound,webhook,client,keystone,execution_ref,jcs,rfc8785,algovoi
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: NOTICE
|
|
18
|
+
Requires-Dist: algovoi-execution-ref>=0.1.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# algovoi-keystone-httpx
|
|
22
|
+
|
|
23
|
+
Bind **outbound** HTTP calls to the AlgoVoi keystone. The `asgi` connector binds the requests a
|
|
24
|
+
service *receives*; this one binds the calls a service *makes*. Wrap any sync `httpx.Client` or
|
|
25
|
+
`requests.Session` and every state-changing call (POST / PUT / PATCH / DELETE) is bound to the
|
|
26
|
+
`decision_ref` that authorised it, emitting a content-addressed `execution_ref` any party can
|
|
27
|
+
recompute offline:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
execution_ref = "sha256:" + SHA-256(JCS({ decision_ref, action_type, scope, outcome, executed_at_ms }))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`action_type` is `http:<method>`; `scope` is the destination `host/path` (query string dropped for
|
|
34
|
+
stability). Outcome comes from the response status: 2xx/3xx COMMITTED, 4xx SKIPPED, 5xx/exception
|
|
35
|
+
FAILED. Reads (GET / HEAD / OPTIONS) pass through unbound. No httpx or requests dependency.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from algovoi_keystone_httpx import keystone_http_client
|
|
39
|
+
|
|
40
|
+
client = keystone_http_client(httpx.Client(), decision_ref=decision_ref)
|
|
41
|
+
client.post("https://api.partner.com/v1/pay", json=payload) # bound
|
|
42
|
+
ref = client.execution_ref
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Records emitted on `client.executions` drop straight into `algovoi-keystone-validate`.
|
|
46
|
+
|
|
47
|
+
Keystone-only edition, Apache-2.0. Built on the AlgoVoi substrate (RFC 8785 JCS + SHA-256).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
NOTICE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
algovoi_keystone_httpx/__init__.py
|
|
6
|
+
algovoi_keystone_httpx.egg-info/PKG-INFO
|
|
7
|
+
algovoi_keystone_httpx.egg-info/SOURCES.txt
|
|
8
|
+
algovoi_keystone_httpx.egg-info/dependency_links.txt
|
|
9
|
+
algovoi_keystone_httpx.egg-info/requires.txt
|
|
10
|
+
algovoi_keystone_httpx.egg-info/top_level.txt
|
|
11
|
+
tests/test_keystone_httpx.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
algovoi-execution-ref>=0.1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
algovoi_keystone_httpx
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=70", "Cython>=3.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "algovoi-keystone-httpx"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Bind outbound HTTP calls to the AlgoVoi keystone -- wrap any httpx.Client or requests.Session and every POST / PUT / PATCH / DELETE gets a content-addressed execution_ref (keystone-only edition, Apache-2.0)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
authors = [{ name = "AlgoVoi", email = "chopmob@gmail.com" }]
|
|
12
|
+
keywords = ["http", "httpx", "requests", "outbound", "webhook", "client", "keystone", "execution_ref", "jcs", "rfc8785", "algovoi"]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = ["algovoi-execution-ref>=0.1.0"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://docs.algovoi.co.uk/keystone-connectors"
|
|
23
|
+
Documentation = "https://docs.algovoi.co.uk/keystone-connectors"
|
|
24
|
+
Repository = "https://github.com/chopmob-cloud/AlgoVoi-Keystone-Connectors"
|
|
25
|
+
Issues = "https://github.com/chopmob-cloud/AlgoVoi-Keystone-Connectors/issues"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["."]
|
|
29
|
+
include = ["algovoi_keystone_httpx*"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
"algovoi_keystone_httpx" = ["*.so", "*.pyd"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.exclude-package-data]
|
|
35
|
+
"algovoi_keystone_httpx" = ["*.c", "*.pyx", "*.pxd"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Tests for algovoi-keystone-httpx (keystone-only edition)."""
|
|
2
|
+
import pytest
|
|
3
|
+
from algovoi_execution_ref import execution_ref
|
|
4
|
+
from algovoi_keystone_httpx import (
|
|
5
|
+
EXECUTION_OUTCOMES, KeystoneHTTPError, keystone_http_client, outcome_for_status,
|
|
6
|
+
request_execution_ref, tamper_detected,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
DR = "sha256:" + "a" * 64
|
|
10
|
+
TS = 1716460800000
|
|
11
|
+
URL = "https://api.partner.com/v1/pay?token=secret"
|
|
12
|
+
SCOPE = "api.partner.com/v1/pay"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Resp:
|
|
16
|
+
def __init__(self, status_code):
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FakeClient:
|
|
21
|
+
"""httpx.Client / requests.Session double. Returns a status set per URL; raises on 'boom'."""
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.calls = []
|
|
24
|
+
|
|
25
|
+
def post(self, url, *a, **k):
|
|
26
|
+
self.calls.append(("post", url))
|
|
27
|
+
if url == "boom":
|
|
28
|
+
raise RuntimeError("connection reset")
|
|
29
|
+
return Resp(201)
|
|
30
|
+
|
|
31
|
+
def put(self, url, *a, **k):
|
|
32
|
+
self.calls.append(("put", url))
|
|
33
|
+
return Resp(200)
|
|
34
|
+
|
|
35
|
+
def delete(self, url, *a, **k):
|
|
36
|
+
self.calls.append(("delete", url))
|
|
37
|
+
return Resp(404)
|
|
38
|
+
|
|
39
|
+
def get(self, url, *a, **k):
|
|
40
|
+
self.calls.append(("get", url))
|
|
41
|
+
return Resp(200) # read path, must pass through unbound
|
|
42
|
+
|
|
43
|
+
def request(self, method, url, *a, **k):
|
|
44
|
+
self.calls.append(("request", method, url))
|
|
45
|
+
return Resp(200)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_request_execution_ref_matches_primitive():
|
|
49
|
+
ref = request_execution_ref(decision_ref=DR, method="POST", url=URL, outcome="COMMITTED", executed_at_ms=TS)
|
|
50
|
+
assert ref == execution_ref(DR, "http:post", SCOPE, "COMMITTED", TS)
|
|
51
|
+
assert ref.startswith("sha256:") and len(ref) == 71
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_scope_drops_query():
|
|
55
|
+
a = request_execution_ref(decision_ref=DR, method="POST", url="https://h/p?x=1", outcome="COMMITTED", executed_at_ms=TS)
|
|
56
|
+
b = request_execution_ref(decision_ref=DR, method="POST", url="https://h/p?x=2", outcome="COMMITTED", executed_at_ms=TS)
|
|
57
|
+
assert a == b # query string is not part of scope
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_outcome_for_status():
|
|
61
|
+
assert outcome_for_status(201) == "COMMITTED"
|
|
62
|
+
assert outcome_for_status(302) == "COMMITTED"
|
|
63
|
+
assert outcome_for_status(404) == "SKIPPED"
|
|
64
|
+
assert outcome_for_status(500) == "FAILED"
|
|
65
|
+
assert outcome_for_status(0) == "FAILED"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_rejects_bad():
|
|
69
|
+
with pytest.raises(KeystoneHTTPError):
|
|
70
|
+
request_execution_ref(decision_ref=DR, method="POST", url=URL, outcome="DONE", executed_at_ms=TS)
|
|
71
|
+
with pytest.raises(KeystoneHTTPError):
|
|
72
|
+
request_execution_ref(decision_ref=DR, method="", url=URL, outcome="COMMITTED", executed_at_ms=TS)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_post_binds_committed():
|
|
76
|
+
c = keystone_http_client(FakeClient(), decision_ref=DR, clock_ms=lambda: TS)
|
|
77
|
+
c.post(URL, json={})
|
|
78
|
+
assert c.execution_ref == execution_ref(DR, "http:post", SCOPE, "COMMITTED", TS)
|
|
79
|
+
assert c.executions[0]["outcome"] == "COMMITTED"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_delete_4xx_is_skipped():
|
|
83
|
+
c = keystone_http_client(FakeClient(), decision_ref=DR, clock_ms=lambda: TS)
|
|
84
|
+
c.delete("https://api.partner.com/v1/pay")
|
|
85
|
+
assert c.execution_ref == execution_ref(DR, "http:delete", SCOPE, "SKIPPED", TS)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_request_write_binds():
|
|
89
|
+
c = keystone_http_client(FakeClient(), decision_ref=DR, clock_ms=lambda: TS)
|
|
90
|
+
c.request("PUT", "https://api.partner.com/v1/pay")
|
|
91
|
+
assert c.execution_ref == execution_ref(DR, "http:put", SCOPE, "COMMITTED", TS)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_request_read_passthrough_unbound():
|
|
95
|
+
c = keystone_http_client(FakeClient(), decision_ref=DR, clock_ms=lambda: TS)
|
|
96
|
+
r = c.request("GET", "https://api.partner.com/v1/pay")
|
|
97
|
+
assert r.status_code == 200
|
|
98
|
+
assert c.executions == [] # GET via request() is not bound
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_get_passthrough_unbound():
|
|
102
|
+
c = keystone_http_client(FakeClient(), decision_ref=DR, clock_ms=lambda: TS)
|
|
103
|
+
c.get("https://api.partner.com/v1/pay")
|
|
104
|
+
assert c.executions == [] # reads are not bound
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_failed_post_records_failed_and_reraises():
|
|
108
|
+
c = keystone_http_client(FakeClient(), decision_ref=DR, clock_ms=lambda: TS)
|
|
109
|
+
with pytest.raises(RuntimeError):
|
|
110
|
+
c.post("boom")
|
|
111
|
+
assert c.executions[-1]["outcome"] == "FAILED"
|
|
112
|
+
assert c.execution_ref == execution_ref(DR, "http:post", "boom", "FAILED", TS)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_rejects_bad_decision():
|
|
116
|
+
with pytest.raises(KeystoneHTTPError):
|
|
117
|
+
keystone_http_client(FakeClient(), decision_ref="")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_on_execution_callback():
|
|
121
|
+
seen = []
|
|
122
|
+
c = keystone_http_client(FakeClient(), decision_ref=DR, clock_ms=lambda: TS, on_execution=seen.append)
|
|
123
|
+
c.post(URL)
|
|
124
|
+
assert seen[0]["execution_ref"] == c.execution_ref
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_tamper_detected():
|
|
128
|
+
ref = request_execution_ref(decision_ref=DR, method="POST", url=URL, outcome="COMMITTED", executed_at_ms=TS)
|
|
129
|
+
assert not tamper_detected(ref, decision_ref=DR, method="POST", url=URL, outcome="COMMITTED", executed_at_ms=TS)
|
|
130
|
+
assert tamper_detected(ref, decision_ref=DR, method="POST", url="https://api.partner.com/v1/refund", outcome="COMMITTED", executed_at_ms=TS)
|
|
131
|
+
assert tamper_detected("not-a-ref", decision_ref=DR, method="POST", url=URL, outcome="COMMITTED", executed_at_ms=TS)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_outcomes_constant():
|
|
135
|
+
assert EXECUTION_OUTCOMES == ("COMMITTED", "SKIPPED", "FAILED", "REVERSED")
|