Skip to content

Commit 0716130

Browse files
author
Mikhail Pyrev
committed
Initial
0 parents  commit 0716130

35 files changed

+1003
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__pycache__/
2+
.idea/
3+
/build/
4+
/dist/
5+
*.sqlite3
6+
local_settings.py
7+
*.egg-info/

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Mikhail Pyrev
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.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
python-capitalist
2+
=================
3+
4+
A little client for API of [capitalist.net](https://capitalist.net/).
5+
6+
Couple of API methods are implemented because I need only them.
7+
Feel free to ask for additional methods. I'll look into it.
8+
9+
`import_batch_advanced` DOES NOT support certificates with passphrase.
10+
11+
12+
LICENSE
13+
-------
14+
15+
MIT

capitalist/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .capitalist import Capitalist
2+
from .const import __version__

capitalist/auth.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from base64 import b64encode
2+
3+
from cryptography.hazmat.backends import default_backend
4+
from cryptography.hazmat.primitives import serialization, hashes
5+
from cryptography.hazmat.primitives.asymmetric import padding
6+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
7+
8+
from .exceptions import RequestException
9+
from .utils import retry
10+
11+
12+
class Authenticator:
13+
def __init__(self, request_executor, login, password):
14+
self.request_executor = request_executor
15+
self.login = login
16+
self.password = password
17+
self._encrypted_password = None
18+
self._token = None
19+
20+
@retry((RequestException,))
21+
def _get_token(self):
22+
data = {
23+
'operation': 'get_token',
24+
'login': self.login,
25+
}
26+
json_data = self.request_executor.request(data=data)
27+
return json_data
28+
29+
def setup(self):
30+
token_response = self._get_token()
31+
self._token = token_response['data']['token']
32+
self._encrypted_password = EncryptedPassword(
33+
self.password, token_response['data']['modulus'], token_response['data']['exponent'])
34+
35+
@property
36+
def encrypted_password(self):
37+
if self._encrypted_password is None:
38+
self.setup()
39+
return self._encrypted_password
40+
41+
@property
42+
def token(self):
43+
if self._token is None:
44+
self.setup()
45+
return self._token
46+
47+
48+
class EncryptedPassword:
49+
"""
50+
PKCS1 v1.5 encrypted password.
51+
"""
52+
53+
def __init__(self, password, modulus, exponent):
54+
self._password = password.encode('utf-8')
55+
modulus = int(modulus, 16)
56+
exponent = int(exponent, 16)
57+
public_numbers = RSAPublicNumbers(exponent, modulus)
58+
self.public_key = public_numbers.public_key(default_backend())
59+
60+
@property
61+
def password(self):
62+
return self.public_key.encrypt(self._password, padding.PKCS1v15()).hex()
63+
64+
65+
class Signer:
66+
def __init__(self, private_key: bytes):
67+
self._key = serialization.load_pem_private_key(private_key, None, default_backend())
68+
69+
def sign(self, message):
70+
signature = self._key.sign(message, padding.PKCS1v15(), hashes.SHA1())
71+
signature = b64encode(signature)
72+
return signature

capitalist/capitalist.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from decimal import Decimal
2+
from pathlib import Path
3+
from typing import Union
4+
5+
from capitalist.exceptions import ResponseException
6+
from capitalist.models import Account, CurrencyRate
7+
from .auth import Authenticator, Signer
8+
from .exceptions import RequestException, ImproperlyConfigured
9+
from .request_executor import RequestExecutor
10+
from .utils import retry
11+
12+
13+
class Capitalist:
14+
def __init__(
15+
self,
16+
login,
17+
password,
18+
request_executor_class=RequestExecutor,
19+
authenticator_class=Authenticator,
20+
signer_class=Signer,
21+
private_key: Union[bytes, Path] = None,
22+
):
23+
self.login = login
24+
self.request_executor = request_executor_class()
25+
self.authenticator = authenticator_class(self.request_executor, login, password)
26+
self.signer = None
27+
if private_key is not None:
28+
if isinstance(private_key, Path):
29+
private_key = private_key.read_bytes()
30+
self.signer = signer_class(private_key)
31+
32+
@retry((RequestException,))
33+
def secure_request(self, operation, data=None, **kwargs):
34+
if data is None:
35+
data = {}
36+
data.update({
37+
'operation': operation,
38+
'login': self.login,
39+
'token': self.authenticator.token,
40+
'encrypted_password': self.authenticator.encrypted_password.password,
41+
})
42+
kwargs['data'] = data
43+
response_json = self.request_executor.request(**kwargs)
44+
code = response_json['code']
45+
if code != 0:
46+
raise ResponseException(code, response_json['message'])
47+
return response_json
48+
49+
def accounts(self):
50+
json_data = self.secure_request('get_accounts')
51+
return [Account.parse_json(acc_json) for acc_json in json_data['data']['accounts']]
52+
53+
def currency_rates(self):
54+
json_data = self.secure_request('currency_rates')['data']['rates']
55+
rates = []
56+
for type_ in CurrencyRate.TYPES:
57+
for rate_data in json_data[type_]:
58+
rates.append(CurrencyRate.parse_json(rate_data, type_=type_))
59+
return rates
60+
61+
def import_batch_advanced(self, payments, account_rur, account_usd, account_eur, account_btc):
62+
if self.signer is None:
63+
raise ImproperlyConfigured('Provide private key and/or passphrase to be able to sign data.')
64+
65+
payment_data = '\n'.join([payment.as_batch_record() for payment in payments])
66+
data = {
67+
'batch': payment_data,
68+
'verification_type': 'SIGNATURE',
69+
'verification_data': self.signer.sign(payment_data.encode('utf-8')),
70+
'account_RUR': account_rur,
71+
'account_USD': account_usd,
72+
'account_EUR': account_eur,
73+
'account_BTC': account_btc,
74+
}
75+
return self.secure_request('import_batch_advanced', data)
76+
77+
def get_document_fee(
78+
self, document_type: str, source_account: str, amount: Decimal, dest_account: str = None,
79+
wiretag: str = None):
80+
data = {
81+
'document_type': document_type,
82+
'source_account': source_account,
83+
'amount': amount,
84+
}
85+
if dest_account:
86+
data['dest_account'] = dest_account
87+
if wiretag:
88+
data['wiretag'] = wiretag
89+
return self.secure_request('get_document_fee', data)

capitalist/const.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__version__ = '1.0.0'
2+
3+
TIMEOUT = 15
4+
API_URL = 'https://api.capitalist.net'

capitalist/contrib/__init__.py

Whitespace-only changes.

capitalist/contrib/django/__init__.py

Whitespace-only changes.

capitalist/contrib/django/django_capitalist/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)