Skip to content

Commit f843d5e

Browse files
author
Oleg Menteshashvili
committed
Initial project
1 parent 84a9b15 commit f843d5e

22 files changed

+829
-0
lines changed

blazingdocs/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
__version__ = '1.0.0'
2+
3+
from .client import BlazingClient
4+
from .models import *
5+
from .enums import DataSourceType
6+
from .exceptions import BlazingException
7+
from .parameters import MergeParameters
8+
from .utils import FormFile, Object

blazingdocs/client.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import uuid
2+
import requests
3+
from builtins import isinstance
4+
from typing import List, Union
5+
from .exceptions import BlazingException
6+
from .models import Operation, Account, Usage, Template
7+
from .parameters import MergeParameters
8+
from .utils import FormFile
9+
10+
11+
# BlazingClient API client.
12+
class BlazingClient:
13+
__headers = None
14+
__api_key: str = None
15+
__base_url: str = 'https://api.blazingdocs.com'
16+
17+
# Creates new instance of client.
18+
def __init__(self, api_key: str):
19+
self.__api_key = api_key
20+
self.__headers = {'X-API-Key': self.__api_key}
21+
22+
# Executes merge operation with union template.
23+
def __merge(self, data: str, filename: str, parameters: MergeParameters, template: Union[str, uuid.UUID, FormFile]) -> Operation:
24+
url = self.__base_url + '/operation/merge'
25+
26+
if not (data and data.strip()):
27+
raise ValueError('Data is not provided')
28+
29+
if not (filename and filename.strip()):
30+
raise ValueError('Output filename is not provided')
31+
32+
if not parameters:
33+
raise ValueError('Merge parameters are not provided')
34+
35+
payload = dict(
36+
Data=(None, data),
37+
OutputName=(None, filename),
38+
MergeParameters=(None, parameters.to_json())
39+
)
40+
41+
if isinstance(template, uuid.UUID):
42+
payload['Template'] = (None, str(template))
43+
elif isinstance(template, str):
44+
payload['Template'] = (None, template.replace('\\', '/'))
45+
elif isinstance(template, FormFile):
46+
payload['Template'] = (template.name, template.content)
47+
else:
48+
raise ValueError('Template is not provided')
49+
50+
response = requests.post(url=url, headers=self.__headers, files=payload, timeout=10)
51+
return Operation(self.__handle_response(response))
52+
53+
# Gets account info.
54+
def get_account(self) -> Account:
55+
url = self.__base_url + '/account'
56+
response = requests.get(url, headers=self.__headers, timeout=10)
57+
return Account(self.__handle_response(response))
58+
59+
# Gets usage info.
60+
def get_usage(self) -> Usage:
61+
url = self.__base_url + '/usage'
62+
response = requests.get(url, headers=self.__headers, timeout=10)
63+
return Usage(self.__handle_response(response))
64+
65+
# Gets templates list.
66+
def get_templates(self, path: str = None) -> List[Template]:
67+
url = self.__base_url + '/templates'
68+
69+
if path is not None:
70+
url += path
71+
72+
response = requests.get(url, headers=self.__headers, timeout=10)
73+
decoded = self.__handle_response(response)
74+
return list(map(Template, decoded))
75+
76+
# Executes merge operation with template id.
77+
def merge_with_id(self, data: str, filename: str, parameters: MergeParameters, template: uuid.UUID) -> Operation:
78+
return self.__merge(data, filename, parameters, template)
79+
80+
# Executes merge operation with template path.
81+
def merge_with_relative_path(self, data: str, filename: str, parameters: MergeParameters, template: str) -> Operation:
82+
return self.__merge(data, filename, parameters, template)
83+
84+
# Executes merge operation with template form file.
85+
def merge_with_form_file(self, data: str, filename: str, parameters: MergeParameters, template: FormFile) -> Operation:
86+
return self.__merge(data, filename, parameters, template)
87+
88+
# Handle response.
89+
def __handle_response(self, response: requests.Response):
90+
try:
91+
if not response.status_code == requests.codes.ok:
92+
message = response.json().get('message', '[Message not found]')
93+
raise BlazingException(response.status_code, message)
94+
except ValueError as ex:
95+
raise ex
96+
97+
return response.json()

blazingdocs/enums.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from enum import Enum
2+
3+
4+
class DataSourceType(str, Enum):
5+
CSV: str = 'CSV'
6+
JSON: str = 'JSON'
7+
XML: str = 'XML'

blazingdocs/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class BlazingException(Exception):
2+
def __init__(self, http_status_code: int, message: str):
3+
super(BlazingException, self).__init__(message)
4+
5+
self.message: str = message
6+
self.status_code: int = http_status_code
7+
8+
def __str__(self):
9+
message = "Message: %s. Status Code: %s" % (self.message, self.status_code)
10+
return message.strip()

blazingdocs/json_encoders.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from json import JSONEncoder
2+
from .parameters import MergeParameters
3+
4+
5+
# JSONEncoder that encodes MergeParameters objects as JSON
6+
class MergeParametersEncoder(JSONEncoder):
7+
def default(self, object):
8+
if isinstance(object, MergeParameters):
9+
return object.__dict__
10+
else:
11+
# Call base class implementation which takes care of
12+
# raising exceptions for unsupported types
13+
return JSONEncoder.default(self, object)

blazingdocs/models.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import requests
2+
from io import BytesIO
3+
from datetime import datetime
4+
from decimal import Decimal
5+
from uuid import UUID
6+
7+
8+
class Plan:
9+
10+
def __init__(self, response):
11+
self.response = response
12+
13+
@property
14+
def id(self) -> UUID:
15+
return self.response['id']
16+
17+
@property
18+
def name(self) -> str:
19+
return self.response['name']
20+
21+
@property
22+
def price(self) -> Decimal:
23+
return self.response['price']
24+
25+
@property
26+
def price_per_unit(self) -> Decimal:
27+
return self.response['pricePerUnit']
28+
29+
@property
30+
def quota(self) -> int:
31+
return self.response['quota']
32+
33+
34+
class Account:
35+
36+
def __init__(self, response):
37+
self.response = response
38+
39+
@property
40+
def id(self) -> UUID:
41+
return self.response['id']
42+
43+
@property
44+
def name(self) -> str:
45+
return self.response['name']
46+
47+
@property
48+
def plan(self) -> Plan:
49+
return Plan(self.response['plan'])
50+
51+
@property
52+
def api_key(self) -> str:
53+
return self.response['apiKey']
54+
55+
@property
56+
def obsolete_api_key(self) -> str:
57+
return self.response['obsoleteApiKey']
58+
59+
@property
60+
def created_at(self) -> datetime:
61+
return self.response['createdAt']
62+
63+
@property
64+
def last_synced_at(self) -> datetime:
65+
return self.response['lastSyncedAt']
66+
67+
@property
68+
def updated_at(self) -> datetime:
69+
return self.response['updatedAt']
70+
71+
72+
class OperationType:
73+
74+
def __init__(self, response):
75+
self.response = response
76+
77+
@property
78+
def name(self) -> str:
79+
return self.response['name']
80+
81+
82+
class Operation:
83+
84+
def __init__(self, response):
85+
self.response = response
86+
87+
@property
88+
def id(self) -> UUID:
89+
return self.response['id']
90+
91+
@property
92+
def operation_type(self) -> OperationType:
93+
return OperationType(self.response['type'])
94+
95+
@property
96+
def page_count(self) -> int:
97+
return self.response['pageCount']
98+
99+
@property
100+
def elapsed_milliseconds(self) -> int:
101+
return self.response['elapsedMilliseconds']
102+
103+
@property
104+
def remote_ip_address(self) -> str:
105+
return self.response['remoteIpAddress']
106+
107+
@property
108+
def files(self) -> []:
109+
return list(map(File, self.response['files']))
110+
111+
@property
112+
def io(self) -> BytesIO:
113+
if not self.files:
114+
raise ValueError('File not found')
115+
116+
response = requests.get(self.files[0].download_url, timeout=1800)
117+
return BytesIO(response.content)
118+
119+
120+
class File:
121+
122+
def __init__(self, response):
123+
self.response = response
124+
125+
@property
126+
def id(self) -> UUID:
127+
return self.response['id']
128+
129+
@property
130+
def name(self) -> str:
131+
return self.response['name']
132+
133+
@property
134+
def content_type(self) -> str:
135+
return self.response['contentType']
136+
137+
@property
138+
def download_url(self) -> str:
139+
return self.response['downloadUrl']
140+
141+
@property
142+
def created_at(self) -> datetime:
143+
return self.response['createdAt']
144+
145+
@property
146+
def last_modified_at(self) -> datetime:
147+
return self.response['lastModifiedAt']
148+
149+
@property
150+
def last_accessed_at(self) -> datetime:
151+
return self.response['lastAccessedAt']
152+
153+
@property
154+
def length(self) -> int:
155+
return self.response['length']
156+
157+
@property
158+
def io(self) -> BytesIO:
159+
response = requests.get(self.download_url, timeout=1800)
160+
return BytesIO(response.content)
161+
162+
163+
class Template(File):
164+
pass
165+
166+
167+
class Usage:
168+
169+
def __init__(self, response):
170+
self.response = response
171+
172+
@property
173+
def quota(self) -> int:
174+
return self.response['quota']
175+
176+
@property
177+
def page_count(self) -> int:
178+
return self.response['pageCount']
179+
180+
@property
181+
def usage(self) -> int:
182+
return self.response['usage']

blazingdocs/parameters.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .enums import DataSourceType
2+
from .utils import Object
3+
4+
5+
class MergeParameters(Object):
6+
def __init__(self):
7+
self.dataSourceName: str = 'data'
8+
self.dataSourceType: DataSourceType = DataSourceType.JSON
9+
self.sequence: bool = False
10+
self.parseColumns: bool = False
11+
self.strict: bool = True

blazingdocs/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import json
2+
from io import BytesIO
3+
4+
5+
class Object:
6+
def to_json(self):
7+
return json.dumps(self, default=lambda o: o.__dict__)
8+
9+
10+
class FormFile:
11+
12+
def __init__(self, name: str):
13+
self.name: str = name
14+
self.contentType: str
15+
self.content: BytesIO

examples/get_account.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from blazingdocs import BlazingClient
2+
3+
4+
def get_account():
5+
client = BlazingClient('YOUR-API-KEY')
6+
account = client.get_account()
7+
8+
9+
if __name__ == '__main__':
10+
get_account()
11+

examples/get_templates.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from blazingdocs import BlazingClient
2+
3+
4+
def get_templates():
5+
client = BlazingClient('YOUR-API-KEY')
6+
templates = client.get_templates()
7+
8+
9+
if __name__ == '__main__':
10+
get_templates()
11+

examples/get_usage.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from blazingdocs import BlazingClient
2+
3+
4+
def get_usage():
5+
client = BlazingClient('YOUR-API-KEY')
6+
usage = client.get_usage()
7+
8+
9+
if __name__ == '__main__':
10+
get_usage()

0 commit comments

Comments
 (0)