Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.

Commit 5b3cd0d

Browse files
authored
Merge pull request #2 from iMicknl/refactor_and_add_endpoints
Expose more endpoints and refactor
2 parents 7098dbb + e0f6419 commit 5b3cd0d

3 files changed

Lines changed: 124 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## 0.2 - 2018-01-24
8+
### Added
9+
- Basic test function
10+
- Add function to retrieve letter status
11+
- Add function to retrieve single letter
12+
- Add function to retrieve single shipment
13+
14+
### Fixed
15+
- Fixed the docstring notation
16+
- Better exception handling & less code duplication
17+
718
## 0.1 - 2017-10-21
819
### Added
920
- Initial (private) release

postnl_api/postnl_api.py

Lines changed: 111 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
"""
2-
Python wrapper for the PostNL API
3-
"""
1+
""" Python wrapper for the PostNL API """
42

53
import logging
6-
from datetime import datetime
4+
from datetime import datetime, timedelta
75
import requests
86

9-
_LOGGER = logging.getLogger(__name__)
10-
117
BASE_URL = 'https://jouw.postnl.nl'
128

139
AUTHENTICATE_URL = BASE_URL + '/mobile/token'
1410
SHIPMENTS_URL = BASE_URL + '/mobile/api/shipments'
1511
PROFILE_URL = BASE_URL + '/mobile/api/profile'
1612
LETTERS_URL = BASE_URL + '/mobile/api/letters'
13+
VALIDATE_LETTERS_URL = BASE_URL + '/mobile/api/letters/validation'
1714

18-
class PostNL_API(object):
15+
DEFAULT_HEADER = {
16+
'api-version': '4.7',
17+
'user-agent': 'PostNL/1 CFNetwork/889.3 Darwin/17.2.0',
18+
}
1919

20-
"""
21-
Interface class for the PostNL API
22-
"""
20+
class PostNL_API(object):
21+
""" Interface class for the PostNL API """
2322

2423
def __init__(self, user, password):
24+
""" Constructor """
2525

2626
self._user = user
2727
self._password = password
@@ -33,58 +33,47 @@ def __init__(self, user, password):
3333
'password': self._password
3434
}
3535

36-
headers = {
37-
'api-version': '4.6',
38-
'user-agent': 'PostNL/1 CFNetwork/889.3 Darwin/17.2.0',
39-
'content-type': "application/x-www-form-urlencoded",
40-
}
36+
try:
37+
response = requests.request(
38+
'POST', AUTHENTICATE_URL, data=payload, headers=DEFAULT_HEADER)
39+
data = response.json()
4140

42-
response = requests.request(
43-
'POST', AUTHENTICATE_URL, data=payload, headers=headers)
41+
except Exception:
42+
raise(Exception)
4443

45-
data = response.json()
44+
if 'error' in data:
45+
raise Exception(data['error'])
4646

4747
self._access_token = data['access_token']
4848
self._refresh_token = data['refresh_token']
49-
self._token_expires_in = data['expires_in'] # TODO Add logic to refresh on invalidate
49+
self._token_expires_in = data['expires_in']
50+
self._token_expires_at = datetime.now() + timedelta(0, data['expires_in'])
5051

51-
"""
52-
Refresh access_token
53-
"""
5452
def refresh_token(self):
53+
""" Refresh access_token """
5554

5655
payload = {
5756
'grant_type': 'refresh_token',
5857
'client_id': 'pwIOSApp',
5958
'refresh_token': self._refresh_token
6059
}
6160

62-
headers = {
63-
'api-version': '4.6',
64-
'user-agent': 'PostNL/1 CFNetwork/889.3 Darwin/17.2.0',
65-
'content-type': "application/x-www-form-urlencoded",
66-
}
67-
6861
response = requests.request(
69-
'POST', AUTHENTICATE_URL, data=payload, headers=headers)
62+
'POST', AUTHENTICATE_URL, data=payload, headers=DEFAULT_HEADER)
7063

7164
data = response.json()
7265

7366
self._access_token = data['access_token']
7467

75-
"""
76-
Retrieve shipments
77-
"""
7868
def get_shipments(self):
69+
""" Retrieve shipments """
7970

8071
headers = {
81-
'api-version': '4.6',
82-
'user-agent': 'PostNL/1 CFNetwork/889.3 Darwin/17.2.0',
8372
'authorization': 'Bearer ' + self._access_token
8473
}
8574

8675
response = requests.request(
87-
'GET', SHIPMENTS_URL, headers=headers)
76+
'GET', SHIPMENTS_URL, headers={**headers, **DEFAULT_HEADER})
8877

8978
if response.status_code == 401:
9079
self.refresh_token()
@@ -94,19 +83,33 @@ def get_shipments(self):
9483

9584
return shipments
9685

97-
"""
98-
Retrieve profile
99-
"""
86+
def get_shipment(self, shipment_id):
87+
""" Retrieve single shipment by id """
88+
89+
headers = {
90+
'authorization': 'Bearer ' + self._access_token
91+
}
92+
93+
response = requests.request(
94+
'GET', SHIPMENTS_URL + '/' + shipment_id, headers={**headers, **DEFAULT_HEADER})
95+
96+
if response.status_code == 401:
97+
self.refresh_token()
98+
shipments = self.get_shipment(shipment_id)
99+
else:
100+
shipments = response.json()
101+
102+
return shipments
103+
100104
def get_profile(self):
105+
""" Retrieve profile """
101106

102107
headers = {
103-
'api-version': '4.6',
104-
'user-agent': 'PostNL/1 CFNetwork/889.3 Darwin/17.2.0',
105108
'authorization': 'Bearer ' + self._access_token
106109
}
107110

108111
response = requests.request(
109-
'GET', PROFILE_URL, headers=headers)
112+
'GET', PROFILE_URL, headers={**headers, **DEFAULT_HEADER})
110113

111114
if response.status_code == 401:
112115
self.refresh_token()
@@ -116,36 +119,69 @@ def get_profile(self):
116119

117120
return profile
118121

119-
"""
120-
Retrieve letters
121-
"""
122-
def get_letters(self):
122+
def validate_letters(self):
123+
""" Retrieve letter validation status """
123124

124125
headers = {
125-
'api-version': '4.6',
126-
'user-agent': 'PostNL/1 CFNetwork/889.3 Darwin/17.2.0',
127126
'authorization': 'Bearer ' + self._access_token
128127
}
129128

130129
response = requests.request(
131-
'GET', LETTERS_URL, headers=headers)
130+
'GET', VALIDATE_LETTERS_URL, headers={**headers, **DEFAULT_HEADER})
132131

132+
if response.status_code == 401:
133+
self.refresh_token()
134+
validation = self.validate_letters()
135+
else:
136+
validation = response.json()
137+
138+
return validation
139+
140+
def get_letters(self):
141+
""" Retrieve letters """
142+
143+
headers = {
144+
'authorization': 'Bearer ' + self._access_token
145+
}
146+
147+
response = requests.request(
148+
'GET', LETTERS_URL, headers={**headers, **DEFAULT_HEADER})
149+
133150
if response.status_code == 401:
134151
self.refresh_token()
135152
letters = self.get_letters()
136153
else:
137154
letters = response.json()
138155

139-
if letters['type'] == 'ProfileValidationFeatureMissing':
140-
_LOGGER.error(letters['message'])
141-
return []
156+
# TODO Add validation / exception handling
157+
# if letters['type'] == 'ProfileValidationFeatureMissing':
158+
# _LOGGER.error(letters['message'])
159+
# return []
142160

143161
return letters
144162

145-
"""
146-
Retrieve relevant shipments
147-
"""
163+
def get_letter(self, letter_id):
164+
""" Retrieve single letter by id """
165+
166+
headers = {
167+
'authorization': 'Bearer ' + self._access_token
168+
}
169+
170+
response = requests.request(
171+
'GET', LETTERS_URL + '/' + letter_id, headers={**headers, **DEFAULT_HEADER})
172+
173+
if response.status_code == 200:
174+
letter = response.json()
175+
elif response.status_code == 401:
176+
self.refresh_token()
177+
letter = self.get_letter(letter_id)
178+
else:
179+
raise Exception('Unknown Error')
180+
181+
return letter
182+
148183
def get_relevant_shipments(self):
184+
""" Retrieve not delivered shipments and shipments delivered today """
149185

150186
shipments = self.get_shipments()
151187
relevant_shipments = []
@@ -159,10 +195,28 @@ def get_relevant_shipments(self):
159195

160196
# Check if package has been delivered today
161197
if shipment['status']['delivery']:
162-
delivery_date = datetime.strptime( shipment['status']['delivery']['deliveryDate'][:19], "%Y-%m-%dT%H:%M:%S" )
198+
delivery_date = datetime.strptime(
199+
shipment['status']['delivery']['deliveryDate'][:19], "%Y-%m-%dT%H:%M:%S")
163200

164201
if delivery_date.date() == datetime.today().date():
165202
relevant_shipments.append(shipment)
166-
continue
167203

168-
return relevant_shipments
204+
return relevant_shipments
205+
206+
def get_relevant_letters(self):
207+
""" Retrieve letters with a future delivery date """
208+
209+
letters = self.get_letters()
210+
relevant_letters = []
211+
212+
for letter in letters:
213+
214+
# Check if letter is scheduled for delivery in the future
215+
if letter['expectedDeliveryDate']:
216+
expected_delivery_date = datetime.strptime(
217+
letter['expectedDeliveryDate'][:19], "%Y-%m-%dT%H:%M:%S")
218+
219+
if expected_delivery_date.date() == datetime.today().date():
220+
relevant_letters.append(letter)
221+
222+
return relevant_letters

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from setuptools import setup, find_packages
22

33
setup(name='postnl_api',
4-
version='0.1',
4+
version='0.2',
55
description='Python wrapper for the PostNL API, a way to track packages using their online portal',
6-
url='http://github.com/imicknl/python-postnl-api',
6+
url='https://github.com/imicknl/python-postnl-api',
77
author='Mick Vleeshouwer',
88
author_email='mick@imick.nl',
99
license='MIT',

0 commit comments

Comments
 (0)