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.
- postfinder-0.1.0/.github/workflows/ci.yml +21 -0
- postfinder-0.1.0/.github/workflows/release.yml +25 -0
- postfinder-0.1.0/.gitignore +6 -0
- postfinder-0.1.0/CHANGELOG.md +13 -0
- postfinder-0.1.0/LICENSE +21 -0
- postfinder-0.1.0/PKG-INFO +211 -0
- postfinder-0.1.0/README.md +164 -0
- postfinder-0.1.0/pyproject.toml +53 -0
- postfinder-0.1.0/src/postfinder/__init__.py +87 -0
- postfinder-0.1.0/src/postfinder/_client.py +374 -0
- postfinder-0.1.0/src/postfinder/_errors.py +63 -0
- postfinder-0.1.0/src/postfinder/_models.py +681 -0
- postfinder-0.1.0/src/postfinder/py.typed +0 -0
- postfinder-0.1.0/tests/test_client.py +539 -0
- postfinder-0.1.0/tests/test_security.py +171 -0
|
@@ -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,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.
|
postfinder-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
]
|