Skip to content

Commit

Permalink
Merge pull request #3 from dogmatic69/feature/oauth
Browse files Browse the repository at this point in the history
feat: oAuth support
  • Loading branch information
dogmatic69 authored Jan 6, 2022
2 parents c78becd + 3f4c3f0 commit 19a95cd
Show file tree
Hide file tree
Showing 17 changed files with 385 additions and 96 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.python
*egg*
client.py
examples/client.py
**/__pycache__
.coverage
htmlcov
Expand Down
14 changes: 9 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: build
build:
build: venv
python setup.py sdist

.PHONY: isort
Expand All @@ -16,13 +16,13 @@ flake8:

.PHONY: test
test:
pytest
pytest --ignore=examples/

.PHONY: ci
ci: isort black flake8 test
ci: venv isort black flake8 test

.PHONY: ci-fix
ci-fix:
ci-fix: venv
isort ./nordigen ./tests ./examples
black ./nordigen ./tests ./examples

Expand All @@ -32,7 +32,7 @@ dev:
$(MAKE) ci

.PHONY: install-pip
install-pip:
install-pip: venv
python -m pip install --upgrade pip

.PHONY: install-dev
Expand All @@ -47,3 +47,7 @@ install-deploy: install-pip
deploy: build
twine upload --verbose dist/*

.PHONY: venv
venv:
$(shell [ ! -d .python ] && python -m venv .python)
. .python/bin/activate
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Nordigen API Client

[![GitHub](https://img.shields.io/github/license/dogmatic69/nordigen-python)](LICENSE)
[![CodeFactor](https://www.codefactor.io/repository/github/dogmatic69/nordigen-python/badge)](https://www.codefactor.io/repository/github/dogmatic69/nordigen-python)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dogmatic69_nordigen-python&metric=alert_status)](https://sonarcloud.io/dashboard?id=dogmatic69_nordigen-python)
[![CI](https://github.com/dogmatic69/nordigen-python/actions/workflows/master.yaml/badge.svg)](https://github.com/dogmatic69/nordigen-python/actions/workflows/master.yaml)

Nordigen is a (always*) free banking API that takes advantage of the EU PSD2
regulations. They connect to banks in over 30 countries using real banking
API's (no screen scraping).
Expand Down Expand Up @@ -28,7 +33,7 @@ pip install nordigen-python

## Usage

Some more indepth working examples can be found in `./examples`. Also check out the test cases for usage examples.
Some more in-depth working examples can be found in `./examples`. Also check out the test cases for usage examples.

Create a client instance

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.2
0.2.0b1
34 changes: 28 additions & 6 deletions nordigen/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
from nordigen.client import AccountClient, AgreementsClient, AspspsClient, RequisitionsClient
import warnings

from apiclient import HeaderAuthentication, NoAuthentication

from nordigen.client import AccountClient, AgreementsClient, AspspsClient, AuthClient, RequisitionsClient
from nordigen.oauth import OAuthAuthentication


def Client(token=None, request_strategy=None, secret_id=None, secret_key=None, version=None):
if token:
warnings.warn("Use Client(secret_id=xxx, secret_key=xxx) instead of token", DeprecationWarning)

if not token and (not secret_id or not secret_key):
raise ValueError("secret_id and secret_key must be provided")

def Client(token, request_strategy=None):
def instance():
return instance

instance.aspsps = AspspsClient(token=token, request_strategy=request_strategy)
instance.agreements = AgreementsClient(token=token, request_strategy=request_strategy)
instance.account = AccountClient(token=token, request_strategy=request_strategy)
instance.requisitions = RequisitionsClient(token=token, request_strategy=request_strategy)
auth = HeaderAuthentication(scheme="Token", token=token)
if not token:
version = "v2" if not version else version
auth = OAuthAuthentication(
body={
"secret_id": secret_id,
"secret_key": secret_key,
},
client=AuthClient(auth=NoAuthentication(), request_strategy=request_strategy, version=version),
)

instance.aspsps = AspspsClient(auth=auth, request_strategy=request_strategy, version=version)
instance.agreements = AgreementsClient(auth=auth, request_strategy=request_strategy, version=version)
instance.account = AccountClient(auth=auth, request_strategy=request_strategy, version=version)
instance.requisitions = RequisitionsClient(auth=auth, request_strategy=request_strategy, version=version)

return instance
102 changes: 65 additions & 37 deletions nordigen/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import urllib

from apiclient import APIClient, HeaderAuthentication, JsonResponseHandler
from apiclient import APIClient, JsonResponseHandler


def next_page_by_url(response, previous_page_url):
Expand All @@ -19,7 +19,7 @@ def next_page_by_url(response, previous_page_url):


class NordigenClient(APIClient):
def __init__(self, token, scheme='https', host='ob.nordigen.com', base='/api', request_strategy=None):
def __init__(self, auth, scheme="https", host="ob.nordigen.com", base="/api", request_strategy=None, version="v2"):
"""Nordigen client base class.
Args:
Expand All @@ -32,10 +32,11 @@ def __init__(self, token, scheme='https', host='ob.nordigen.com', base='/api', r
self.request_strategy = request_strategy
self.scheme = scheme
self.host = host
self.base = base
version = f"/{version}" if version else ""
self.base = f"{base}{version}"

super(NordigenClient, self).__init__(
authentication_method=HeaderAuthentication(scheme='Token', token=token),
authentication_method=auth,
response_handler=JsonResponseHandler,
request_strategy=request_strategy,
)
Expand All @@ -50,73 +51,97 @@ def url(self, fragment, url_args={}):
Returns:
str
"""
url_args = ('?' + urllib.parse.urlencode(url_args)) if url_args else ''
return f'{self.scheme}://{self.host}{self.base}/{fragment}/{url_args}'
url_args = ("?" + urllib.parse.urlencode(url_args)) if url_args else ""
return f"{self.scheme}://{self.host}{self.base}/{fragment}/{url_args}"


class AuthClient(NordigenClient):
def token(self, secret_id, secret_key):
url = self.url(fragment="token/new")
return self.post(
url,
data={
"secret_id": secret_id,
"secret_key": secret_key,
},
)

def refresh(self, refresh_token):
url = self.url(fragment="token/refresh")
return self.post(
url,
data={
"refresh_token": refresh_token,
},
)


class AccountClient(NordigenClient):
def info(self, id):
url = self.url(fragment=f'accounts/{id}')
url = self.url(fragment=f"accounts/{id}")
return self.get(url)

def balances(self, id):
url = self.url(fragment=f'accounts/{id}/balances')
url = self.url(fragment=f"accounts/{id}/balances")
return self.get(url)

def details(self, id):
url = self.url(fragment=f'accounts/{id}/details')
url = self.url(fragment=f"accounts/{id}/details")
return self.get(url)

def transactions(self, id):
url = self.url(fragment=f'accounts/{id}/transactions')
url = self.url(fragment=f"accounts/{id}/transactions")
return self.get(url)


class AgreementsClient(NordigenClient):
def create(self, enduser_id, aspsp_id, historical_days=30):
url = self.url(fragment='agreements/enduser')
return self.post(url, data={
"max_historical_days": historical_days,
"enduser_id": enduser_id,
"aspsp_id": aspsp_id,
})
url = self.url(fragment="agreements/enduser")
return self.post(
url,
data={
"max_historical_days": historical_days,
"enduser_id": enduser_id,
"aspsp_id": aspsp_id,
},
)

def by_enduser_id(self, enduser_id, limit=None, offset=None):
url_args = dict(enduser_id=enduser_id, limit=limit, offset=offset)
url_args = {k: v for k, v in url_args.items() if v}

url = self.url(fragment='agreements/enduser', url_args=url_args)
url = self.url(fragment="agreements/enduser", url_args=url_args)
return self.get(url)

def by_id(self, id):
url = self.url(fragment=f'agreements/enduser/{id}')
url = self.url(fragment=f"agreements/enduser/{id}")
return self.get(url)

def remove(self, id):
url = self.url(fragment=f'agreements/enduser/{id}')
url = self.url(fragment=f"agreements/enduser/{id}")
return self.delete(url)

def accept(self, id):
url = self.url(fragment=f'agreements/enduser/{id}/accept')
return self.put(url, {
"user_agent": "user-agent",
"ip_address": "127.0.0.1"
})
url = self.url(fragment=f"agreements/enduser/{id}/accept")
return self.put(url, {"user_agent": "user-agent", "ip_address": "127.0.0.1"})

def text(self, id):
url = self.url(fragment=f'agreements/enduser/{id}/text')
url = self.url(fragment=f"agreements/enduser/{id}/text")
return self.get(url)


class AspspsClient(NordigenClient):
def by_country(self, country):
url = self.url(fragment='aspsps', url_args={
'country': country,
})
url = self.url(
fragment="aspsps",
url_args={
"country": country,
},
)
return self.get(url)

def by_id(self, id):
url = self.url(fragment=f'aspsps/{id}')
url = self.url(fragment=f"aspsps/{id}")
return self.get(url)


Expand All @@ -125,19 +150,19 @@ def list(self, limit=None, offset=None):
url_args = dict(limit=limit, offset=offset)
url_args = {k: v for k, v in url_args.items() if v}

url = self.url(fragment='requisitions', url_args=url_args)
url = self.url(fragment="requisitions", url_args=url_args)
return self.get(url)

def by_id(self, id):
url = self.url(fragment=f'requisitions/{id}')
url = self.url(fragment=f"requisitions/{id}")
return self.get(url)

def remove(self, id):
url = self.url(fragment=f'requisitions/{id}')
url = self.url(fragment=f"requisitions/{id}")
return self.delete(url)

def create(self, redirect, enduser_id, reference, agreements=[], language=None):
url = self.url(fragment='requisitions')
url = self.url(fragment="requisitions")
data = {
"redirect": redirect,
"agreements": agreements,
Expand All @@ -150,7 +175,10 @@ def create(self, redirect, enduser_id, reference, agreements=[], language=None):
return self.post(url, data=data)

def initiate(self, id, aspsp_id):
url = self.url(fragment=f'requisitions/{id}/links')
return self.post(url, {
"aspsp_id": aspsp_id,
})
url = self.url(fragment=f"requisitions/{id}/links")
return self.post(
url,
{
"aspsp_id": aspsp_id,
},
)
45 changes: 45 additions & 0 deletions nordigen/oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from time import time

from apiclient.authentication_methods import BaseAuthenticationMethod


class OAuthAuthentication(BaseAuthenticationMethod):
"""Authentication using secret_id and secret_key."""

_access_token = None
_token_expiration = None
_refresh_token = None
_refresh_expiration = None

def __init__(
self,
body,
client,
expiry_margin=10,
):
"""Initialize OAuthAuthentication."""
self._client = client
self._body = body
self._expiry_margin = expiry_margin

def get_headers(self):
self.refresh_token()
return {
"Authorization": f"Bearer {self._access_token}",
}

def refresh_token(self):
if self._token_expiration and self._token_expiration >= int(time()):
return True

if self._refresh_expiration and self._refresh_expiration >= int(time()):
ret = self._client.refresh(refresh_token=self._refresh_token)
self._access_token = ret.get("access")
self._token_expiration = int(time()) + int(ret.get("access_expires")) - self._expiry_margin
return True

ret = self._client.token(**self._body)
self._access_token = ret.get("access")
self._refresh_token = ret.get("refresh")
self._token_expiration = int(time()) + int(ret.get("access_expires")) - self._expiry_margin
self._refresh_expiration = int(time()) + int(ret.get("refresh_expires")) - self._expiry_margin
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[tool.black]
line-length=120
py36=true
py36=true
15 changes: 13 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,16 @@
from nordigen import Client


def test_client(token="secret-token", request_strategy=Mock(spec=BaseRequestStrategy)):
return Client(token=token, request_strategy=request_strategy)
def test_client(
token=None,
request_strategy=Mock(spec=BaseRequestStrategy),
secret_id="secret-id",
secret_key="secret-key",
):
args = dict(
token=token,
request_strategy=request_strategy,
secret_id=secret_id,
secret_key=secret_key,
)
return Client(**args)
Loading

0 comments on commit 19a95cd

Please sign in to comment.