Skip to content

Commit 40560a3

Browse files
committed
Added default private key to constructor for ease of use + added extra headers to invoicing
1 parent c2a1d12 commit 40560a3

10 files changed

Lines changed: 89 additions & 71 deletions

File tree

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ and configure your API key which you can find in the [Twikey merchant interface]
5151
```python
5252
import twikey
5353

54+
APIKEY = 'apikey_as_found_in_twikey'
5455
twikeyClient = twikey.TwikeyClient(APIKEY, "https://api.beta.twikey.com")
5556
```
5657

@@ -60,7 +61,11 @@ Invite a customer to sign a SEPA mandate using a specific behaviour template (ct
6061
the behaviour or flow that the customer will experience. This 'ct' can be found in the template section of the settings.
6162

6263
```python
63-
invite = twikeyClient.document.create({
64+
import twikey
65+
66+
ct = 123 # See settings/profile in twikey
67+
68+
invite = twikey.TwikeyClient.document.create({
6469
"ct": ct,
6570
"email": "info@twikey.com",
6671
"firstname": "Info",
@@ -81,24 +86,26 @@ think of as reading out a queue. Since it'll return you the changes since the la
8186
import twikey
8287

8388
class MyDocumentFeed(twikey.DocumentFeed):
84-
def newDocument(self, doc):
89+
def newDocument(self, doc, evt_time):
8590
print("new ", doc["MndtId"])
8691

87-
def updatedDocument(self, orinalDocNumber, doc, reason):
92+
def updatedDocument(self, original_mandate_number, doc, reason, evt_time):
8893
print("update ", doc["MndtId"], "b/c", reason["Rsn"])
8994

90-
def cancelDocument(self, docNumber, reason):
91-
print("cancelled ", docNumber, "b/c", reason["Rsn"])
95+
def cancelDocument(self, doc_number, reason, evt_time):
96+
print("cancelled ", doc_number, "b/c", reason["Rsn"])
9297

93-
twikeyClient.document.feed(MyDocumentFeed())
98+
twikey.TwikeyClient.document.feed(MyDocumentFeed())
9499
```
95100

96101
## Transactions
97102

98103
Send new transactions and act upon feedback from the bank.
99104

100105
```python
101-
tx = twikeyClient.transaction.create({
106+
import twikey
107+
108+
tx = twikey.TwikeyClient.transaction.create({
102109
"mndtId" : "CORERECURRENTNL16318",
103110
"message" : "Test Message",
104111
"ref" : "Merchant Reference",
@@ -116,7 +123,7 @@ class MyFeed(twikey.TransactionFeed):
116123
def transaction(self, transaction):
117124
print("TX ", transaction.ref, transaction.state)
118125

119-
twikeyClient.transaction.feed(MyFeed())
126+
twikey.TwikeyClient.transaction.feed(MyFeed())
120127
```
121128

122129
## Webhook ##
@@ -125,13 +132,15 @@ When wants to inform you about new updates about documents or payments a `webhoo
125132

126133
```python
127134
import Flask
128-
import twikey
135+
import urllib
136+
import twikey
129137

138+
APIKEY = 'apikey_as_found_in_twikey'
130139
app = Flask(__name__)
131140

132141
@app.route('/webhook', methods=['GET'])
133142
def webhook(request):
134-
payload = unquote(request.query_string)
143+
payload = urllib.parse.unquote(request.query_string)
135144
received_sign = request.headers.get('X-Signature')
136145
if not received_sign:
137146
return False

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
urllib3~=1.26.5
2+
requests~=2.25.1

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def get_long_description():
1010

1111
setup(
1212
name="twikey-api-python",
13-
version="v0.1.4",
13+
version="v0.1.7",
1414
description="Python interface with the Twikey api",
1515
author="Twikey",
1616
author_email="support@twikey.com",

tests/test_document.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,16 @@ def test_feed(self):
4343

4444

4545
class MyDocumentFeed(twikey.DocumentFeed):
46-
def newDocument(self, doc, evtTime):
47-
print("Document created ", doc["MndtId"], "@", evtTime)
46+
def newDocument(self, doc, evt_time):
47+
print("Document created ", doc["MndtId"], "@", evt_time)
4848

49-
def updatedDocument(self, original_number, doc, reason, evtTime):
49+
def updatedDocument(self, original_number, doc, reason, evt_time):
5050
print(
51-
"Document updated ", original_number, "b/c", reason["Rsn"], "@", evtTime
51+
"Document updated ", original_number, "b/c", reason["Rsn"], "@", evt_time
5252
)
5353

54-
def cancelDocument(self, number, reason, evtTime):
55-
print("Document cancelled ", number, "b/c", reason["Rsn"], "@", evtTime)
54+
def cancelDocument(self, number, reason, evt_time):
55+
print("Document cancelled ", number, "b/c", reason["Rsn"], "@", evt_time)
5656

5757

5858
if __name__ == "__main__":

tox.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ deps =
2626
pytest-cov >= 2.8.1, < 2.11.0
2727
pytest-mock >= 2.0.0
2828
pytest-xdist >= 1.31.0
29+
requests ~= 2.25.1
2930
commands = pytest --cov {posargs:-n auto}
3031
# compilation flags can be useful when prebuilt wheels cannot be used, e.g.
3132
# PyPy 2 needs to compile the `cryptography` module. On macOS this can be done
@@ -37,6 +38,6 @@ passenv = LDFLAGS CFLAGS
3738
[testenv:fmt]
3839
description = run code formatting using black
3940
basepython = python3.9
40-
deps = black==20.8b1
41+
deps = black==22.6.0
4142
commands = black . {posargs}
4243
skip_install = true

twikey/client.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import binascii
2-
import hmac
3-
import struct
4-
import time
5-
import datetime
6-
71
import requests
82

93
from .document import Document
104
from .transaction import Transaction
115
from .paylink import Paylink
126
from .invoice import Invoice
137
import logging
8+
import datetime
149

1510

1611
class TwikeyClient(object):
@@ -19,7 +14,7 @@ class TwikeyClient(object):
1914
api_token = None # Once authenticated
2015
merchant_id = 0 # Once authenticated
2116
private_key = None
22-
vendorPrefix = "own"
17+
vendorPrefix = b"own"
2318
api_base = "https://api.twikey.com"
2419

2520
document = None
@@ -32,9 +27,11 @@ def __init__(
3227
api_key,
3328
base_url="https://api.twikey.com",
3429
user_agent="twikey-python/v0.1.0",
30+
private_key=None,
3531
) -> None:
3632
self.user_agent = user_agent
3733
self.api_key = api_key
34+
self.private_key = private_key
3835
self.api_base = base_url
3936
self.merchant_id = 0
4037
self.document = Document(self)
@@ -48,14 +45,17 @@ def instance_url(self, url=""):
4845

4946
def get_totp(self, vendorPrefix, secret):
5047
"""Return the Time-Based One-Time Password for the current time, and the provided secret (base32 encoded)"""
51-
secret = bytearray(vendorPrefix) + binascii.unhexlify(secret)
52-
counter = struct.pack(">Q", int(time.time()) // 30)
53-
5448
import hashlib
49+
import struct
50+
import binascii
51+
import hmac
52+
import time
5553

56-
hash = hmac.new(secret, counter, hashlib.sha256).digest()
57-
offset = ord(hash[19]) & 0xF
54+
secret = vendorPrefix + binascii.unhexlify(secret)
55+
counter = struct.pack(">Q", int(time.time()) // 30)
5856

57+
hash = hmac.new(secret, counter, hashlib.sha256).digest()
58+
offset = hash[19] & 0xF
5959
return (
6060
struct.unpack(">I", hash[offset : offset + 4])[0] & 0x7FFFFFFF
6161
) % 100000000

twikey/document.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ def create(self, data):
1313
data = data or {}
1414
self.client.refreshTokenIfRequired()
1515
response = requests.post(url=url, data=data, headers=self.client.headers())
16-
jsonResponse = response.json()
16+
json_response = response.json()
1717
if "ApiErrorCode" in response.headers:
18-
error = jsonResponse
18+
error = json_response
1919
raise Exception("Error invite : %s" % error)
20-
self.logger.debug("Added new mandate : %s" % jsonResponse["mndtId"])
21-
return jsonResponse
20+
self.logger.debug("Added new mandate : %s" % json_response["mndtId"])
21+
return json_response
2222

2323
def update(self, data):
2424
url = self.client.instance_url("/mandate/update")
@@ -45,7 +45,7 @@ def cancel(self, mandate_number, reason):
4545
error = response.json()
4646
raise Exception("Error invite : %s" % error)
4747

48-
def feed(self, documentFeed):
48+
def feed(self, document_feed):
4949
url = self.client.instance_url("/mandate")
5050

5151
self.client.refreshTokenIfRequired()
@@ -54,39 +54,39 @@ def feed(self, documentFeed):
5454
if "ApiErrorCode" in response.headers:
5555
error = response.json()
5656
raise Exception("Error feed : %s" % error)
57-
feedResponse = response.json()
58-
while len(feedResponse["Messages"]) > 0:
59-
self.logger.debug("Feed handling : %d" % (len(feedResponse["Messages"])))
60-
for msg in feedResponse["Messages"]:
57+
feed_response = response.json()
58+
while len(feed_response["Messages"]) > 0:
59+
self.logger.debug("Feed handling : %d" % (len(feed_response["Messages"])))
60+
for msg in feed_response["Messages"]:
6161
if "AmdmntRsn" in msg:
6262
mndt_id_ = msg["OrgnlMndtId"]
63-
self.logger.debug("Feed update : %s" % (mndt_id_))
63+
self.logger.debug("Feed update : %s" % mndt_id_)
6464
mndt_ = msg["Mndt"]
6565
rsn_ = msg["AmdmntRsn"]
66-
documentFeed.updatedDocument(mndt_id_, mndt_, rsn_, msg["EvtTime"])
66+
document_feed.updatedDocument(mndt_id_, mndt_, rsn_, msg["EvtTime"])
6767
elif "CxlRsn" in msg:
6868
self.logger.debug("Feed cancel : %s" % (msg["OrgnlMndtId"]))
69-
documentFeed.cancelDocument(
69+
document_feed.cancelDocument(
7070
msg["OrgnlMndtId"], msg["CxlRsn"], msg["EvtTime"]
7171
)
7272
else:
7373
self.logger.debug("Feed create : %s" % (msg["Mndt"]))
74-
documentFeed.newDocument(msg["Mndt"], msg["EvtTime"])
74+
document_feed.newDocument(msg["Mndt"], msg["EvtTime"])
7575
response = requests.get(url=url, headers=self.client.headers())
7676
if "ApiErrorCode" in response.headers:
7777
error = response.json()
7878
raise Exception(
7979
"Error invite : %s - %s" % (error["code"], error["message"])
8080
)
81-
feedResponse = response.json()
81+
feed_response = response.json()
8282

8383

8484
class DocumentFeed:
85-
def newDocument(self, doc, evtTime):
85+
def newDocument(self, doc, evt_time):
8686
pass
8787

88-
def updatedDocument(self, original_mandate_number, doc, reason, evtTime):
88+
def updatedDocument(self, original_mandate_number, doc, reason, evt_time):
8989
pass
9090

91-
def cancelDocument(self, docNumber, reason, evtTime):
91+
def cancelDocument(self, doc_number, reason, evt_time):
9292
pass

twikey/invoice.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,26 @@ def __init__(self, client) -> None:
88
self.client = client
99
self.logger = logging.getLogger(__name__)
1010

11-
def create(self, data):
11+
def create(self, data, origin=False, purpose=False, manual=False):
1212
url = self.client.instance_url("/invoice")
1313
data = data or {}
1414
self.client.refreshTokenIfRequired()
1515
headers = self.client.headers("application/json")
16+
if origin:
17+
headers["X-PARTNER"] = origin
18+
if purpose:
19+
headers["X-Purpose"] = purpose
20+
if manual:
21+
headers["X-MANUAL"] = "true"
1622
response = requests.post(url=url, json=data, headers=headers)
17-
jsonResponse = response.json()
23+
json_response = response.json()
1824
if "ApiErrorCode" in response.headers:
19-
error = jsonResponse
25+
error = json_response
2026
raise Exception("Error creating : %s" % error)
21-
self.logger.debug("Added invoice : %s" % jsonResponse["url"])
22-
return jsonResponse
27+
self.logger.debug("Added invoice : %s" % json_response["url"])
28+
return json_response
2329

24-
def feed(self, invoiceFeed, *includes):
30+
def feed(self, invoice_feed, *includes):
2531
_includes = ""
2632
for include in includes:
2733
_includes += "&include=" + include
@@ -36,17 +42,17 @@ def feed(self, invoiceFeed, *includes):
3642
"Error feed : %s - %s"
3743
% (response.headers["ApiErrorCode"], response.headers["ApiError"])
3844
)
39-
feedResponse = response.json()
40-
while len(feedResponse["Invoices"]) > 0:
41-
self.logger.debug("Feed handling : %d" % (len(feedResponse["Invoices"])))
42-
for invoice in feedResponse["Invoices"]:
45+
feed_response = response.json()
46+
while len(feed_response["Invoices"]) > 0:
47+
self.logger.debug("Feed handling : %d" % (len(feed_response["Invoices"])))
48+
for invoice in feed_response["Invoices"]:
4349
self.logger.debug("Feed handling : %s" % invoice)
44-
invoiceFeed.invoice(invoice)
50+
invoice_feed.invoice(invoice)
4551
response = requests.get(url=url, headers=self.client.headers())
4652
if "ApiErrorCode" in response.headers:
4753
error = response.json()
4854
raise Exception("Error feed : %s" % error)
49-
feedResponse = response.json()
55+
feed_response = response.json()
5056

5157
def geturl(self, invoice_id):
5258
return "%s/%s/%s" % (

twikey/paylink.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def create(self, data):
1616
raise Exception("Error creating paylink : %s" % error)
1717
return response.json()
1818

19-
def feed(self, paylinkFeed):
19+
def feed(self, paylink_feed):
2020
url = self.client.instance_url("/payment/link/feed")
2121

2222
self.client.refreshTokenIfRequired()
@@ -25,15 +25,15 @@ def feed(self, paylinkFeed):
2525
if "ApiErrorCode" in response.headers:
2626
error = response.json()
2727
raise Exception("Error feed : %s" % error)
28-
feedResponse = response.json()
29-
while len(feedResponse["Links"]) > 0:
30-
for msg in feedResponse["Links"]:
31-
paylinkFeed.paylink(msg)
28+
feed_response = response.json()
29+
while len(feed_response["Links"]) > 0:
30+
for msg in feed_response["Links"]:
31+
paylink_feed.paylink(msg)
3232
response = requests.get(url=url, headers=self.client.headers())
3333
if "ApiErrorCode" in response.headers:
3434
error = response.json()
3535
raise Exception("Error feed : %s" % error)
36-
feedResponse = response.json()
36+
feed_response = response.json()
3737

3838

3939
class PaylinkFeed:

twikey/transaction.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def create(self, data):
1717
raise Exception("Error sending transaction : %s" % error)
1818
return response.json()["Entries"][0]
1919

20-
def feed(self, transactionFeed):
20+
def feed(self, transaction_feed):
2121
url = self.client.instance_url("/transaction")
2222

2323
self.client.refreshTokenIfRequired()
@@ -26,15 +26,15 @@ def feed(self, transactionFeed):
2626
if "ApiErrorCode" in response.headers:
2727
error = response.json()
2828
raise Exception("Error feed : %s" % error)
29-
feedResponse = response.json()
30-
while len(feedResponse["Entries"]) > 0:
31-
for msg in feedResponse["Entries"]:
32-
transactionFeed.transaction(msg)
29+
feed_response = response.json()
30+
while len(feed_response["Entries"]) > 0:
31+
for msg in feed_response["Entries"]:
32+
transaction_feed.transaction(msg)
3333
response = requests.get(url=url, headers=self.client.headers())
3434
if "ApiErrorCode" in response.headers:
3535
error = response.json()
3636
raise Exception("Error creating : %s" % error)
37-
feedResponse = response.json()
37+
feed_response = response.json()
3838

3939

4040
class TransactionFeed:

0 commit comments

Comments
 (0)