nodii-address 0.1.0__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.
@@ -0,0 +1,51 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ coverage/
5
+ .DS_Store
6
+ *.log
7
+ .env
8
+ .env.local
9
+ .env.*.local
10
+ .turbo/
11
+ .bun/
12
+
13
+ # AI review loop artifacts (scripts/review/ai-review.ts)
14
+ .claude-pre-review.md
15
+ .claude-pre-review.json
16
+ .claude/pending-patches/
17
+
18
+ # Agent session state (not committed; per-session)
19
+ .claude/endpoints-snapshot.json
20
+ .agent-todo.md
21
+
22
+ # Python (uv)
23
+ .venv/
24
+ __pycache__/
25
+ *.pyc
26
+ *.pyo
27
+ *.egg-info/
28
+ .mypy_cache/
29
+ .ruff_cache/
30
+ .pytest_cache/
31
+ htmlcov/
32
+ .coverage
33
+ .coverage.*
34
+ coverage.xml
35
+
36
+ # Go
37
+ *.test
38
+ *.out
39
+ go.work.sum
40
+
41
+ # Generated proto artifacts (regenerate via `bun proto:gen`)
42
+ ts/_generated/
43
+ python/_generated/
44
+ go/_generated/
45
+
46
+ # Test-stack volumes (docker-compose persistent data)
47
+ infra/test-stack/data/
48
+ # synthetic consumer go binaries
49
+ synthetic-consumers/telemetry/go/nodii-synthetic-telemetry
50
+ synthetic-consumers/pii/go/nodii-synthetic-pii
51
+ synthetic-consumers/auth-sdk/go/nodii-synthetic-auth-sdk
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: nodii-address
3
+ Version: 0.1.0
4
+ Summary: Nodii canonical postal Address value-object + validator (ISO-3166-1 alpha-2; AddressInvalid).
5
+ Project-URL: Homepage, https://github.com/cognion-nucleus/nodii-libs
6
+ Project-URL: Repository, https://github.com/cognion-nucleus/nodii-libs
7
+ Author-email: Nodii <ops@nodii.co>
8
+ License: MIT
9
+ Keywords: address,nodii
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
@@ -0,0 +1,51 @@
1
+ # nodii-address
2
+
3
+ The canonical postal **Address** value-object + validator for the Nodii stack.
4
+ A pure value-object library (no I/O): a dataclass, a validator, and PII-field
5
+ tagging. Geocoding / distributor resolution is **not** here — that stays in
6
+ nodii-geo-service.
7
+
8
+ ```python
9
+ from nodii_address import (
10
+ validate_address,
11
+ is_valid_address,
12
+ AddressInvalid,
13
+ ADDRESS_PII_FIELDS,
14
+ Address,
15
+ )
16
+
17
+ addr = validate_address(
18
+ {"line1": "221B Baker St", "city": "London", "country_code": "gb"}
19
+ )
20
+ # → Address(line1="221B Baker St", city="London", country_code="GB", ...)
21
+
22
+ try:
23
+ validate_address({"line1": "x", "city": "y", "country_code": "ZZ"})
24
+ except AddressInvalid as e:
25
+ e.code # "ADDRESS_INVALID" → map to CUSTOMER_ADDRESS_INVALID
26
+ e.reason # "COUNTRY_CODE_INVALID"
27
+ e.field # "country_code"
28
+ ```
29
+
30
+ ## Shape
31
+
32
+ `{label?, line1, line2?, city, region?, postal_code?, country_code}` — matches
33
+ the gRPC `Address` message. `country_code` is ISO-3166-1 alpha-2 (validated
34
+ against the assigned set). Required: `line1`, `city`, `country_code`.
35
+
36
+ ## Validation
37
+
38
+ `validate_address(data)` trims string fields, upper-cases `country_code`, drops
39
+ empty optionals (→ `None`), and raises `AddressInvalid` (`.code`, `.reason`,
40
+ `.field`) on a missing required field or an invalid country code.
41
+ `is_valid_address` is the non-raising variant. Per-country postal/region format
42
+ rules are intentionally out of scope (geo-service owns those).
43
+
44
+ ## PII
45
+
46
+ `ADDRESS_PII_FIELDS` lists the fields that are PII (`line1`, `line2`, `city`,
47
+ `region`, `postal_code`) and must be encrypted under the owning subject's DEK at
48
+ rest by the consumer (via `nodii-pii`). `label` and `country_code` are not PII.
49
+ This library never stores or encrypts.
50
+
51
+ Shipped in parity with `@nodii/address` (TS) and `nodii.co/nodii-libs/go/address`.
@@ -0,0 +1,108 @@
1
+ # nodii-address — the canonical postal Address value-object + validator.
2
+
3
+ [project]
4
+ name = "nodii-address"
5
+ version = "0.1.0"
6
+ description = "Nodii canonical postal Address value-object + validator (ISO-3166-1 alpha-2; AddressInvalid)."
7
+ requires-python = ">=3.11"
8
+ license = { text = "MIT" }
9
+ authors = [
10
+ { name = "Nodii", email = "ops@nodii.co" },
11
+ ]
12
+ keywords = ["nodii", "address"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "License :: OSI Approved :: MIT License",
18
+ ]
19
+ dependencies = []
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/cognion-nucleus/nodii-libs"
23
+ Repository = "https://github.com/cognion-nucleus/nodii-libs"
24
+
25
+ [build-system]
26
+ requires = ["hatchling"]
27
+ build-backend = "hatchling.build"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/nodii_address"]
31
+
32
+ [tool.hatch.build.targets.sdist]
33
+ include = ["src/", "tests/", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "ruff>=0.7",
38
+ "mypy>=1.13",
39
+ "pytest>=8.3",
40
+ "pytest-asyncio>=0.24",
41
+ "pytest-cov>=6.0",
42
+ ]
43
+
44
+ [tool.ruff]
45
+ line-length = 100
46
+ target-version = "py311"
47
+
48
+ [tool.ruff.lint]
49
+ select = [
50
+ "E", # pycodestyle errors
51
+ "W", # pycodestyle warnings
52
+ "F", # pyflakes
53
+ "I", # isort
54
+ "B", # flake8-bugbear
55
+ "C4", # flake8-comprehensions
56
+ "UP", # pyupgrade
57
+ "N", # pep8-naming
58
+ "ASYNC", # flake8-async
59
+ "S", # flake8-bandit (security)
60
+ "PT", # flake8-pytest-style
61
+ "RET", # flake8-return
62
+ "SIM", # flake8-simplify
63
+ ]
64
+ ignore = [
65
+ "S101", # use of assert detected — fine in tests
66
+ ]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ "tests/**/*.py" = ["S101", "S105", "S106"] # bandit relaxed for tests
70
+
71
+ [tool.ruff.format]
72
+ quote-style = "double"
73
+ indent-style = "space"
74
+
75
+ [tool.mypy]
76
+ strict = true
77
+ python_version = "3.11"
78
+ warn_redundant_casts = true
79
+ warn_unused_ignores = true
80
+ warn_unreachable = true
81
+ disallow_untyped_decorators = true
82
+
83
+ [tool.pytest.ini_options]
84
+ testpaths = ["tests"]
85
+ asyncio_mode = "auto"
86
+ addopts = [
87
+ "-ra",
88
+ "--strict-markers",
89
+ "--strict-config",
90
+ "--cov=src",
91
+ "--cov-report=term-missing",
92
+ "--cov-report=html",
93
+ "--cov-report=xml",
94
+ ]
95
+
96
+ [tool.coverage.run]
97
+ source = ["src"]
98
+ branch = true
99
+
100
+ [tool.coverage.report]
101
+ fail_under = 80
102
+ show_missing = true
103
+ exclude_lines = [
104
+ "pragma: no cover",
105
+ "raise NotImplementedError",
106
+ "if TYPE_CHECKING:",
107
+ "if __name__ == \"__main__\":",
108
+ ]
@@ -0,0 +1,409 @@
1
+ """nodii-address — the canonical postal Address value-object + validator.
2
+
3
+ A pure value-object library (NO I/O): an ``Address`` dataclass, a
4
+ ``validate_address`` validator surfacing a typed ``AddressInvalid`` (mapped by
5
+ consumers to CUSTOMER_ADDRESS_INVALID), and the PII-field tagging. Geocoding /
6
+ distributor resolution is explicitly NOT here — it stays in nodii-geo-service
7
+ (``geo.DistributionResolver``). Request 5235eb58 (nodii-customer-service).
8
+
9
+ PII posture: line1/line2/city/region/postal_code are PII under the OWNING
10
+ subject's DEK (the consuming service encrypts via nodii-pii); ``label`` is a
11
+ non-PII tag and ``country_code`` is non-PII. This lib only TAGS which fields are
12
+ PII (see ``ADDRESS_PII_FIELDS``) — it never stores or encrypts.
13
+
14
+ Cross-lang parity with @nodii/address (TS) + go/address.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Mapping
20
+ from dataclasses import dataclass
21
+ from typing import Any, Literal
22
+
23
+ __version__ = "0.0.1"
24
+
25
+ LIB_NAME = "address"
26
+
27
+ #: Fields of :class:`Address` that carry PII and MUST be encrypted under the
28
+ #: owning subject's DEK at rest. ``label`` and ``country_code`` are NOT PII.
29
+ ADDRESS_PII_FIELDS: tuple[str, ...] = (
30
+ "line1",
31
+ "line2",
32
+ "city",
33
+ "region",
34
+ "postal_code",
35
+ )
36
+
37
+ AddressInvalidReason = Literal[
38
+ "LINE1_REQUIRED",
39
+ "CITY_REQUIRED",
40
+ "COUNTRY_CODE_REQUIRED",
41
+ "COUNTRY_CODE_INVALID",
42
+ "NOT_AN_OBJECT",
43
+ ]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class Address:
48
+ """Canonical postal address (gRPC ``Address`` message shape).
49
+
50
+ Required: ``line1``, ``city``, ``country_code``. Optional: ``label``,
51
+ ``line2``, ``region``, ``postal_code``. ``country_code`` is ISO-3166-1
52
+ alpha-2 (uppercase).
53
+ """
54
+
55
+ line1: str
56
+ city: str
57
+ country_code: str
58
+ label: str | None = None
59
+ line2: str | None = None
60
+ region: str | None = None
61
+ postal_code: str | None = None
62
+
63
+
64
+ class AddressInvalid(ValueError): # noqa: N818 — contract-specified name (spec 5235eb58 + TS/Go parity); not "AddressInvalidError"
65
+ """Raised by :func:`validate_address`. Consumers map ``.code`` (stable
66
+ string) to their transport status — for nodii-customer-service that is
67
+ ``CUSTOMER_ADDRESS_INVALID``.
68
+ """
69
+
70
+ code = "ADDRESS_INVALID"
71
+
72
+ def __init__(self, reason: AddressInvalidReason, field: str | None = None) -> None:
73
+ self.reason: AddressInvalidReason = reason
74
+ self.field = field
75
+ msg = (
76
+ f'address invalid: {reason} (field "{field}")'
77
+ if field
78
+ else f"address invalid: {reason}"
79
+ )
80
+ super().__init__(msg)
81
+
82
+
83
+ # ISO-3166-1 alpha-2 — the official assigned set (249 codes), byte-identical to
84
+ # the TS + Go ports so validation is cross-lang identical. Sorted; user-assigned
85
+ # ranges (e.g. XK) excluded.
86
+ ISO_3166_1_ALPHA2: frozenset[str] = frozenset(
87
+ [
88
+ "AD",
89
+ "AE",
90
+ "AF",
91
+ "AG",
92
+ "AI",
93
+ "AL",
94
+ "AM",
95
+ "AO",
96
+ "AQ",
97
+ "AR",
98
+ "AS",
99
+ "AT",
100
+ "AU",
101
+ "AW",
102
+ "AX",
103
+ "AZ",
104
+ "BA",
105
+ "BB",
106
+ "BD",
107
+ "BE",
108
+ "BF",
109
+ "BG",
110
+ "BH",
111
+ "BI",
112
+ "BJ",
113
+ "BL",
114
+ "BM",
115
+ "BN",
116
+ "BO",
117
+ "BQ",
118
+ "BR",
119
+ "BS",
120
+ "BT",
121
+ "BV",
122
+ "BW",
123
+ "BY",
124
+ "BZ",
125
+ "CA",
126
+ "CC",
127
+ "CD",
128
+ "CF",
129
+ "CG",
130
+ "CH",
131
+ "CI",
132
+ "CK",
133
+ "CL",
134
+ "CM",
135
+ "CN",
136
+ "CO",
137
+ "CR",
138
+ "CU",
139
+ "CV",
140
+ "CW",
141
+ "CX",
142
+ "CY",
143
+ "CZ",
144
+ "DE",
145
+ "DJ",
146
+ "DK",
147
+ "DM",
148
+ "DO",
149
+ "DZ",
150
+ "EC",
151
+ "EE",
152
+ "EG",
153
+ "EH",
154
+ "ER",
155
+ "ES",
156
+ "ET",
157
+ "FI",
158
+ "FJ",
159
+ "FK",
160
+ "FM",
161
+ "FO",
162
+ "FR",
163
+ "GA",
164
+ "GB",
165
+ "GD",
166
+ "GE",
167
+ "GF",
168
+ "GG",
169
+ "GH",
170
+ "GI",
171
+ "GL",
172
+ "GM",
173
+ "GN",
174
+ "GP",
175
+ "GQ",
176
+ "GR",
177
+ "GS",
178
+ "GT",
179
+ "GU",
180
+ "GW",
181
+ "GY",
182
+ "HK",
183
+ "HM",
184
+ "HN",
185
+ "HR",
186
+ "HT",
187
+ "HU",
188
+ "ID",
189
+ "IE",
190
+ "IL",
191
+ "IM",
192
+ "IN",
193
+ "IO",
194
+ "IQ",
195
+ "IR",
196
+ "IS",
197
+ "IT",
198
+ "JE",
199
+ "JM",
200
+ "JO",
201
+ "JP",
202
+ "KE",
203
+ "KG",
204
+ "KH",
205
+ "KI",
206
+ "KM",
207
+ "KN",
208
+ "KP",
209
+ "KR",
210
+ "KW",
211
+ "KY",
212
+ "KZ",
213
+ "LA",
214
+ "LB",
215
+ "LC",
216
+ "LI",
217
+ "LK",
218
+ "LR",
219
+ "LS",
220
+ "LT",
221
+ "LU",
222
+ "LV",
223
+ "LY",
224
+ "MA",
225
+ "MC",
226
+ "MD",
227
+ "ME",
228
+ "MF",
229
+ "MG",
230
+ "MH",
231
+ "MK",
232
+ "ML",
233
+ "MM",
234
+ "MN",
235
+ "MO",
236
+ "MP",
237
+ "MQ",
238
+ "MR",
239
+ "MS",
240
+ "MT",
241
+ "MU",
242
+ "MV",
243
+ "MW",
244
+ "MX",
245
+ "MY",
246
+ "MZ",
247
+ "NA",
248
+ "NC",
249
+ "NE",
250
+ "NF",
251
+ "NG",
252
+ "NI",
253
+ "NL",
254
+ "NO",
255
+ "NP",
256
+ "NR",
257
+ "NU",
258
+ "NZ",
259
+ "OM",
260
+ "PA",
261
+ "PE",
262
+ "PF",
263
+ "PG",
264
+ "PH",
265
+ "PK",
266
+ "PL",
267
+ "PM",
268
+ "PN",
269
+ "PR",
270
+ "PS",
271
+ "PT",
272
+ "PW",
273
+ "PY",
274
+ "QA",
275
+ "RE",
276
+ "RO",
277
+ "RS",
278
+ "RU",
279
+ "RW",
280
+ "SA",
281
+ "SB",
282
+ "SC",
283
+ "SD",
284
+ "SE",
285
+ "SG",
286
+ "SH",
287
+ "SI",
288
+ "SJ",
289
+ "SK",
290
+ "SL",
291
+ "SM",
292
+ "SN",
293
+ "SO",
294
+ "SR",
295
+ "SS",
296
+ "ST",
297
+ "SV",
298
+ "SX",
299
+ "SY",
300
+ "SZ",
301
+ "TC",
302
+ "TD",
303
+ "TF",
304
+ "TG",
305
+ "TH",
306
+ "TJ",
307
+ "TK",
308
+ "TL",
309
+ "TM",
310
+ "TN",
311
+ "TO",
312
+ "TR",
313
+ "TT",
314
+ "TV",
315
+ "TW",
316
+ "TZ",
317
+ "UA",
318
+ "UG",
319
+ "UM",
320
+ "US",
321
+ "UY",
322
+ "UZ",
323
+ "VA",
324
+ "VC",
325
+ "VE",
326
+ "VG",
327
+ "VI",
328
+ "VN",
329
+ "VU",
330
+ "WF",
331
+ "WS",
332
+ "YE",
333
+ "YT",
334
+ "ZA",
335
+ "ZM",
336
+ "ZW",
337
+ ] # noqa: SIM905 — split-string keeps the 249-code set byte-identical to TS/Go
338
+ )
339
+
340
+
341
+ def is_valid_country_code(code: str) -> bool:
342
+ """``True`` iff ``code`` is an assigned ISO-3166-1 alpha-2 code (uppercase)."""
343
+ return code in ISO_3166_1_ALPHA2
344
+
345
+
346
+ def _trim_or_empty(v: Any) -> str:
347
+ return v.strip() if isinstance(v, str) else ""
348
+
349
+
350
+ def _opt_trim(v: Any) -> str | None:
351
+ s = _trim_or_empty(v)
352
+ return s or None
353
+
354
+
355
+ def validate_address(data: Mapping[str, Any]) -> Address:
356
+ """Validate + normalize an address. Returns a clean :class:`Address` (string
357
+ fields trimmed; empty optionals dropped; ``country_code`` upper-cased) or
358
+ raises :class:`AddressInvalid`.
359
+
360
+ Rules: ``line1``, ``city``, ``country_code`` required + non-empty;
361
+ ``country_code`` must be a valid ISO-3166-1 alpha-2 code. Per-country
362
+ postal/region format rules are intentionally OUT of scope (geo-service).
363
+ """
364
+ if not isinstance(data, Mapping):
365
+ raise AddressInvalid("NOT_AN_OBJECT")
366
+ line1 = _trim_or_empty(data.get("line1"))
367
+ if line1 == "":
368
+ raise AddressInvalid("LINE1_REQUIRED", "line1")
369
+ city = _trim_or_empty(data.get("city"))
370
+ if city == "":
371
+ raise AddressInvalid("CITY_REQUIRED", "city")
372
+ cc_raw = _trim_or_empty(data.get("country_code"))
373
+ if cc_raw == "":
374
+ raise AddressInvalid("COUNTRY_CODE_REQUIRED", "country_code")
375
+ country_code = cc_raw.upper()
376
+ if not is_valid_country_code(country_code):
377
+ raise AddressInvalid("COUNTRY_CODE_INVALID", "country_code")
378
+ return Address(
379
+ line1=line1,
380
+ city=city,
381
+ country_code=country_code,
382
+ label=_opt_trim(data.get("label")),
383
+ line2=_opt_trim(data.get("line2")),
384
+ region=_opt_trim(data.get("region")),
385
+ postal_code=_opt_trim(data.get("postal_code")),
386
+ )
387
+
388
+
389
+ def is_valid_address(data: Mapping[str, Any]) -> bool:
390
+ """Non-throwing variant: ``True`` iff ``data`` validates."""
391
+ try:
392
+ validate_address(data)
393
+ return True
394
+ except AddressInvalid:
395
+ return False
396
+
397
+
398
+ __all__ = [
399
+ "ADDRESS_PII_FIELDS",
400
+ "ISO_3166_1_ALPHA2",
401
+ "LIB_NAME",
402
+ "Address",
403
+ "AddressInvalid",
404
+ "AddressInvalidReason",
405
+ "__version__",
406
+ "is_valid_address",
407
+ "is_valid_country_code",
408
+ "validate_address",
409
+ ]
@@ -0,0 +1,95 @@
1
+ """nodii-address validator tests (parity with @nodii/address TS + go/address)."""
2
+
3
+ import pytest
4
+
5
+ from nodii_address import (
6
+ ADDRESS_PII_FIELDS,
7
+ Address,
8
+ AddressInvalid,
9
+ is_valid_address,
10
+ is_valid_country_code,
11
+ validate_address,
12
+ )
13
+
14
+
15
+ def test_full_address_trims_and_uppercases_country() -> None:
16
+ a = validate_address(
17
+ {
18
+ "label": " home ",
19
+ "line1": " 221B Baker St ",
20
+ "line2": " Flat 2 ",
21
+ "city": " London ",
22
+ "region": " Greater London ",
23
+ "postal_code": " NW1 6XE ",
24
+ "country_code": "gb",
25
+ }
26
+ )
27
+ assert a == Address(
28
+ line1="221B Baker St",
29
+ city="London",
30
+ country_code="GB",
31
+ label="home",
32
+ line2="Flat 2",
33
+ region="Greater London",
34
+ postal_code="NW1 6XE",
35
+ )
36
+
37
+
38
+ def test_minimal_address_empty_optionals_become_none() -> None:
39
+ a = validate_address(
40
+ {"line1": "1 Main St", "city": "Bengaluru", "country_code": "IN", "region": " "}
41
+ )
42
+ assert a.line1 == "1 Main St"
43
+ assert a.city == "Bengaluru"
44
+ assert a.country_code == "IN"
45
+ assert a.region is None
46
+ assert a.postal_code is None
47
+ assert a.label is None
48
+
49
+
50
+ @pytest.mark.parametrize(
51
+ ("data", "reason", "field"),
52
+ [
53
+ ({"city": "X", "country_code": "US"}, "LINE1_REQUIRED", "line1"),
54
+ ({"line1": "L", "country_code": "US"}, "CITY_REQUIRED", "city"),
55
+ ({"line1": "L", "city": "X"}, "COUNTRY_CODE_REQUIRED", "country_code"),
56
+ (
57
+ {"line1": "L", "city": "X", "country_code": "ZZ"},
58
+ "COUNTRY_CODE_INVALID",
59
+ "country_code",
60
+ ),
61
+ ],
62
+ )
63
+ def test_address_invalid_reasons(data: dict, reason: str, field: str) -> None:
64
+ with pytest.raises(AddressInvalid) as exc:
65
+ validate_address(data)
66
+ assert exc.value.reason == reason
67
+ assert exc.value.field == field
68
+ assert exc.value.code == "ADDRESS_INVALID"
69
+
70
+
71
+ def test_not_an_object() -> None:
72
+ for bad in (None, 42, "str", ["a"]):
73
+ with pytest.raises(AddressInvalid) as exc:
74
+ validate_address(bad) # type: ignore[arg-type]
75
+ assert exc.value.reason == "NOT_AN_OBJECT"
76
+
77
+
78
+ def test_is_valid_country_code() -> None:
79
+ assert is_valid_country_code("US") is True
80
+ assert is_valid_country_code("IN") is True
81
+ assert is_valid_country_code("GB") is True
82
+ assert is_valid_country_code("ZZ") is False
83
+ assert is_valid_country_code("us") is False # lowercase not in the set
84
+ assert is_valid_country_code("USA") is False
85
+
86
+
87
+ def test_is_valid_address() -> None:
88
+ assert is_valid_address({"line1": "L", "city": "C", "country_code": "FR"}) is True
89
+ assert is_valid_address({"line1": "L", "city": "C", "country_code": "XX"}) is False
90
+
91
+
92
+ def test_pii_fields_tag() -> None:
93
+ assert ADDRESS_PII_FIELDS == ("line1", "line2", "city", "region", "postal_code")
94
+ assert "label" not in ADDRESS_PII_FIELDS
95
+ assert "country_code" not in ADDRESS_PII_FIELDS