Skip to content
This repository was archived by the owner on Dec 8, 2023. It is now read-only.

Commit 6fcfac6

Browse files
authored
Merge pull request #15 from alma/auth/configurable-credentials
Auth/configurable credentials
2 parents b236017 + 01bb178 commit 6fcfac6

14 files changed

Lines changed: 317 additions & 50 deletions

alma/client.py

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,95 @@
11
import logging
22
import platform
3+
from typing import Optional
34

45
from . import endpoints
56
from .api_modes import ApiModes
67
from .context import Context
8+
from .credentials import (
9+
MerchantIdCredentials,
10+
AlmaSessionCredentials,
11+
ApiKeyCredentials,
12+
Credentials,
13+
)
714
from .version import __version__ as alma_version
815

916

1017
class Client:
1118
SANDBOX_API_URL = "https://api.sandbox.getalma.eu"
1219
LIVE_API_URL = "https://api.getalma.eu"
1320

14-
def __init__(self, api_key, **options):
15-
if not api_key:
16-
raise ValueError("An API key is required to instantiate a new Client")
21+
@classmethod
22+
def with_api_key(cls, api_key: str, **options):
23+
return cls(credentials=ApiKeyCredentials(api_key), **options)
24+
25+
@classmethod
26+
def with_merchant_id(cls, merchant_id: str, mode: ApiModes = ApiModes.LIVE, **options):
27+
return cls(credentials=MerchantIdCredentials(mode, merchant_id), **options)
28+
29+
@classmethod
30+
def with_alma_session(
31+
cls,
32+
session_id: str,
33+
cookie_name: str = "alma_sess",
34+
mode: ApiModes = ApiModes.LIVE,
35+
**options
36+
):
37+
return cls(credentials=AlmaSessionCredentials(mode, session_id, cookie_name), **options)
38+
39+
def __init__(
40+
self,
41+
api_key: Optional[str] = None,
42+
credentials: Optional[Credentials] = None,
43+
mode: Optional[ApiModes] = None,
44+
**kwargs
45+
):
46+
"""
47+
Create a new instance of the Alma API Client.
48+
49+
It is recommended to use one the convenience methods instead of the default constructor:
50+
- Client.with_api_key
51+
- Client.with_merchant_id
52+
- Client.with_alma_session
53+
54+
:param api_key: Deprecated - use Client.with_api_key("<api_key>") instead
55+
:type api_key: str
56+
57+
:param credentials A `Credentials` instance to be used to configure requests made to
58+
the API. This would typically be set by one of the convenience
59+
methods mentioned above.
60+
:type credentials Credentials
61+
62+
:param mode Deprecated. Use `mode` param of convenience methods above
63+
API mode to be used: either ApiModes.LIVE or ApiModes.TEST
64+
:type mode ApiModes
65+
66+
:keyword logger A logger instance to be used instead of the default one
67+
:keyword api_root root URL(s) to call Alma's API at.
68+
You probably don't want to change it!
69+
70+
Expected types:
71+
-------------
72+
str: the provided URL will be used for both LIVE and TEST modes
73+
dict: must have two keys, ApiModes.LIVE and ApiModes.TEST, each
74+
value must be the URL to be used for each mode
75+
76+
"""
77+
if isinstance(credentials, ApiKeyCredentials):
78+
api_key = credentials.api_key
79+
80+
if not credentials:
81+
if not api_key:
82+
raise ValueError("Valid credentials are required to instantiate a new Client")
83+
84+
# Backward compatibility with the older init method
85+
credentials = ApiKeyCredentials(api_key)
1786

1887
options = {
1988
"api_root": {ApiModes.TEST: self.SANDBOX_API_URL, ApiModes.LIVE: self.LIVE_API_URL},
20-
"mode": ApiModes.LIVE if api_key.startswith("sk_live") else ApiModes.TEST,
89+
"mode": mode if mode is not None else credentials.mode,
2190
"logger": logging.getLogger("alma-python-client"),
22-
**options,
91+
"credentials": credentials,
92+
**kwargs,
2393
}
2494

2595
if type(options["api_root"]) is str:
@@ -37,10 +107,9 @@ def __init__(self, api_key, **options):
37107
)
38108
)
39109

40-
self.context = Context(api_key, options)
41-
110+
self.context = Context(options)
42111
self.init_user_agent()
43-
self._endpoints = {}
112+
self._endpoints = {} # type: ignore
44113

45114
def add_user_agent_component(self, component, version):
46115
self.context.add_user_agent_component(component, version)

alma/context.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
class Context:
2-
def __init__(self, api_key, options):
3-
self.api_key = api_key
2+
def __init__(self, options):
43
self.options = options
54

65
self.user_agent_components = []
@@ -17,6 +16,10 @@ def api_root(self):
1716
def mode(self):
1817
return self.options["mode"]
1918

19+
@property
20+
def credentials(self):
21+
return self.options["credentials"]
22+
2023
def url_for(self, path):
2124
root = self.api_root[self.mode].rstrip("/")
2225

alma/credentials/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# flake8: noqa
2+
3+
from .base import Credentials
4+
from .alma_session import AlmaSessionCredentials
5+
from .api_key import ApiKeyCredentials
6+
from .merchant_id import MerchantIdCredentials

alma/credentials/alma_session.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from . import Credentials
2+
from .. import ApiModes
3+
4+
5+
class AlmaSessionCredentials(Credentials):
6+
def __init__(self, mode: ApiModes, session_id: str, cookie_name: str):
7+
super().__init__(mode)
8+
self.session_id = session_id
9+
self.cookie_name = cookie_name
10+
11+
def configure(self, request):
12+
request.cookies[self.cookie_name] = self.session_id

alma/credentials/api_key.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from . import Credentials
2+
from .. import ApiModes
3+
4+
5+
class ApiKeyCredentials(Credentials):
6+
def __init__(self, api_key: str):
7+
self.api_key = api_key
8+
super().__init__(self.mode)
9+
10+
def configure(self, request):
11+
request.headers["Authorization"] = "Alma-Auth {api_key}".format(api_key=self.api_key)
12+
13+
@property
14+
def mode(self):
15+
return ApiModes.LIVE if self.api_key.startswith("sk_live") else ApiModes.TEST

alma/credentials/base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from alma import ApiModes
2+
from alma.request import Request
3+
4+
5+
class Credentials:
6+
def __init__(self, mode: ApiModes):
7+
self._mode = mode
8+
9+
def configure(self, request: Request):
10+
raise NotImplementedError()
11+
12+
@property
13+
def mode(self):
14+
return self._mode

alma/credentials/merchant_id.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from . import Credentials
2+
from .. import ApiModes
3+
4+
5+
class MerchantIdCredentials(Credentials):
6+
def __init__(self, mode: ApiModes, merchant_id: str):
7+
super().__init__(mode)
8+
self.merchant_id = merchant_id
9+
10+
def configure(self, request):
11+
request.headers["Authorization"] = "Alma-Merchant-Auth {merchant_id}".format(
12+
merchant_id=self.merchant_id
13+
)

alma/endpoints/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from .merchants import Merchants
55
from .orders import Orders
66
from .exports import Exports
7-
from .payments import Payments
7+
from .payments import Payments

alma/request.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1+
from functools import wraps
2+
13
import requests
24

35
from .response import Response
46

57

8+
def configure_credentials(func):
9+
def decorator(f):
10+
@wraps(f)
11+
def decorated(request, *args, **kwargs):
12+
request.context.credentials.configure(request)
13+
return f(request, *args, **kwargs)
14+
15+
return decorated
16+
17+
return decorator(func)
18+
19+
620
class RequestError(Exception):
721
def __init__(self, error, url, response):
822
self.error = error
@@ -17,11 +31,9 @@ def __init__(self, context, url):
1731

1832
self.headers = {
1933
"User-Agent": self.context.user_agent_string(),
20-
"Authorization": "Alma-Auth {context_api_key}".format(
21-
context_api_key=self.context.api_key
22-
),
2334
"Accept": "application/json",
2435
}
36+
self.cookies = {}
2537
self.params = {}
2638
self.body = None
2739

@@ -35,16 +47,19 @@ def set_query_params(self, params):
3547
self.params = params
3648
return self
3749

50+
@configure_credentials
3851
def get(self):
39-
res = requests.get(self.url, self.params, headers=self.headers)
52+
res = requests.get(self.url, self.params, headers=self.headers, cookies=self.cookies)
4053
return self._process_response(res)
4154

55+
@configure_credentials
4256
def post(self):
43-
res = requests.post(self.url, json=self.body, headers=self.headers)
57+
res = requests.post(self.url, json=self.body, headers=self.headers, cookies=self.cookies)
4458
return self._process_response(res)
4559

60+
@configure_credentials
4661
def put(self):
47-
res = requests.put(self.url, json=self.body, headers=self.headers)
62+
res = requests.put(self.url, json=self.body, headers=self.headers, cookies=self.cookies)
4863
return self._process_response(res)
4964

5065
def _process_response(self, resp):

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ exclude = '''
66
/(
77
\.git
88
| tmp
9+
| .venv
910
)/
1011
'''

0 commit comments

Comments
 (0)