postfinder 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,21 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python }}
19
+ # No install step: the library has no dependencies and the suite runs on
20
+ # stdlib unittest, so this is the real "does it work from scratch".
21
+ - run: PYTHONPATH=src python -m unittest discover -s tests -v
@@ -0,0 +1,25 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ # Trusted publishing: PyPI verifies this workflow's identity, so there is no
8
+ # long lived API token in the repository to leak. Configure the publisher at
9
+ # pypi.org/manage/project/postfinder/settings/publishing/ before the first tag.
10
+ permissions:
11
+ id-token: write
12
+ contents: read
13
+
14
+ jobs:
15
+ publish:
16
+ runs-on: ubuntu-latest
17
+ environment: pypi
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - run: PYTHONPATH=src python -m unittest discover -s tests
24
+ - run: pip install build && python -m build
25
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .venv/
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
6
+
7
+ - `search`, `nearby`, `place`, `countries`, `country`, `region`, `locality`,
8
+ `category`, `postcodes` and `postcode`, covering the free read only surface of
9
+ api.postfinder.io.
10
+ - Every row that can build the path to its own page on postfinder.io does.
11
+ - `PostcodeIndex.suburbs_in` answers from an index already fetched, building its
12
+ map once rather than walking every region per lookup.
13
+ - No dependencies.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PostFinder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.5
2
+ Name: postfinder
3
+ Version: 0.1.0
4
+ Summary: Find post offices, parcel lockers and post boxes near a point, and look up postcodes. Free and keyless.
5
+ Project-URL: Homepage, https://postfinder.io
6
+ Project-URL: Documentation, https://postfinder.io/en/docs/
7
+ Project-URL: Source, https://github.com/postfinder/postfinder-python
8
+ Project-URL: Changelog, https://github.com/postfinder/postfinder-python/blob/main/CHANGELOG.md
9
+ Author: PostFinder
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 PostFinder
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: australia,australia post locations,geonames,nearest location api,new zealand,openstreetmap,parcel locker api,post box locations,post office locator,postcode api,postcode lookup,store locator api
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Scientific/Engineering :: GIS
43
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.9
46
+ Description-Content-Type: text/markdown
47
+
48
+ # postfinder
49
+
50
+ **Post offices, parcel lockers and post boxes, as an API.** Ask what is nearest
51
+ a coordinate, look up what a postcode covers, or read a suburb's locations.
52
+
53
+ - Free and keyless. No account, no quota to buy, nothing to configure
54
+ - Answers are cached at the edge, so a repeated question is fast and costs
55
+ nobody anything
56
+ - Every row knows the page it belongs to on postfinder.io, so linking out takes
57
+ no second request
58
+ - No dependencies
59
+
60
+ ```sh
61
+ pip install postfinder
62
+ ```
63
+
64
+ ## Quick start
65
+
66
+ ```python
67
+ from postfinder import PostFinder
68
+
69
+ pf = PostFinder()
70
+
71
+ for p in pf.nearby(-37.7404, 144.9633, category="post-offices", country="australia"):
72
+ print(p.name, p.address, f"{p.distance_m} m")
73
+ ```
74
+
75
+ ```
76
+ Coburg Post Office 484 Sydney Rd 420 m
77
+ Coburg North LPO 12 Elizabeth St 1800 m
78
+ ```
79
+
80
+ Nearest first, within 50km, at most 30 rows. That is the question "where do I
81
+ post this", which is a different question from "list every post box in
82
+ Victoria".
83
+
84
+ ## The six categories
85
+
86
+ ```python
87
+ from postfinder import CATEGORIES # what nearby() accepts
88
+ ```
89
+
90
+ `post-offices`, `post-boxes`, `express-post-boxes`, `parcel-lockers`,
91
+ `drop-off-points`, `collection-points`.
92
+
93
+ A name that is not one of those raises `ValueError` before a request goes out,
94
+ rather than spending a round trip to be told 400.
95
+
96
+ ## Typeahead
97
+
98
+ ```python
99
+ for hit in pf.search("coburg"):
100
+ print(hit.kind, hit.name, hit.postcode, hit.path)
101
+ ```
102
+
103
+ ```
104
+ locality Coburg 3058 /en/australia/victoria/coburg/
105
+ place Coburg Post Office 3058 /en/australia/victoria/coburg/
106
+ ```
107
+
108
+ Two characters minimum: below that the client returns an empty list without
109
+ asking, which is what the service answers anyway. Debounce by at least 150ms.
110
+ Firing on every keystroke spends bandwidth for no better answer.
111
+
112
+ ## Postcodes
113
+
114
+ A postcode is not a suburb. 3058 is Coburg, Coburg North and Merlynston, and an
115
+ address in any of them is written with the same four digits.
116
+
117
+ ```python
118
+ detail = pf.postcode("australia", "3058")
119
+ for suburb in detail.localities:
120
+ print(suburb.name, suburb.place_count, suburb.path)
121
+ ```
122
+
123
+ The whole country comes in one response, and it is meant to be kept:
124
+
125
+ ```python
126
+ index = pf.postcodes("australia") # one request
127
+ index.suburbs_in("3058") # no request, and no rescan
128
+ index.suburbs_in("2044")
129
+ ```
130
+
131
+ `suburbs_in` builds its map on first use and keeps it, so resolving a column of
132
+ ten thousand postcodes is one pass over the index rather than ten thousand walks
133
+ through every state.
134
+
135
+ ## A location, and a suburb
136
+
137
+ ```python
138
+ place = pf.place("k7m2p9x4")
139
+ print(place.place.name, place.locality.name, place.path)
140
+ print(place.reviews.summary.count, place.reviews.summary.average)
141
+
142
+ suburb = pf.locality("australia", "victoria", "coburg")
143
+ print(suburb.locality.place_count, suburb.count_of("parcel-lockers"))
144
+ for p in suburb.places:
145
+ print(p.name, suburb.path_of(p))
146
+ ```
147
+
148
+ A place's public id is permanent. It is minted once and never derived from a
149
+ source record, so a feed that renumbers its rows does not change it. Store the
150
+ id, not the name or the path.
151
+
152
+ ## Browsing
153
+
154
+ ```python
155
+ pf.countries() # every country with pages
156
+ pf.country("australia") # its states
157
+ pf.region("australia", "victoria", limit=100) # its localities, a page at a time
158
+ pf.category("australia", "parcel-lockers") # counts per state, busiest suburbs
159
+ ```
160
+
161
+ ## Errors
162
+
163
+ ```python
164
+ from postfinder import NotFound, BadRequest, RateLimited, PostFinderError
165
+
166
+ try:
167
+ pf.place(stored_id)
168
+ except NotFound:
169
+ ... # retired, or never there
170
+ ```
171
+
172
+ `NotFound` is ordinary rather than a failure: a suburb with no locations has no
173
+ page, and a location that closed is retired. Every error carries the `status`,
174
+ `title` and `detail` the service sent, because that is the part that says what to
175
+ do about it.
176
+
177
+ ## Being a good citizen
178
+
179
+ The API is free and asks for care in return: around a thousand requests a month
180
+ from one address, results kept rather than re-fetched, typing debounced. If you
181
+ need more than that, say what you are building at
182
+ [postfinder.io/en/contact/](https://postfinder.io/en/contact/).
183
+
184
+ Introduce yourself and it is easier to help you before a rate limit does:
185
+
186
+ ```python
187
+ pf = PostFinder(contact="https://example.com/about-our-bot")
188
+ ```
189
+
190
+ ## Attribution
191
+
192
+ Locations come from OpenStreetMap (ODbL), localities and postcodes from GeoNames
193
+ (CC BY 4.0). If you publish what you get back, you carry those credits with it.
194
+ The [sources page](https://postfinder.io/en/legal/) names each one.
195
+
196
+ ## Also available
197
+
198
+ | | |
199
+ |---|---|
200
+ | JavaScript and TypeScript | [`@postfinder/client`](https://www.npmjs.com/package/@postfinder/client) |
201
+ | React | [`@postfinder/react`](https://www.npmjs.com/package/@postfinder/react) |
202
+ | Vue | [`@postfinder/vue`](https://www.npmjs.com/package/@postfinder/vue) |
203
+ | Go | [`postfinder-go`](https://github.com/postfinder/postfinder-go) |
204
+
205
+ Looking for Australian street addresses rather than locations? That is
206
+ [Locio](https://locio.com.au): G-NAF address autocomplete, validation and
207
+ geocoding.
208
+
209
+ ## Licence
210
+
211
+ MIT.
@@ -0,0 +1,164 @@
1
+ # postfinder
2
+
3
+ **Post offices, parcel lockers and post boxes, as an API.** Ask what is nearest
4
+ a coordinate, look up what a postcode covers, or read a suburb's locations.
5
+
6
+ - Free and keyless. No account, no quota to buy, nothing to configure
7
+ - Answers are cached at the edge, so a repeated question is fast and costs
8
+ nobody anything
9
+ - Every row knows the page it belongs to on postfinder.io, so linking out takes
10
+ no second request
11
+ - No dependencies
12
+
13
+ ```sh
14
+ pip install postfinder
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from postfinder import PostFinder
21
+
22
+ pf = PostFinder()
23
+
24
+ for p in pf.nearby(-37.7404, 144.9633, category="post-offices", country="australia"):
25
+ print(p.name, p.address, f"{p.distance_m} m")
26
+ ```
27
+
28
+ ```
29
+ Coburg Post Office 484 Sydney Rd 420 m
30
+ Coburg North LPO 12 Elizabeth St 1800 m
31
+ ```
32
+
33
+ Nearest first, within 50km, at most 30 rows. That is the question "where do I
34
+ post this", which is a different question from "list every post box in
35
+ Victoria".
36
+
37
+ ## The six categories
38
+
39
+ ```python
40
+ from postfinder import CATEGORIES # what nearby() accepts
41
+ ```
42
+
43
+ `post-offices`, `post-boxes`, `express-post-boxes`, `parcel-lockers`,
44
+ `drop-off-points`, `collection-points`.
45
+
46
+ A name that is not one of those raises `ValueError` before a request goes out,
47
+ rather than spending a round trip to be told 400.
48
+
49
+ ## Typeahead
50
+
51
+ ```python
52
+ for hit in pf.search("coburg"):
53
+ print(hit.kind, hit.name, hit.postcode, hit.path)
54
+ ```
55
+
56
+ ```
57
+ locality Coburg 3058 /en/australia/victoria/coburg/
58
+ place Coburg Post Office 3058 /en/australia/victoria/coburg/
59
+ ```
60
+
61
+ Two characters minimum: below that the client returns an empty list without
62
+ asking, which is what the service answers anyway. Debounce by at least 150ms.
63
+ Firing on every keystroke spends bandwidth for no better answer.
64
+
65
+ ## Postcodes
66
+
67
+ A postcode is not a suburb. 3058 is Coburg, Coburg North and Merlynston, and an
68
+ address in any of them is written with the same four digits.
69
+
70
+ ```python
71
+ detail = pf.postcode("australia", "3058")
72
+ for suburb in detail.localities:
73
+ print(suburb.name, suburb.place_count, suburb.path)
74
+ ```
75
+
76
+ The whole country comes in one response, and it is meant to be kept:
77
+
78
+ ```python
79
+ index = pf.postcodes("australia") # one request
80
+ index.suburbs_in("3058") # no request, and no rescan
81
+ index.suburbs_in("2044")
82
+ ```
83
+
84
+ `suburbs_in` builds its map on first use and keeps it, so resolving a column of
85
+ ten thousand postcodes is one pass over the index rather than ten thousand walks
86
+ through every state.
87
+
88
+ ## A location, and a suburb
89
+
90
+ ```python
91
+ place = pf.place("k7m2p9x4")
92
+ print(place.place.name, place.locality.name, place.path)
93
+ print(place.reviews.summary.count, place.reviews.summary.average)
94
+
95
+ suburb = pf.locality("australia", "victoria", "coburg")
96
+ print(suburb.locality.place_count, suburb.count_of("parcel-lockers"))
97
+ for p in suburb.places:
98
+ print(p.name, suburb.path_of(p))
99
+ ```
100
+
101
+ A place's public id is permanent. It is minted once and never derived from a
102
+ source record, so a feed that renumbers its rows does not change it. Store the
103
+ id, not the name or the path.
104
+
105
+ ## Browsing
106
+
107
+ ```python
108
+ pf.countries() # every country with pages
109
+ pf.country("australia") # its states
110
+ pf.region("australia", "victoria", limit=100) # its localities, a page at a time
111
+ pf.category("australia", "parcel-lockers") # counts per state, busiest suburbs
112
+ ```
113
+
114
+ ## Errors
115
+
116
+ ```python
117
+ from postfinder import NotFound, BadRequest, RateLimited, PostFinderError
118
+
119
+ try:
120
+ pf.place(stored_id)
121
+ except NotFound:
122
+ ... # retired, or never there
123
+ ```
124
+
125
+ `NotFound` is ordinary rather than a failure: a suburb with no locations has no
126
+ page, and a location that closed is retired. Every error carries the `status`,
127
+ `title` and `detail` the service sent, because that is the part that says what to
128
+ do about it.
129
+
130
+ ## Being a good citizen
131
+
132
+ The API is free and asks for care in return: around a thousand requests a month
133
+ from one address, results kept rather than re-fetched, typing debounced. If you
134
+ need more than that, say what you are building at
135
+ [postfinder.io/en/contact/](https://postfinder.io/en/contact/).
136
+
137
+ Introduce yourself and it is easier to help you before a rate limit does:
138
+
139
+ ```python
140
+ pf = PostFinder(contact="https://example.com/about-our-bot")
141
+ ```
142
+
143
+ ## Attribution
144
+
145
+ Locations come from OpenStreetMap (ODbL), localities and postcodes from GeoNames
146
+ (CC BY 4.0). If you publish what you get back, you carry those credits with it.
147
+ The [sources page](https://postfinder.io/en/legal/) names each one.
148
+
149
+ ## Also available
150
+
151
+ | | |
152
+ |---|---|
153
+ | JavaScript and TypeScript | [`@postfinder/client`](https://www.npmjs.com/package/@postfinder/client) |
154
+ | React | [`@postfinder/react`](https://www.npmjs.com/package/@postfinder/react) |
155
+ | Vue | [`@postfinder/vue`](https://www.npmjs.com/package/@postfinder/vue) |
156
+ | Go | [`postfinder-go`](https://github.com/postfinder/postfinder-go) |
157
+
158
+ Looking for Australian street addresses rather than locations? That is
159
+ [Locio](https://locio.com.au): G-NAF address autocomplete, validation and
160
+ geocoding.
161
+
162
+ ## Licence
163
+
164
+ MIT.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "postfinder"
7
+ version = "0.1.0"
8
+ description = "Find post offices, parcel lockers and post boxes near a point, and look up postcodes. Free and keyless."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "PostFinder" }]
13
+ keywords = [
14
+ "post office locator",
15
+ "parcel locker api",
16
+ "post box locations",
17
+ "postcode api",
18
+ "postcode lookup",
19
+ "australia post locations",
20
+ "store locator api",
21
+ "nearest location api",
22
+ "openstreetmap",
23
+ "geonames",
24
+ "australia",
25
+ "new zealand",
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 4 - Beta",
29
+ "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ "Topic :: Scientific/Engineering :: GIS",
39
+ "Typing :: Typed",
40
+ ]
41
+ # No dependencies, on purpose. The whole client is one urllib call and a set of
42
+ # dataclasses; asking for requests would make this the heaviest thing in
43
+ # somebody's lambda for no gain.
44
+ dependencies = []
45
+
46
+ [project.urls]
47
+ Homepage = "https://postfinder.io"
48
+ Documentation = "https://postfinder.io/en/docs/"
49
+ Source = "https://github.com/postfinder/postfinder-python"
50
+ Changelog = "https://github.com/postfinder/postfinder-python/blob/main/CHANGELOG.md"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/postfinder"]
@@ -0,0 +1,87 @@
1
+ """PostFinder: post offices, parcel lockers and post boxes, as an API.
2
+
3
+ Free, keyless and cached at the edge. The directory behind postfinder.io.
4
+
5
+ >>> from postfinder import PostFinder
6
+ >>> pf = PostFinder()
7
+ >>> near = pf.nearby(-37.7404, 144.9633, category="post-offices",
8
+ ... country="australia")
9
+ >>> near[0].name, near[0].distance_m
10
+ ('Coburg Post Office', 420)
11
+
12
+ Data from OpenStreetMap (ODbL) and GeoNames (CC BY 4.0). Publishing what you
13
+ get back means carrying those credits: postfinder.io/en/legal/.
14
+ """
15
+
16
+ from ._client import DEFAULT_BASE_URL, MIN_QUERY, PostFinder, Transport, __version__
17
+ from ._errors import BadRequest, NotFound, PostFinderError, RateLimited
18
+ from ._models import (
19
+ CATEGORIES,
20
+ CategoryHub,
21
+ Country,
22
+ CountryDetail,
23
+ CountrySummary,
24
+ Facet,
25
+ Locality,
26
+ LocalityDetail,
27
+ LocalityLink,
28
+ LocalitySummary,
29
+ NearbyPlace,
30
+ Photo,
31
+ Place,
32
+ PlaceDetail,
33
+ PlaceLink,
34
+ PostcodeDetail,
35
+ PostcodeEntry,
36
+ PostcodeIndex,
37
+ Region,
38
+ RegionDetail,
39
+ RegionPostcodes,
40
+ RegionSummary,
41
+ Review,
42
+ Reviews,
43
+ ReviewSummary,
44
+ SearchHit,
45
+ locality_path,
46
+ place_path,
47
+ )
48
+
49
+ __all__ = [
50
+ "CATEGORIES",
51
+ "DEFAULT_BASE_URL",
52
+ "MIN_QUERY",
53
+ "BadRequest",
54
+ "CategoryHub",
55
+ "Country",
56
+ "CountryDetail",
57
+ "CountrySummary",
58
+ "Facet",
59
+ "Locality",
60
+ "LocalityDetail",
61
+ "LocalityLink",
62
+ "LocalitySummary",
63
+ "NearbyPlace",
64
+ "NotFound",
65
+ "Photo",
66
+ "Place",
67
+ "PlaceDetail",
68
+ "PlaceLink",
69
+ "PostFinder",
70
+ "PostFinderError",
71
+ "PostcodeDetail",
72
+ "PostcodeEntry",
73
+ "PostcodeIndex",
74
+ "RateLimited",
75
+ "Region",
76
+ "RegionDetail",
77
+ "RegionPostcodes",
78
+ "RegionSummary",
79
+ "Review",
80
+ "ReviewSummary",
81
+ "Reviews",
82
+ "SearchHit",
83
+ "Transport",
84
+ "__version__",
85
+ "locality_path",
86
+ "place_path",
87
+ ]