py-uk-postcode 1.0.0__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.
postcode/__init__.py ADDED
@@ -0,0 +1,365 @@
1
+ """Validate, parse, format, and search UK postcodes.
2
+
3
+ This package is a Python port of ``postcode-js``. Validation intentionally
4
+ checks the shape of a postcode, not whether the postcode currently exists.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Literal
12
+
13
+ __version__ = "1.0.0"
14
+
15
+ DISTRICT_SPLIT_REGEX = re.compile(
16
+ r"^([A-Za-z]{1,2}[0-9])([A-Za-z])$"
17
+ )
18
+ UNIT_REGEX = re.compile(r"[A-Za-z]{2}$")
19
+ INCODE_REGEX = re.compile(r"[0-9][A-Za-z]{2}$")
20
+ OUTCODE_REGEX = re.compile(r"^[A-Za-z]{1,2}[0-9][A-Za-z0-9]?$")
21
+ POSTCODE_REGEX = re.compile(
22
+ r"^[A-Za-z]{1,2}[0-9][A-Za-z0-9]?\s*[0-9][A-Za-z]{2}$"
23
+ )
24
+ POSTCODE_CORPUS_REGEX = re.compile(
25
+ r"[A-Za-z]{1,2}[0-9][A-Za-z0-9]?\s*[0-9][A-Za-z]{2}"
26
+ )
27
+ AREA_REGEX = re.compile(r"^[A-Za-z]{1,2}")
28
+ FIXABLE_REGEX = re.compile(
29
+ r"^\s*[A-Za-z01]{1,2}[0-9OoIi][A-Za-z0-9]?"
30
+ r"\s*[0-9OoIi][A-Za-z01]{2}\s*$"
31
+ )
32
+
33
+ SPACE_REGEX = re.compile(r"\s+")
34
+
35
+
36
+ class _PostcodeResult:
37
+ """Shared conveniences for parsed postcode result objects."""
38
+
39
+ valid: bool
40
+ postcode: str | None
41
+ incode: str | None
42
+ outcode: str | None
43
+ area: str | None
44
+ district: str | None
45
+ sub_district: str | None
46
+ sector: str | None
47
+ unit: str | None
48
+
49
+ @property
50
+ def subDistrict(self) -> str | None: # noqa: N802
51
+ """JavaScript-compatible alias for :attr:`sub_district`."""
52
+
53
+ return self.sub_district
54
+
55
+ def __getitem__(self, key: str) -> object:
56
+ """Allow dictionary-style access, as with the JavaScript result object."""
57
+
58
+ if key == "subDistrict":
59
+ key = "sub_district"
60
+ try:
61
+ return getattr(self, key)
62
+ except AttributeError as error:
63
+ raise KeyError(key) from error
64
+
65
+ def as_dict(self, *, camel_case: bool = False) -> dict[str, object]:
66
+ """Return the parsed result as a dictionary."""
67
+
68
+ result: dict[str, object] = {
69
+ "valid": self.valid,
70
+ "postcode": self.postcode,
71
+ "incode": self.incode,
72
+ "outcode": self.outcode,
73
+ "area": self.area,
74
+ "district": self.district,
75
+ "sub_district": self.sub_district,
76
+ "sector": self.sector,
77
+ "unit": self.unit,
78
+ }
79
+ if camel_case:
80
+ result["subDistrict"] = result.pop("sub_district")
81
+ return result
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ValidPostcode(_PostcodeResult):
86
+ """The normalised components of a valid-looking postcode."""
87
+
88
+ valid: Literal[True]
89
+ postcode: str
90
+ incode: str
91
+ outcode: str
92
+ area: str
93
+ district: str
94
+ sub_district: str | None
95
+ sector: str
96
+ unit: str
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class InvalidPostcode(_PostcodeResult):
101
+ """The result returned when a postcode does not have a valid shape."""
102
+
103
+ valid: Literal[False] = False
104
+ postcode: None = None
105
+ incode: None = None
106
+ outcode: None = None
107
+ area: None = None
108
+ district: None = None
109
+ sub_district: None = None
110
+ sector: None = None
111
+ unit: None = None
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class ReplaceResult:
116
+ """Postcodes found in a corpus and the text after replacement."""
117
+
118
+ match: list[str]
119
+ result: str
120
+
121
+ def __getitem__(self, key: str) -> object:
122
+ try:
123
+ return getattr(self, key)
124
+ except AttributeError as error:
125
+ raise KeyError(key) from error
126
+
127
+
128
+ ParsedPostcode = ValidPostcode | InvalidPostcode
129
+
130
+
131
+ def _sanitize(value: str) -> str:
132
+ """Remove whitespace and uppercase a string."""
133
+
134
+ return SPACE_REGEX.sub("", value).upper()
135
+
136
+
137
+ def is_valid(postcode: str) -> bool:
138
+ """Return whether *postcode* conforms to a supported UK postcode shape."""
139
+
140
+ return POSTCODE_REGEX.match(postcode) is not None
141
+
142
+
143
+ def valid_outcode(outcode: str) -> bool:
144
+ """Return whether *outcode* has a valid outward-code shape."""
145
+
146
+ return OUTCODE_REGEX.match(outcode) is not None
147
+
148
+
149
+ def to_normalised(postcode: str) -> str | None:
150
+ """Return an uppercase, correctly spaced postcode, or ``None`` if invalid."""
151
+
152
+ outcode = to_outcode(postcode)
153
+ if outcode is None:
154
+ return None
155
+ incode = to_incode(postcode)
156
+ assert incode is not None
157
+ return f"{outcode} {incode}"
158
+
159
+
160
+ def to_outcode(postcode: str) -> str | None:
161
+ """Return the correctly formatted outward code, or ``None`` if invalid."""
162
+
163
+ if not is_valid(postcode):
164
+ return None
165
+ return INCODE_REGEX.sub("", _sanitize(postcode))
166
+
167
+
168
+ def to_incode(postcode: str) -> str | None:
169
+ """Return the correctly formatted inward code, or ``None`` if invalid."""
170
+
171
+ if not is_valid(postcode):
172
+ return None
173
+ match_result = INCODE_REGEX.search(_sanitize(postcode))
174
+ assert match_result is not None
175
+ return match_result.group(0)
176
+
177
+
178
+ def to_area(postcode: str) -> str | None:
179
+ """Return the postcode area, or ``None`` if invalid."""
180
+
181
+ if not is_valid(postcode):
182
+ return None
183
+ match_result = AREA_REGEX.search(_sanitize(postcode))
184
+ assert match_result is not None
185
+ return match_result.group(0)
186
+
187
+
188
+ def to_sector(postcode: str) -> str | None:
189
+ """Return the postcode sector, or ``None`` if invalid."""
190
+
191
+ outcode = to_outcode(postcode)
192
+ incode = to_incode(postcode)
193
+ if outcode is None or incode is None:
194
+ return None
195
+ return f"{outcode} {incode[0]}"
196
+
197
+
198
+ def to_unit(postcode: str) -> str | None:
199
+ """Return the postcode unit, or ``None`` if invalid."""
200
+
201
+ if not is_valid(postcode):
202
+ return None
203
+ match_result = UNIT_REGEX.search(_sanitize(postcode))
204
+ assert match_result is not None
205
+ return match_result.group(0)
206
+
207
+
208
+ def to_district(postcode: str) -> str | None:
209
+ """Return the postcode district, or ``None`` if invalid."""
210
+
211
+ outcode = to_outcode(postcode)
212
+ if outcode is None:
213
+ return None
214
+ split = DISTRICT_SPLIT_REGEX.match(outcode)
215
+ if split is None:
216
+ return outcode
217
+ return split.group(1)
218
+
219
+
220
+ def to_sub_district(postcode: str) -> str | None:
221
+ """Return the sub-district, or ``None`` if absent or the postcode is invalid."""
222
+
223
+ outcode = to_outcode(postcode)
224
+ if outcode is None or DISTRICT_SPLIT_REGEX.match(outcode) is None:
225
+ return None
226
+ return outcode
227
+
228
+
229
+ def parse(postcode: str) -> ParsedPostcode:
230
+ """Parse *postcode* into a typed result containing all postcode components."""
231
+
232
+ if not is_valid(postcode):
233
+ return InvalidPostcode()
234
+
235
+ normalised = to_normalised(postcode)
236
+ incode = to_incode(postcode)
237
+ outcode = to_outcode(postcode)
238
+ area = to_area(postcode)
239
+ district = to_district(postcode)
240
+ sector = to_sector(postcode)
241
+ unit = to_unit(postcode)
242
+
243
+ # The validation above guarantees all of these components are present.
244
+ assert normalised is not None
245
+ assert incode is not None
246
+ assert outcode is not None
247
+ assert area is not None
248
+ assert district is not None
249
+ assert sector is not None
250
+ assert unit is not None
251
+
252
+ return ValidPostcode(
253
+ valid=True,
254
+ postcode=normalised,
255
+ incode=incode,
256
+ outcode=outcode,
257
+ area=area,
258
+ district=district,
259
+ sub_district=to_sub_district(postcode),
260
+ sector=sector,
261
+ unit=unit,
262
+ )
263
+
264
+
265
+ def match(corpus: str) -> list[str]:
266
+ """Return postcode-shaped matches from a body of text."""
267
+
268
+ return [result.group(0) for result in POSTCODE_CORPUS_REGEX.finditer(corpus)]
269
+
270
+
271
+ def replace(corpus: str, replace_with: str = "") -> ReplaceResult:
272
+ """Replace postcode-shaped text and return both the matches and new corpus."""
273
+
274
+ return ReplaceResult(
275
+ match=match(corpus),
276
+ result=POSTCODE_CORPUS_REGEX.sub(replace_with, corpus),
277
+ )
278
+
279
+
280
+ _TO_LETTER = {"0": "O", "1": "I"}
281
+ _TO_NUMBER = {"O": "0", "I": "1"}
282
+
283
+
284
+ def _coerce(pattern: str, value: str) -> str:
285
+ output = []
286
+ for index, character in enumerate(value):
287
+ target = pattern[index]
288
+ if target == "N":
289
+ output.append(_TO_NUMBER.get(character, character))
290
+ elif target == "L":
291
+ output.append(_TO_LETTER.get(character, character))
292
+ else:
293
+ output.append(character)
294
+ return "".join(output)
295
+
296
+
297
+ def _coerce_outcode(value: str) -> str:
298
+ patterns = {2: "LN", 3: "L??", 4: "LLN?"}
299
+ return _coerce(patterns[len(value)], value)
300
+
301
+
302
+ def fix(value: str) -> str:
303
+ """Correct common O/0 and I/1 mistakes and normalise obvious postcodes.
304
+
305
+ If the input cannot be reliably coerced into a postcode shape, it is
306
+ returned unchanged.
307
+ """
308
+
309
+ if FIXABLE_REGEX.match(value) is None:
310
+ return value
311
+ sanitized = _sanitize(value.strip())
312
+ inward = sanitized[-3:]
313
+ return f'{_coerce_outcode(sanitized[:-3])} {_coerce("NLL", inward)}'
314
+
315
+
316
+ # Migration aliases matching the original JavaScript named exports.
317
+ isValid = is_valid
318
+ validOutcode = valid_outcode
319
+ toNormalised = to_normalised
320
+ toOutcode = to_outcode
321
+ toIncode = to_incode
322
+ toArea = to_area
323
+ toDistrict = to_district
324
+ toSubDistrict = to_sub_district
325
+ toSector = to_sector
326
+ toUnit = to_unit
327
+
328
+ __all__ = [
329
+ "AREA_REGEX",
330
+ "DISTRICT_SPLIT_REGEX",
331
+ "FIXABLE_REGEX",
332
+ "INCODE_REGEX",
333
+ "InvalidPostcode",
334
+ "OUTCODE_REGEX",
335
+ "POSTCODE_CORPUS_REGEX",
336
+ "POSTCODE_REGEX",
337
+ "ParsedPostcode",
338
+ "ReplaceResult",
339
+ "UNIT_REGEX",
340
+ "ValidPostcode",
341
+ "fix",
342
+ "isValid",
343
+ "is_valid",
344
+ "match",
345
+ "parse",
346
+ "replace",
347
+ "toArea",
348
+ "toDistrict",
349
+ "toIncode",
350
+ "toNormalised",
351
+ "toOutcode",
352
+ "toSector",
353
+ "toSubDistrict",
354
+ "toUnit",
355
+ "to_area",
356
+ "to_district",
357
+ "to_incode",
358
+ "to_normalised",
359
+ "to_outcode",
360
+ "to_sector",
361
+ "to_sub_district",
362
+ "to_unit",
363
+ "validOutcode",
364
+ "valid_outcode",
365
+ ]
postcode/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,340 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-uk-postcode
3
+ Version: 1.0.0
4
+ Summary: UK postcode validation, parsing, formatting, and text utilities
5
+ Author: Area360
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/area360-uk/py-postcode
8
+ Project-URL: Documentation, https://github.com/area360-uk/py-postcode#readme
9
+ Project-URL: Repository, https://github.com/area360-uk/py-postcode
10
+ Project-URL: Issues, https://github.com/area360-uk/py-postcode/issues
11
+ Project-URL: Changelog, https://github.com/area360-uk/py-postcode/blob/main/CHANGELOG.md
12
+ Keywords: uk,postcode,validation,parsing
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.2; extra == "dev"
28
+ Requires-Dist: mypy>=1.11; extra == "dev"
29
+ Requires-Dist: pytest>=8; extra == "dev"
30
+ Requires-Dist: pytest-cov>=5; extra == "dev"
31
+ Requires-Dist: ruff>=0.6; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ <p align="center">
35
+ <img src="https://raw.githubusercontent.com/area360-uk/py-postcode/main/assets/py-postcode.png" alt="Py UK Postcode" width="100%">
36
+ </p>
37
+
38
+ # Py UK Postcode
39
+
40
+ > Validate and parse UK postcodes in Python
41
+
42
+ [![CI](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml/badge.svg)](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/py-uk-postcode.svg)](https://pypi.org/project/py-uk-postcode/)
44
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
45
+
46
+ Utility methods for UK postcodes, including validating the shape of a postcode
47
+ and extracting postcode elements such as incodes, outcodes, areas, and
48
+ [more](#definitions).
49
+
50
+ The implementation is a close Python port of [ideal-postcodes/postcode](https://github.com/ideal-postcodes/postcode), whose format rules
51
+ were tested against roughly 1.7 million postcodes from the ONS Postcode
52
+ Directory.
53
+
54
+ ## Features
55
+
56
+ - [Check](#validate) whether a postcode conforms to the
57
+ [correct format](https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#formatting)
58
+ - Small, single-purpose functions with no runtime dependencies
59
+ - [Extract](#parse) postcode elements such as the incode, outcode, and sector
60
+ - Search for and replace postcodes in larger bodies of text
61
+ - Correct common `O`/`0` and `I`/`1` input mistakes
62
+ - Fully typed Python API
63
+
64
+ ## Acknowledgments and credits
65
+
66
+ `py-uk-postcode` is a Python port of the original [ideal-postcodes/postcode](https://github.com/ideal-postcodes/postcode), created
67
+ by Ideal Postcodes and released under the MIT License. The original project's
68
+ API design, implementation, documentation, and test fixtures formed the basis
69
+ of this port.
70
+
71
+ ## Links
72
+
73
+ - [GitHub repository](https://github.com/area360-uk/py-postcode)
74
+ - [Package on PyPI](https://pypi.org/project/py-uk-postcode/)
75
+ - [Issue tracker](https://github.com/area360-uk/py-postcode/issues)
76
+ - [Postcode element definitions](#definitions)
77
+ - [Notes](#notes)
78
+
79
+ ## Getting started
80
+
81
+ ### Installation
82
+
83
+ With `pip`:
84
+
85
+ ```bash
86
+ python -m pip install py-uk-postcode
87
+ ```
88
+
89
+ With `uv`:
90
+
91
+ ```bash
92
+ uv add py-uk-postcode
93
+ ```
94
+
95
+ With Poetry:
96
+
97
+ ```bash
98
+ poetry add py-uk-postcode
99
+ ```
100
+
101
+ With PDM:
102
+
103
+ ```bash
104
+ pdm add py-uk-postcode
105
+ ```
106
+
107
+ With Conda, create or activate an environment and install the package from
108
+ PyPI:
109
+
110
+ ```bash
111
+ conda create --name postcode python=3.14 pip
112
+ conda activate postcode
113
+ python -m pip install py-uk-postcode
114
+ ```
115
+
116
+ The distribution is named `py-uk-postcode`; import it in Python as `postcode`.
117
+
118
+ ### Validate
119
+
120
+ ```python
121
+ from postcode import is_valid
122
+
123
+ is_valid("AA1 1AB") # => True
124
+ ```
125
+
126
+ ### Parse
127
+
128
+ Pass a string to `parse()`. It returns either a `ValidPostcode` or an
129
+ `InvalidPostcode`, both of which expose their values as attributes.
130
+
131
+ #### Valid postcode
132
+
133
+ ```python
134
+ from postcode import parse
135
+
136
+ result = parse("Sw1A 2aa")
137
+
138
+ result.postcode # => "SW1A 2AA"
139
+ result.outcode # => "SW1A"
140
+ result.incode # => "2AA"
141
+ result.area # => "SW"
142
+ result.district # => "SW1"
143
+ result.unit # => "AA"
144
+ result.sector # => "SW1A 2"
145
+ result.sub_district # => "SW1A"
146
+ result.valid # => True
147
+ ```
148
+
149
+ #### Invalid postcode
150
+
151
+ ```python
152
+ result = parse(" Oh no, ): ")
153
+
154
+ result.postcode # => None
155
+ result.outcode # => None
156
+ result.incode # => None
157
+ result.area # => None
158
+ result.district # => None
159
+ result.unit # => None
160
+ result.sector # => None
161
+ result.sub_district # => None
162
+ result.valid # => False
163
+ ```
164
+
165
+ #### Type narrowing
166
+
167
+ `parse()` returns a typed union. Type checkers can narrow it by checking the
168
+ literal `valid` attribute:
169
+
170
+ ```python
171
+ from postcode import parse
172
+
173
+ result = parse("SW1A 2AA")
174
+
175
+ if result.valid:
176
+ print(result.outcode.lower())
177
+ if result.sub_district is not None:
178
+ print(result.sub_district.lower())
179
+ else:
180
+ print("Invalid postcode")
181
+ ```
182
+
183
+ #### Valid postcode object
184
+
185
+ | Postcode | `.outcode` | `.incode` | `.area` | `.district` | `.sub_district` | `.sector` | `.unit` |
186
+ |----------|------------|-----------|---------|-------------|-----------------|-----------|---------|
187
+ | AA9A 9AA | AA9A | 9AA | AA | AA9 | AA9A | AA9A 9 | AA |
188
+ | A9A 9AA | A9A | 9AA | A | A9 | A9A | A9A 9 | AA |
189
+ | A9 9AA | A9 | 9AA | A | A9 | `None` | A9 9 | AA |
190
+ | A99 9AA | A99 | 9AA | A | A99 | `None` | A99 9 | AA |
191
+ | AA9 9AA | AA9 | 9AA | AA | AA9 | `None` | AA9 9 | AA |
192
+ | AA99 9AA | AA99 | 9AA | AA | AA99 | `None` | AA99 9 | AA |
193
+
194
+ ### Exported functions
195
+
196
+ If you need a single value, import the corresponding function directly.
197
+
198
+ #### Validation
199
+
200
+ ```python
201
+ from postcode import is_valid, valid_outcode
202
+
203
+ is_valid("Sw1A 2aa") # => True
204
+ valid_outcode("SW1A") # => True
205
+ ```
206
+
207
+ #### Formatting
208
+
209
+ ```python
210
+ from postcode import (
211
+ to_area,
212
+ to_district,
213
+ to_incode,
214
+ to_normalised,
215
+ to_outcode,
216
+ to_sector,
217
+ to_sub_district,
218
+ to_unit,
219
+ )
220
+
221
+ to_normalised("Sw1A 2aa") # => "SW1A 2AA"
222
+ to_outcode("Sw1A 2aa") # => "SW1A"
223
+ to_incode("Sw1A 2aa") # => "2AA"
224
+ to_area("Sw1A 2aa") # => "SW"
225
+ to_district("Sw1A 2aa") # => "SW1"
226
+ to_sub_district("Sw1A 2aa") # => "SW1A"
227
+ to_sector("Sw1A 2aa") # => "SW1A 2"
228
+ to_unit("Sw1A 2aa") # => "AA"
229
+ ```
230
+
231
+ All formatting functions return `None` when given an invalid postcode.
232
+
233
+ #### Fix
234
+
235
+ `fix()` attempts to clean up a postcode without validating it. It replaces
236
+ commonly confused characters (`O`/`0` and `I`/`1`), uppercases the value, and
237
+ corrects its spacing. If the input cannot be reliably fixed, the original
238
+ string is returned.
239
+
240
+ ```python
241
+ from postcode import fix, parse
242
+
243
+ fix("SWIA 2AA") # => "SW1A 2AA"
244
+ fix("SW1A 21A") # => "SW1A 2IA"
245
+ fix("SW1A OAA") # => "SW1A 0AA"
246
+ fix("SW1A 20A") # => "SW1A 2OA"
247
+ fix(" SW1A 2AO") # => "SW1A 2AO"
248
+ fix("sw1a 2aa") # => "SW1A 2AA"
249
+
250
+ result = parse(fix("SW1A 2A0"))
251
+ result.incode # => "2AO"
252
+
253
+ fix("12a") # => "12a"
254
+ ```
255
+
256
+ #### Extract and replace
257
+
258
+ `match()` retrieves postcode-shaped values from a body of text. Matches retain
259
+ their original casing and spacing.
260
+
261
+ ```python
262
+ from postcode import match, to_normalised, to_outcode
263
+
264
+ matches = match("The two addresses are SW1A2aa and SW1A 2AB")
265
+ # => ["SW1A2aa", "SW1A 2AB"]
266
+
267
+ [to_normalised(value) for value in matches]
268
+ # => ["SW1A 2AA", "SW1A 2AB"]
269
+
270
+ [to_outcode(value) for value in matches]
271
+ # => ["SW1A", "SW1A"]
272
+
273
+ match("Some London outward codes are SW1A, NW1 and E1") # => []
274
+ ```
275
+
276
+ `replace()` replaces postcode-shaped values and returns a `ReplaceResult`
277
+ containing the matches and resulting text:
278
+
279
+ ```python
280
+ from postcode import replace
281
+
282
+ replacement = replace("The two addresses are SW1A2AA and SW1A 2AB")
283
+ replacement.match
284
+ # => ["SW1A2AA", "SW1A 2AB"]
285
+ replacement.result
286
+ # => "The two addresses are and "
287
+
288
+ replace("The address is SW1A 2AA", "Downing Street").result
289
+ # => "The address is Downing Street"
290
+ ```
291
+
292
+ ### Regular expressions
293
+
294
+ The compiled regular expressions used by the package are public:
295
+
296
+ ```python
297
+ from postcode import POSTCODE_REGEX
298
+
299
+ bool(POSTCODE_REGEX.match("SW1A 2AA")) # => True
300
+ ```
301
+
302
+ The other exports are `AREA_REGEX`, `DISTRICT_SPLIT_REGEX`, `FIXABLE_REGEX`,
303
+ `INCODE_REGEX`, `OUTCODE_REGEX`, `POSTCODE_CORPUS_REGEX`, and `UNIT_REGEX`.
304
+
305
+ ## Definitions
306
+
307
+ A UK postcode is made up of an outward code and an inward code. The outward
308
+ code contains the area and district, and can include a sub-district. The inward
309
+ code contains the sector digit and unit letters. For example, in `SW1A 2AA`:
310
+
311
+ - Area: `SW`
312
+ - District: `SW1`
313
+ - Sub-district: `SW1A`
314
+ - Outcode: `SW1A`
315
+ - Incode: `2AA`
316
+ - Sector: `SW1A 2`
317
+ - Unit: `AA`
318
+
319
+ ## Notes
320
+
321
+ Postcodes cannot be authoritatively validated with a regular expression,
322
+ however complex. True validation requires checking against a current postcode
323
+ dataset. This package validates the *shape* of a postcode and can therefore
324
+ produce false positives or negatives when used as an existence check.
325
+
326
+ ## Development
327
+
328
+ ```bash
329
+ python -m pip install -e ".[dev]"
330
+ pytest
331
+ ruff check .
332
+ mypy
333
+ python -m build
334
+ ```
335
+
336
+ ## License
337
+
338
+ MIT
339
+
340
+ Contains Ordnance Survey Data © Crown Copyright & Database Right.
@@ -0,0 +1,7 @@
1
+ postcode/__init__.py,sha256=zOPNnFlSxe_cDtFlDnV0R4w-_6M-7M7glRWoKbcvxpc,9638
2
+ postcode/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
3
+ py_uk_postcode-1.0.0.dist-info/licenses/LICENSE,sha256=mJHzYC7jnX1lwuCGv6jwZDdZCePqcjgiyH3E4E3HySg,1137
4
+ py_uk_postcode-1.0.0.dist-info/METADATA,sha256=Qs9Rv1YNHk5xl8yXsBNGJv0sbxy7-DUVjHWvk3wKQVA,9784
5
+ py_uk_postcode-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ py_uk_postcode-1.0.0.dist-info/top_level.txt,sha256=yHUOHTtClfsmTkD3sMkdQaTLP7XrR2Mj3NW29Pe2LTY,9
7
+ py_uk_postcode-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Area360 (Python port)
4
+ Copyright (c) 2020 IDDQD Limited (Original implementation)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ postcode