ng-postcode 0.1.0__tar.gz → 0.2.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.
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/PKG-INFO +1 -1
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/pyproject.toml +1 -1
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/src/ng_postcode/api.py +63 -13
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/tests/test_api.py +21 -5
- ng_postcode-0.2.1/tests/test_live_responses.py +116 -0
- ng_postcode-0.2.1/tests/test_tolerance.py +50 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/.gitignore +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/LICENSE +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/README.md +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/src/ng_postcode/__init__.py +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/src/ng_postcode/_postcode.py +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/src/ng_postcode/client.py +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/src/ng_postcode/py.typed +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/tests/test_client.py +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/tests/test_conformance.py +0 -0
- {ng_postcode-0.1.0 → ng_postcode-0.2.1}/tests/test_postcode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ng-postcode
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Parse, validate and format Nigeria's NIPOST digital postcode (NDAPS) offline, plus a client for the postcode.gov.ng API.
|
|
5
5
|
Project-URL: Repository, https://github.com/Adeniyikayodee/ng-postcode
|
|
6
6
|
Project-URL: Issues, https://github.com/Adeniyikayodee/ng-postcode/issues
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ng-postcode"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Parse, validate and format Nigeria's NIPOST digital postcode (NDAPS) offline, plus a client for the postcode.gov.ng API."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -8,6 +8,7 @@ offline.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import math
|
|
11
12
|
from collections.abc import Callable, Mapping
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from typing import Any, Generic, TypeVar
|
|
@@ -74,12 +75,17 @@ class Lookup:
|
|
|
74
75
|
"""Level 4. Undocumented, so left as raw JSON."""
|
|
75
76
|
point_geometry: Any
|
|
76
77
|
"""Level 5. Undocumented, so left as raw JSON."""
|
|
78
|
+
status: str | None = None
|
|
79
|
+
"""`valid`, `not_found`, or `invalid` for a malformed code. Sent at every level."""
|
|
80
|
+
verified: bool | None = None
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
@dataclass(frozen=True, slots=True)
|
|
80
84
|
class Suggestion:
|
|
81
85
|
code: str
|
|
82
|
-
|
|
86
|
+
"""The value of the segment being completed, such as `A03`, not a full prefix."""
|
|
87
|
+
label: str | None
|
|
88
|
+
"""Documented by NIPOST but not sent by the live API as of October 2026."""
|
|
83
89
|
|
|
84
90
|
|
|
85
91
|
@dataclass(frozen=True, slots=True)
|
|
@@ -117,29 +123,54 @@ class Reverse:
|
|
|
117
123
|
"""Set when nothing is in range."""
|
|
118
124
|
radius_m: float | None
|
|
119
125
|
"""The radius the API actually applied."""
|
|
126
|
+
depth: str | None = None
|
|
127
|
+
"""How deep the match goes, such as `unit`."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, slots=True)
|
|
131
|
+
class NearbyUnit:
|
|
132
|
+
postcode: str
|
|
133
|
+
display: str
|
|
134
|
+
distance_m: float | None
|
|
120
135
|
|
|
121
136
|
|
|
122
137
|
def lookup(code: Postcode, level: int = 1) -> Request[Lookup]:
|
|
123
138
|
"""Resolve a postcode. Levels are cumulative from 1 (validity only) to 5, and
|
|
124
|
-
the API caps the answer at the level granted to the key.
|
|
139
|
+
the API caps the answer at the level granted to the key.
|
|
140
|
+
|
|
141
|
+
Raises `ValueError` for a level outside 1 to 5.
|
|
142
|
+
"""
|
|
143
|
+
if level not in range(1, 6):
|
|
144
|
+
raise ValueError(f"level must be 1 to 5, got {level!r}")
|
|
125
145
|
return Request("/v1/lookup", (("code", str(code)), ("level", str(level))), _lookup)
|
|
126
146
|
|
|
127
147
|
|
|
128
148
|
def autocomplete(partial: str) -> Request[Autocomplete]:
|
|
129
|
-
"""Suggest completions for a partial postcode such as `EK 01 A`.
|
|
149
|
+
"""Suggest completions for a partial postcode such as `EK 01 A`.
|
|
150
|
+
|
|
151
|
+
Raises `ValueError` for an empty `partial`: the live API never answers one.
|
|
152
|
+
"""
|
|
153
|
+
if not partial.strip():
|
|
154
|
+
raise ValueError("partial must not be empty")
|
|
130
155
|
return Request("/v1/search/autocomplete", (("q", partial),), _autocomplete)
|
|
131
156
|
|
|
132
157
|
|
|
133
158
|
def reverse(at: Coordinate, max_distance_m: float | None = None) -> Request[Reverse]:
|
|
134
159
|
"""Find the postcode of the nearest building, within 25 m unless `max_distance_m`
|
|
135
|
-
says otherwise. The API clamps it to 250 m.
|
|
160
|
+
says otherwise. The API clamps it to 250 m.
|
|
161
|
+
|
|
162
|
+
Raises `ValueError` for a coordinate or distance that is not a finite number.
|
|
163
|
+
"""
|
|
136
164
|
return Request("/v1/search/reverse", _around(at, "max_distance_m", max_distance_m), _reverse)
|
|
137
165
|
|
|
138
166
|
|
|
139
|
-
def nearby(at: Coordinate, radius_m: float | None = None) -> Request[
|
|
140
|
-
"""List buildings around a point, within 300 m unless `radius_m`
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
def nearby(at: Coordinate, radius_m: float | None = None) -> Request[tuple[NearbyUnit, ...]]:
|
|
168
|
+
"""List buildings around a point, nearest first, within 300 m unless `radius_m`
|
|
169
|
+
says otherwise. Empty when nothing is in range.
|
|
170
|
+
|
|
171
|
+
Raises `ValueError` for a coordinate or radius that is not a finite number.
|
|
172
|
+
"""
|
|
173
|
+
return Request("/v1/search/nearby", _around(at, "radius", radius_m), _nearby)
|
|
143
174
|
|
|
144
175
|
|
|
145
176
|
def decode(request: Request[T], status: int, body: str) -> T | ApiError:
|
|
@@ -150,10 +181,14 @@ def decode(request: Request[T], status: int, body: str) -> T | ApiError:
|
|
|
150
181
|
return _malformed(status, f"not JSON: {error}")
|
|
151
182
|
if not isinstance(envelope, dict):
|
|
152
183
|
return _malformed(status, "expected a JSON object")
|
|
153
|
-
failure =
|
|
154
|
-
if failure
|
|
184
|
+
failure = envelope.get("error")
|
|
185
|
+
if isinstance(failure, dict):
|
|
155
186
|
code = _text(failure, "code") or "unknown_error"
|
|
156
187
|
return ApiError(status, code, _text(failure, "message") or "")
|
|
188
|
+
if isinstance(failure, str):
|
|
189
|
+
return ApiError(status, "unknown_error", failure)
|
|
190
|
+
if not 200 <= status < 300:
|
|
191
|
+
return _malformed(status, "an error status without an error")
|
|
157
192
|
data = request.read(envelope.get("data"))
|
|
158
193
|
return data if data is not None else _malformed(status, "unexpected data")
|
|
159
194
|
|
|
@@ -165,6 +200,8 @@ def _around(at: Coordinate, key: str, metres: float | None) -> Params:
|
|
|
165
200
|
|
|
166
201
|
def _number_text(value: float) -> str:
|
|
167
202
|
number = float(value)
|
|
203
|
+
if not math.isfinite(number):
|
|
204
|
+
raise ValueError(f"expected a finite number, got {value!r}")
|
|
168
205
|
return str(int(number)) if number.is_integer() else repr(number)
|
|
169
206
|
|
|
170
207
|
|
|
@@ -191,8 +228,18 @@ def _float(value: Any) -> float | None:
|
|
|
191
228
|
return float(value) if is_number else None
|
|
192
229
|
|
|
193
230
|
|
|
194
|
-
def
|
|
195
|
-
|
|
231
|
+
def _nearby(data: Any) -> tuple[NearbyUnit, ...] | None:
|
|
232
|
+
if not isinstance(data, list):
|
|
233
|
+
return None
|
|
234
|
+
return tuple(
|
|
235
|
+
NearbyUnit(
|
|
236
|
+
postcode=_text(item, "postcode") or "",
|
|
237
|
+
display=_text(item, "display") or "",
|
|
238
|
+
distance_m=_number(item, "distance_m"),
|
|
239
|
+
)
|
|
240
|
+
for item in data
|
|
241
|
+
if isinstance(item, dict)
|
|
242
|
+
)
|
|
196
243
|
|
|
197
244
|
|
|
198
245
|
def _lookup(data: Any) -> Lookup | None:
|
|
@@ -215,6 +262,8 @@ def _lookup(data: Any) -> Lookup | None:
|
|
|
215
262
|
building_use_status=_text(data, "building_use_status"),
|
|
216
263
|
other_building_info=data.get("other_building_info"),
|
|
217
264
|
point_geometry=data.get("point_geometry"),
|
|
265
|
+
status=_text(data, "status"),
|
|
266
|
+
verified=data["verified"] if isinstance(data.get("verified"), bool) else None,
|
|
218
267
|
)
|
|
219
268
|
|
|
220
269
|
|
|
@@ -223,7 +272,7 @@ def _autocomplete(data: Any) -> Autocomplete | None:
|
|
|
223
272
|
return None
|
|
224
273
|
items = data.get("suggestions")
|
|
225
274
|
suggestions = tuple(
|
|
226
|
-
Suggestion(code=_text(item, "code") or "", label=_text(item, "label")
|
|
275
|
+
Suggestion(code=_text(item, "code") or "", label=_text(item, "label"))
|
|
227
276
|
for item in (items if isinstance(items, list) else [])
|
|
228
277
|
if isinstance(item, dict)
|
|
229
278
|
)
|
|
@@ -255,6 +304,7 @@ def _reverse(data: Any) -> Reverse | None:
|
|
|
255
304
|
state=_text(data, "state"),
|
|
256
305
|
message=_text(data, "message"),
|
|
257
306
|
radius_m=_number(data, "radius_m"),
|
|
307
|
+
depth=_text(data, "depth"),
|
|
258
308
|
)
|
|
259
309
|
|
|
260
310
|
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
5
7
|
from ng_postcode import Postcode, Segment, parse
|
|
6
8
|
from ng_postcode.api import (
|
|
7
9
|
AdministrativeAddress,
|
|
@@ -72,20 +74,22 @@ def test_fields_above_the_granted_level_are_none() -> None:
|
|
|
72
74
|
assert (found.administrative_address, found.recent_house_address) == (None, None)
|
|
73
75
|
|
|
74
76
|
|
|
75
|
-
def
|
|
76
|
-
data = {"segment": "lga", "suggestions": [{"code": "
|
|
77
|
+
def test_decodes_documented_labels_and_echoed_coordinates() -> None:
|
|
78
|
+
data = {"segment": "lga", "suggestions": [{"code": "01", "label": "ADO EKITI"}]}
|
|
77
79
|
found = decode(autocomplete("EK"), 200, body(data))
|
|
78
80
|
assert not isinstance(found, ApiError)
|
|
79
81
|
assert found.segment is Segment.LGA
|
|
80
|
-
assert found.suggestions[0].code == "
|
|
82
|
+
assert (found.suggestions[0].code, found.suggestions[0].label) == ("01", "ADO EKITI")
|
|
81
83
|
|
|
82
84
|
miss = {"found": False, "coordinate": [5.22, 7.62], "message": "none", "radius_m": 25}
|
|
83
85
|
result = decode(reverse(HERE), 200, body(miss))
|
|
84
86
|
assert not isinstance(result, ApiError)
|
|
85
|
-
assert (result.found, result.unit, result.radius_m) == (False, None, 25.0)
|
|
86
87
|
assert result.coordinate == HERE
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
|
|
90
|
+
def test_an_empty_autocomplete_is_refused_before_it_hangs_the_api() -> None:
|
|
91
|
+
with pytest.raises(ValueError, match="must not be empty"):
|
|
92
|
+
autocomplete(" ")
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
def test_error_envelopes_and_bad_bodies_become_values() -> None:
|
|
@@ -97,3 +101,15 @@ def test_error_envelopes_and_bad_bodies_become_values() -> None:
|
|
|
97
101
|
error = decode(request, status, text)
|
|
98
102
|
assert isinstance(error, ApiError)
|
|
99
103
|
assert (error.status, error.code) == (status, "malformed_response")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_rejects_requests_the_api_cannot_answer() -> None:
|
|
107
|
+
code = Postcode("EK01A03FK01")
|
|
108
|
+
for level in (0, 6):
|
|
109
|
+
with pytest.raises(ValueError, match="level must be 1 to 5"):
|
|
110
|
+
lookup(code, level)
|
|
111
|
+
for bad in (float("nan"), float("inf")):
|
|
112
|
+
with pytest.raises(ValueError, match="finite"):
|
|
113
|
+
reverse(Coordinate(lat=bad, lng=5.2))
|
|
114
|
+
with pytest.raises(ValueError, match="finite"):
|
|
115
|
+
nearby(Coordinate(lat=7.6, lng=5.2), bad)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Decodes the response bodies captured from the live API in `spec/responses.json`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from ng_postcode import Postcode, Segment
|
|
12
|
+
from ng_postcode.api import (
|
|
13
|
+
ApiError,
|
|
14
|
+
Coordinate,
|
|
15
|
+
NearbyUnit,
|
|
16
|
+
Request,
|
|
17
|
+
autocomplete,
|
|
18
|
+
decode,
|
|
19
|
+
lookup,
|
|
20
|
+
nearby,
|
|
21
|
+
reverse,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
RESPONSES = Path(__file__).resolve().parents[2] / "spec" / "responses.json"
|
|
27
|
+
LIVE: dict[str, Any] = (
|
|
28
|
+
json.loads(RESPONSES.read_text(encoding="utf-8")) if RESPONSES.exists() else {}
|
|
29
|
+
)
|
|
30
|
+
CODE = Postcode("FC03B06AG12")
|
|
31
|
+
HERE = Coordinate(lat=7.6211, lng=5.2214)
|
|
32
|
+
|
|
33
|
+
pytestmark = pytest.mark.skipif(
|
|
34
|
+
not LIVE, reason="spec/responses.json lives in the repository, not the sdist"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def live(request: Request[T], name: str) -> T | ApiError:
|
|
39
|
+
captured = LIVE[name]
|
|
40
|
+
return decode(request, captured["status"], json.dumps(captured["body"]))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_lookup_statuses() -> None:
|
|
44
|
+
valid = live(lookup(CODE), "lookup_valid")
|
|
45
|
+
assert not isinstance(valid, ApiError)
|
|
46
|
+
assert (valid.valid, valid.status, valid.verified) == (True, "valid", False)
|
|
47
|
+
assert valid.administrative_address is None
|
|
48
|
+
|
|
49
|
+
missing = live(lookup(CODE), "lookup_not_found")
|
|
50
|
+
assert not isinstance(missing, ApiError)
|
|
51
|
+
assert (missing.valid, missing.status) == (False, "not_found")
|
|
52
|
+
|
|
53
|
+
malformed = live(lookup(CODE), "lookup_invalid")
|
|
54
|
+
assert not isinstance(malformed, ApiError)
|
|
55
|
+
assert (malformed.valid, malformed.status) == (False, "invalid")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_autocomplete_sends_segment_values_without_labels() -> None:
|
|
59
|
+
states = live(autocomplete("E"), "autocomplete_state")
|
|
60
|
+
assert not isinstance(states, ApiError)
|
|
61
|
+
assert states.segment is Segment.STATE
|
|
62
|
+
assert [(s.code, s.label) for s in states.suggestions] == [
|
|
63
|
+
("EB", None),
|
|
64
|
+
("ED", None),
|
|
65
|
+
("EK", None),
|
|
66
|
+
("EN", None),
|
|
67
|
+
]
|
|
68
|
+
units = live(autocomplete("EK 01 A29 KR 3"), "autocomplete_unit_empty")
|
|
69
|
+
assert not isinstance(units, ApiError)
|
|
70
|
+
assert (units.segment, units.suggestions) == (Segment.UNIT, ())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_reverse() -> None:
|
|
74
|
+
found = live(reverse(HERE), "reverse_found")
|
|
75
|
+
assert not isinstance(found, ApiError)
|
|
76
|
+
assert found.unit is not None
|
|
77
|
+
assert (found.unit.postcode, found.unit.distance_m, found.unit.confidence) == (
|
|
78
|
+
"EK-01-A29-KR-36",
|
|
79
|
+
15.7,
|
|
80
|
+
"high",
|
|
81
|
+
)
|
|
82
|
+
assert (found.area, found.district, found.state, found.depth) == (
|
|
83
|
+
"EK-01-A29-KR",
|
|
84
|
+
"EK-01-A29",
|
|
85
|
+
"EK",
|
|
86
|
+
"unit",
|
|
87
|
+
)
|
|
88
|
+
assert found.coordinate == HERE
|
|
89
|
+
assert (found.unit.address, found.radius_m) == (None, 25.0)
|
|
90
|
+
|
|
91
|
+
nothing = live(reverse(HERE), "reverse_not_found")
|
|
92
|
+
assert not isinstance(nothing, ApiError)
|
|
93
|
+
assert (nothing.found, nothing.unit) == (False, None)
|
|
94
|
+
assert nothing.message == "no postcode within range of this location"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_nearby_is_a_list_nearest_first() -> None:
|
|
98
|
+
units = live(nearby(HERE), "nearby_found")
|
|
99
|
+
assert not isinstance(units, ApiError)
|
|
100
|
+
assert units[0] == NearbyUnit("EK-01-A29-KR-36", "EK 01 A29 KR 36", 15.7)
|
|
101
|
+
assert [u.distance_m for u in units] == [15.7, 18.3, 31.0]
|
|
102
|
+
assert live(nearby(HERE), "nearby_empty") == ()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.parametrize(
|
|
106
|
+
("name", "code"),
|
|
107
|
+
[
|
|
108
|
+
("lookup_level_not_granted", "level_not_granted"),
|
|
109
|
+
("reverse_bad_request", "bad_request"),
|
|
110
|
+
("invalid_api_key", "invalid_api_key"),
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
def test_errors(name: str, code: str) -> None:
|
|
114
|
+
error = live(lookup(CODE), name)
|
|
115
|
+
assert isinstance(error, ApiError)
|
|
116
|
+
assert (error.status, error.code) == (LIVE[name]["status"], code)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Runs the shared cases in `spec/tolerance.json`: bodies the live API may one day send."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from ng_postcode import Postcode
|
|
12
|
+
from ng_postcode.api import (
|
|
13
|
+
ApiError,
|
|
14
|
+
Coordinate,
|
|
15
|
+
Request,
|
|
16
|
+
autocomplete,
|
|
17
|
+
decode,
|
|
18
|
+
lookup,
|
|
19
|
+
nearby,
|
|
20
|
+
reverse,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
SPEC = Path(__file__).resolve().parents[2] / "spec" / "tolerance.json"
|
|
24
|
+
CASES: list[dict[str, Any]] = (
|
|
25
|
+
json.loads(SPEC.read_text(encoding="utf-8"))["cases"] if SPEC.exists() else []
|
|
26
|
+
)
|
|
27
|
+
HERE = Coordinate(lat=7.6211, lng=5.2214)
|
|
28
|
+
REQUESTS: dict[str, Request[Any]] = {
|
|
29
|
+
"lookup": lookup(Postcode("FC03B06AG12"), 1),
|
|
30
|
+
"autocomplete": autocomplete("E"),
|
|
31
|
+
"reverse": reverse(HERE),
|
|
32
|
+
"nearby": nearby(HERE),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pytestmark = pytest.mark.skipif(
|
|
36
|
+
not CASES, reason="spec/tolerance.json lives in the repository, not the sdist"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("case", CASES, ids=[case["name"] for case in CASES])
|
|
41
|
+
def test_reaches_the_shared_outcome(case: dict[str, Any]) -> None:
|
|
42
|
+
body = case["text"] if "text" in case else json.dumps(case["body"])
|
|
43
|
+
decoded = decode(REQUESTS[case["request"]], case["status"], body)
|
|
44
|
+
if not isinstance(decoded, ApiError):
|
|
45
|
+
found: tuple[str, str | None] = ("ok", None)
|
|
46
|
+
elif decoded.code == "malformed_response":
|
|
47
|
+
found = ("malformed", None)
|
|
48
|
+
else:
|
|
49
|
+
found = ("rejected", decoded.code)
|
|
50
|
+
assert found == (case["outcome"], case.get("code"))
|
|
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
|