decaf-api-client 0.0.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- decaf/api/client/__init__.py +2 -0
- decaf/api/client/_internal.py +13 -0
- decaf/api/client/machinery.py +820 -0
- decaf/api/client/py.typed +0 -0
- decaf/api/client/types.py +150 -0
- decaf/api/client/utils.py +201 -0
- decaf_api_client-0.0.12.dist-info/LICENSE +29 -0
- decaf_api_client-0.0.12.dist-info/METADATA +121 -0
- decaf_api_client-0.0.12.dist-info/RECORD +11 -0
- decaf_api_client-0.0.12.dist-info/WHEEL +5 -0
- decaf_api_client-0.0.12.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the essential machinery to perform remote API requests and define endpoint client implementations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"APIClientError",
|
|
7
|
+
"APIHeaderAuthorization",
|
|
8
|
+
"APIKeyAuthorization",
|
|
9
|
+
"APIServerError",
|
|
10
|
+
"APIServerErrorBadAuthorization",
|
|
11
|
+
"APIServerErrorBadRequest",
|
|
12
|
+
"APIServerErrorBlowup",
|
|
13
|
+
"APIServerErrorNotFound",
|
|
14
|
+
"APITokenAuthorization",
|
|
15
|
+
"Authorization",
|
|
16
|
+
"BaseCreateForm",
|
|
17
|
+
"BasePatchForm",
|
|
18
|
+
"BaseResource",
|
|
19
|
+
"BaseUpdateForm",
|
|
20
|
+
"Client",
|
|
21
|
+
"DeleteEndpoint",
|
|
22
|
+
"Endpoint",
|
|
23
|
+
"Error",
|
|
24
|
+
"Missing",
|
|
25
|
+
"RFiles",
|
|
26
|
+
"RHeaders",
|
|
27
|
+
"RMethod",
|
|
28
|
+
"RParams",
|
|
29
|
+
"RPath",
|
|
30
|
+
"RTimeout",
|
|
31
|
+
"Request",
|
|
32
|
+
"ResourceCreateEndpoint",
|
|
33
|
+
"ResourceEndpoint",
|
|
34
|
+
"ResourceListEndpoint",
|
|
35
|
+
"ResourcePatchEndpoint",
|
|
36
|
+
"ResourceRetrieveEndpoint",
|
|
37
|
+
"ResourceUpdateEndpoint",
|
|
38
|
+
"StandardResourceEndpoint",
|
|
39
|
+
"command",
|
|
40
|
+
"query",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
import time
|
|
45
|
+
from abc import abstractmethod
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from json import load as loadjson
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import (
|
|
50
|
+
Any,
|
|
51
|
+
BinaryIO,
|
|
52
|
+
Callable,
|
|
53
|
+
ClassVar,
|
|
54
|
+
Dict,
|
|
55
|
+
Generator,
|
|
56
|
+
Generic,
|
|
57
|
+
Iterable,
|
|
58
|
+
List,
|
|
59
|
+
Literal,
|
|
60
|
+
Optional,
|
|
61
|
+
Set,
|
|
62
|
+
TextIO,
|
|
63
|
+
Tuple,
|
|
64
|
+
Type,
|
|
65
|
+
TypeVar,
|
|
66
|
+
Union,
|
|
67
|
+
)
|
|
68
|
+
from urllib.parse import urljoin
|
|
69
|
+
|
|
70
|
+
import requests
|
|
71
|
+
from pydantic import BaseModel, Field
|
|
72
|
+
from requests.auth import AuthBase
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class Error(Exception):
|
|
77
|
+
"""
|
|
78
|
+
Provides a base exception model for the DECAF client library.
|
|
79
|
+
|
|
80
|
+
.. note:: You should probably catch sub-classes of this exception class.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
#: Error message.
|
|
84
|
+
message: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class APIClientError(Error):
|
|
89
|
+
"""
|
|
90
|
+
Provides an exception model for errors generated during request-response cycle (not by the API server).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
#: Underlying exception.
|
|
94
|
+
underlying: Exception
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class APIServerError(Error):
|
|
99
|
+
"""
|
|
100
|
+
Provides an exception for API server generated errors.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
#: HTTP status code of the error response received from the API server.
|
|
104
|
+
status: int
|
|
105
|
+
|
|
106
|
+
#: Payload of the error response received from the API server.
|
|
107
|
+
payload: Union[str, bytes, None]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class APIServerErrorBadRequest(APIServerError):
|
|
112
|
+
"""
|
|
113
|
+
Provides an exception for API server generated HTTP 400 errors.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
#: Error definition.
|
|
117
|
+
feedback: Dict[str, List[str]]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class APIServerErrorBadAuthorization(APIServerError):
|
|
122
|
+
"""
|
|
123
|
+
Provides an exception for API server generated HTTP 401, 403 and 405 errors.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class APIServerErrorNotFound(APIServerError):
|
|
131
|
+
"""
|
|
132
|
+
Provides an exception for API server generated HTTP 404 errors.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class APIServerErrorBlowup(APIServerError):
|
|
140
|
+
"""
|
|
141
|
+
Provides an exception for API server generated HTTP 5XX errors.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
#: Defines a type alias for HTTP verbs.
|
|
148
|
+
RMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
149
|
+
|
|
150
|
+
#: Defines a type alias for relative path or path segments of the remote API endpoint.
|
|
151
|
+
RPath = Union[str, List[Union[str, int]]]
|
|
152
|
+
|
|
153
|
+
#: Defines a type alias for query-string parameters.
|
|
154
|
+
RParams = Dict[str, Union[str, int, List[str], List[int]]]
|
|
155
|
+
|
|
156
|
+
#: Defines a type alias for HTTP request headers.
|
|
157
|
+
RHeaders = Dict[str, str]
|
|
158
|
+
|
|
159
|
+
#: Defines a type alias for file objects.
|
|
160
|
+
_FileObject = Union[str, bytes, bytearray, TextIO, BinaryIO]
|
|
161
|
+
|
|
162
|
+
#: Defines a type alias for file object maps to be passed for multi-part uploads.
|
|
163
|
+
RFiles = Dict[str, Union[_FileObject, Tuple[_FileObject, str], Tuple[_FileObject, str, Dict[str, str]]]]
|
|
164
|
+
|
|
165
|
+
#: Defines a type alias for HTTP request timeouts.
|
|
166
|
+
RTimeout = Tuple[float, float]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class Request:
|
|
171
|
+
"""
|
|
172
|
+
Defines a high(ish)-level client request model.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
#: Request method.
|
|
176
|
+
method: RMethod
|
|
177
|
+
|
|
178
|
+
#: Relative path or path segments for the remote API endpoint URL.
|
|
179
|
+
path: RPath
|
|
180
|
+
|
|
181
|
+
#: Request parameters, if any.
|
|
182
|
+
params: Optional[RParams] = None
|
|
183
|
+
|
|
184
|
+
#: Request data payload, if any.
|
|
185
|
+
data: Any = None
|
|
186
|
+
|
|
187
|
+
#: Request data payload in JSON, if any (convenience for sending ``application/json`` payloads).
|
|
188
|
+
json: Any = None
|
|
189
|
+
|
|
190
|
+
#: Request data payload files, if any (convenience for uploading multipart payloads).
|
|
191
|
+
files: Optional[RFiles] = None
|
|
192
|
+
|
|
193
|
+
#: Request headers.
|
|
194
|
+
headers: Optional[RHeaders] = None
|
|
195
|
+
|
|
196
|
+
#: Defines the timeout, if any:
|
|
197
|
+
timeout: Optional[RTimeout] = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Authorization:
|
|
201
|
+
"""
|
|
202
|
+
Provides a base class for client authorization.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
@abstractmethod
|
|
206
|
+
def headers(self) -> Dict[str, str]:
|
|
207
|
+
"""
|
|
208
|
+
Provides the client authorization header for HTTP requests.
|
|
209
|
+
"""
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True)
|
|
214
|
+
class APIKeyAuthorization(Authorization):
|
|
215
|
+
"""
|
|
216
|
+
Provides API key authorization method.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
#: Defines the API key.
|
|
220
|
+
key: str = field(repr=False)
|
|
221
|
+
|
|
222
|
+
#: Defines the API secret.
|
|
223
|
+
scr: str = field(repr=False)
|
|
224
|
+
|
|
225
|
+
def headers(self) -> Dict[str, str]:
|
|
226
|
+
return {"Authorization": f"Key {self.key}:{self.scr}"}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class APITokenAuthorization(Authorization):
|
|
231
|
+
"""
|
|
232
|
+
Provides API token authorization method.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
#: API token.
|
|
236
|
+
token: str = field(repr=False)
|
|
237
|
+
|
|
238
|
+
def headers(self) -> Dict[str, str]:
|
|
239
|
+
return {"Authorization": f"Token {self.token}"}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass(frozen=True)
|
|
243
|
+
class APIHeaderAuthorization(Authorization):
|
|
244
|
+
"""
|
|
245
|
+
Provides authorization method directly from authentication headers.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
#: HTTP headers
|
|
249
|
+
buffer: RHeaders
|
|
250
|
+
|
|
251
|
+
def headers(self) -> Dict[str, str]:
|
|
252
|
+
return self.buffer
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass(frozen=True)
|
|
256
|
+
class Client:
|
|
257
|
+
"""
|
|
258
|
+
Provides a high-level client implementation for DECAF API.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
#: Base API URI.
|
|
262
|
+
url: str
|
|
263
|
+
|
|
264
|
+
#: API authorization.
|
|
265
|
+
auth: Authorization
|
|
266
|
+
|
|
267
|
+
class _NullAuth(AuthBase):
|
|
268
|
+
"""
|
|
269
|
+
Provides the no-op authentication provider implementation.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __call__(self, r: Any) -> Any:
|
|
273
|
+
return r
|
|
274
|
+
|
|
275
|
+
#: Defines the no-op authentication provider.
|
|
276
|
+
_noop_auth: ClassVar[_NullAuth] = _NullAuth()
|
|
277
|
+
|
|
278
|
+
#: Defines success status codes for each HTTP method.
|
|
279
|
+
_successmap: ClassVar[Dict[RMethod, Set[int]]] = {
|
|
280
|
+
"GET": {200},
|
|
281
|
+
"POST": {200, 201, 202},
|
|
282
|
+
"PUT": {200, 201, 202},
|
|
283
|
+
"PATCH": {200, 201, 202},
|
|
284
|
+
"DELETE": {200, 201, 202, 204},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
def __post_init__(self) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Sanitizes the base API URL.
|
|
290
|
+
|
|
291
|
+
>>> client = Client("http://example.com///", Authorization())
|
|
292
|
+
>>> client.url
|
|
293
|
+
'http://example.com'
|
|
294
|
+
>>> client = Client("http://example.com/api//", Authorization())
|
|
295
|
+
>>> client.url
|
|
296
|
+
'http://example.com/api'
|
|
297
|
+
"""
|
|
298
|
+
object.__setattr__(self, "url", re.sub(r"[/]+$", "", self.url))
|
|
299
|
+
|
|
300
|
+
def urlize(self, path: RPath) -> str:
|
|
301
|
+
"""
|
|
302
|
+
Builds the full URL for the given relative path or path segments.
|
|
303
|
+
|
|
304
|
+
:param path: Relative path or path segments of the remote API endpoint.
|
|
305
|
+
:return: Remote endpoint URL.
|
|
306
|
+
|
|
307
|
+
>>> client = Client("http://example.com///", Authorization())
|
|
308
|
+
>>> client.urlize("test")
|
|
309
|
+
'http://example.com/test/'
|
|
310
|
+
>>> client.urlize("test/")
|
|
311
|
+
'http://example.com/test/'
|
|
312
|
+
>>> client.urlize("/test")
|
|
313
|
+
'http://example.com/test/'
|
|
314
|
+
>>> client.urlize("/test/")
|
|
315
|
+
'http://example.com/test/'
|
|
316
|
+
>>> client.urlize("test//")
|
|
317
|
+
'http://example.com/test/'
|
|
318
|
+
>>> client.urlize("//test")
|
|
319
|
+
'http://example.com/test/'
|
|
320
|
+
>>> client.urlize("//test//")
|
|
321
|
+
'http://example.com/test/'
|
|
322
|
+
>>> client.urlize(["test"])
|
|
323
|
+
'http://example.com/test/'
|
|
324
|
+
>>> client.urlize(["test/"])
|
|
325
|
+
'http://example.com/test/'
|
|
326
|
+
>>> client.urlize(["/test"])
|
|
327
|
+
'http://example.com/test/'
|
|
328
|
+
>>> client.urlize(["/test/"])
|
|
329
|
+
'http://example.com/test/'
|
|
330
|
+
>>> client.urlize(["test//"])
|
|
331
|
+
'http://example.com/test/'
|
|
332
|
+
>>> client.urlize(["//test"])
|
|
333
|
+
'http://example.com/test/'
|
|
334
|
+
>>> client.urlize(["//test//"])
|
|
335
|
+
'http://example.com/test/'
|
|
336
|
+
>>> client.urlize(["//test//", "//test2//", "//test3/4/5//"])
|
|
337
|
+
'http://example.com/test/test2/test3/4/5/'
|
|
338
|
+
"""
|
|
339
|
+
if isinstance(path, list):
|
|
340
|
+
path = "/".join(str(i).strip("/") for i in path)
|
|
341
|
+
return urljoin(self.url + "/", re.sub(r"[/]*$", "/", re.sub(r"^[/]*", "", path)))
|
|
342
|
+
|
|
343
|
+
def run(self, request: Request) -> requests.Response:
|
|
344
|
+
"""
|
|
345
|
+
Runs the given request and returns the raw response.
|
|
346
|
+
|
|
347
|
+
:param request: :py:class:`Request` instance to run.
|
|
348
|
+
:return: Raw :py:class:`requests.Response` instance.
|
|
349
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
return requests.request(
|
|
353
|
+
method=request.method,
|
|
354
|
+
url=self.urlize(request.path),
|
|
355
|
+
params=request.params,
|
|
356
|
+
data=request.data,
|
|
357
|
+
json=request.json,
|
|
358
|
+
files=request.files, # type: ignore
|
|
359
|
+
headers={**(request.headers or {}), **self.auth.headers()},
|
|
360
|
+
timeout=request.timeout,
|
|
361
|
+
auth=self._noop_auth,
|
|
362
|
+
)
|
|
363
|
+
except requests.exceptions.HTTPError as err:
|
|
364
|
+
raise APIClientError("HTTP Error", err)
|
|
365
|
+
except requests.exceptions.Timeout as err:
|
|
366
|
+
raise APIClientError("Timeout Error", err)
|
|
367
|
+
except requests.exceptions.ConnectionError as err:
|
|
368
|
+
raise APIClientError("Connection Error", err)
|
|
369
|
+
except requests.exceptions.RequestException as err:
|
|
370
|
+
raise APIClientError("Request Error", err)
|
|
371
|
+
|
|
372
|
+
def request(
|
|
373
|
+
self,
|
|
374
|
+
method: RMethod,
|
|
375
|
+
path: RPath,
|
|
376
|
+
params: Optional[RParams] = None,
|
|
377
|
+
data: Any = None,
|
|
378
|
+
json: Any = None,
|
|
379
|
+
files: Optional[RFiles] = None,
|
|
380
|
+
headers: Optional[RHeaders] = None,
|
|
381
|
+
timeout: Optional[RTimeout] = None,
|
|
382
|
+
asis: bool = False,
|
|
383
|
+
) -> Any:
|
|
384
|
+
"""
|
|
385
|
+
Provides a high-level interface to run HTTP request to the remote API server.
|
|
386
|
+
|
|
387
|
+
:param method: Request method.
|
|
388
|
+
:param path: Relative path or path segments for the remote API endpoint URL.
|
|
389
|
+
:param params: Request parameters, if any.
|
|
390
|
+
:param data: Request data payload, if any.
|
|
391
|
+
:param json: Request data payload in JSON, if any (convenience for sending ``application/json`` payloads).
|
|
392
|
+
:param files: Request data payload files, if any (convenience for uploading multipart payloads).
|
|
393
|
+
:param headers: Request headers.
|
|
394
|
+
:param timeout: Defines the timeout, if any.
|
|
395
|
+
:param asis: Whether to return response content as is or marshall it to Python object (default is ``False``).
|
|
396
|
+
:return: Raw response content or Python object as per ``asis`` parameter.
|
|
397
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
398
|
+
:raises APIServerError: In case that there is a server generated error reported via HTTP status codes.
|
|
399
|
+
"""
|
|
400
|
+
## Build the request, attempt to run it and get a response:
|
|
401
|
+
response = self.run(
|
|
402
|
+
Request(
|
|
403
|
+
method=method,
|
|
404
|
+
path=path,
|
|
405
|
+
params=params,
|
|
406
|
+
data=data,
|
|
407
|
+
json=json,
|
|
408
|
+
files=files,
|
|
409
|
+
headers=headers,
|
|
410
|
+
timeout=timeout,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
## Check the status code and return if successful:
|
|
415
|
+
if response.status_code in self._successmap[method]:
|
|
416
|
+
if not response.content:
|
|
417
|
+
return None
|
|
418
|
+
return response.content if asis else response.json()
|
|
419
|
+
|
|
420
|
+
## We have a remote error. Encode it into an exception and raise it:
|
|
421
|
+
if response.status_code >= 500:
|
|
422
|
+
raise APIServerErrorBlowup("Server error", response.status_code, response.content)
|
|
423
|
+
elif response.status_code == 404:
|
|
424
|
+
raise APIServerErrorNotFound("Resource not found", response.status_code, response.content)
|
|
425
|
+
elif response.status_code == 400:
|
|
426
|
+
raise APIServerErrorBadRequest("Bad request", response.status_code, response.content, response.json())
|
|
427
|
+
elif response.status_code in {401, 403, 405}:
|
|
428
|
+
raise APIServerErrorBadAuthorization("Bad authorization", response.status_code, response.content)
|
|
429
|
+
else:
|
|
430
|
+
raise APIServerError("Unknown error", response.status_code, response.content)
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def version(self) -> str:
|
|
434
|
+
"""
|
|
435
|
+
Returns the remote API version.
|
|
436
|
+
"""
|
|
437
|
+
return self.request("GET", "version")["version"] # type: ignore
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def healthcheck(self) -> str:
|
|
441
|
+
"""
|
|
442
|
+
Performs an async healthcheck.
|
|
443
|
+
"""
|
|
444
|
+
## Create a healthcheck job and get its ID:
|
|
445
|
+
jobid = self.request("GET", ["jobs", "healthcheck", "status"])["id"]
|
|
446
|
+
|
|
447
|
+
## Get job result:
|
|
448
|
+
finished, result = self.get_job_result(jobid)
|
|
449
|
+
|
|
450
|
+
## Check:
|
|
451
|
+
while not finished:
|
|
452
|
+
## Sleep a while:
|
|
453
|
+
time.sleep(1)
|
|
454
|
+
|
|
455
|
+
## Get job result:
|
|
456
|
+
finished, result = self.get_job_result(jobid)
|
|
457
|
+
|
|
458
|
+
## Done, return:
|
|
459
|
+
return result # type: ignore
|
|
460
|
+
|
|
461
|
+
def get_job_result(self, jobid: str) -> Tuple[bool, Optional[Any]]:
|
|
462
|
+
"""
|
|
463
|
+
Returns if the job is finished and associated data if so.
|
|
464
|
+
|
|
465
|
+
:param jobid: Job identifier.
|
|
466
|
+
:return: A tuple of status (``False`` meaning "pending", ``True`` meaning "finished") and optional content.
|
|
467
|
+
:raises APIServerError: In case that the status is unknown.
|
|
468
|
+
"""
|
|
469
|
+
## Get the job status:
|
|
470
|
+
content = self.request("GET", ["jobs", jobid])
|
|
471
|
+
|
|
472
|
+
## Get the state:
|
|
473
|
+
state = content["state"]
|
|
474
|
+
|
|
475
|
+
## Check:
|
|
476
|
+
if state == "PENDING":
|
|
477
|
+
return False, None
|
|
478
|
+
elif state == "SUCCESS":
|
|
479
|
+
return True, content["result"]
|
|
480
|
+
else:
|
|
481
|
+
raise APIServerError("Unknown response from Job status endpoint", -1, content)
|
|
482
|
+
|
|
483
|
+
@classmethod
|
|
484
|
+
def from_profile(cls, name: str, path: Optional[Path] = None) -> "Client":
|
|
485
|
+
"""
|
|
486
|
+
Attempts the create a :py:class:`Client` instance for the given profile name.
|
|
487
|
+
|
|
488
|
+
:param name: Profile name.
|
|
489
|
+
:param path: Optional path to the configuration file (default is ``~/.decaf.json``).
|
|
490
|
+
:return: A :py:class:`Client` instance.
|
|
491
|
+
:raises FileNotFoundError: In case that the configuration file is not found.
|
|
492
|
+
:raises KeyError: In case that the profile is not found.
|
|
493
|
+
"""
|
|
494
|
+
## If we don't have a configuration path, use the default:
|
|
495
|
+
if path is None:
|
|
496
|
+
path = Path.home() / ".decaf.json"
|
|
497
|
+
|
|
498
|
+
## Attempt to read in the configuration:
|
|
499
|
+
with path.open() as ifile:
|
|
500
|
+
profile = {p["name"]: p for p in loadjson(ifile)["profiles"]}[name]
|
|
501
|
+
|
|
502
|
+
## Build the client and return:
|
|
503
|
+
return Client(profile["url"], APIKeyAuthorization(key=profile["key"], scr=profile["secret"]))
|
|
504
|
+
|
|
505
|
+
@classmethod
|
|
506
|
+
def from_apikey(cls, url: str, key: str, secret: str) -> "Client":
|
|
507
|
+
"""
|
|
508
|
+
Attempts the create a :py:class:`Client` instance for the given API key/secret.
|
|
509
|
+
|
|
510
|
+
:param url: Base API url.
|
|
511
|
+
:param key: API key.
|
|
512
|
+
:param secret: API secret.
|
|
513
|
+
:return: A :py:class:`Client` instance.
|
|
514
|
+
"""
|
|
515
|
+
return Client(url, APIKeyAuthorization(key, secret))
|
|
516
|
+
|
|
517
|
+
@classmethod
|
|
518
|
+
def from_apitoken(cls, url: str, token: str) -> "Client":
|
|
519
|
+
"""
|
|
520
|
+
Attempts the create a :py:class:`Client` instance for the given API token.
|
|
521
|
+
|
|
522
|
+
Note that API tokens have shorter expiry than API keys. You may wish to choose API keys for clients with
|
|
523
|
+
longer life time.
|
|
524
|
+
|
|
525
|
+
:param url: Base API url.
|
|
526
|
+
:param token: API token.
|
|
527
|
+
:return: A :py:class:`Client` instance.
|
|
528
|
+
"""
|
|
529
|
+
return Client(url, APITokenAuthorization(token))
|
|
530
|
+
|
|
531
|
+
@classmethod
|
|
532
|
+
def from_headers(cls, url: str, headers: RHeaders) -> "Client":
|
|
533
|
+
"""
|
|
534
|
+
Attempts the create a :py:class:`Client` instance for the given HTTP headers.
|
|
535
|
+
|
|
536
|
+
:param url: Base API url.
|
|
537
|
+
:param headers: HTTP authentication headers.
|
|
538
|
+
:return: A :py:class:`Client` instance.
|
|
539
|
+
"""
|
|
540
|
+
return Client(url, APIHeaderAuthorization(headers))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
#: Defines a generic type alias.
|
|
544
|
+
_T = TypeVar("_T")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def query(func: Callable[..., _T]) -> Callable[..., _T]:
|
|
548
|
+
"""
|
|
549
|
+
Decorates a query method of an endpoint.
|
|
550
|
+
|
|
551
|
+
:param func: method to be decorated.
|
|
552
|
+
:return: Decorated method.
|
|
553
|
+
"""
|
|
554
|
+
setattr(func, "__decaf_endpoint_method_query__", True)
|
|
555
|
+
return func
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def command(func: Callable[..., _T]) -> Callable[..., _T]:
|
|
559
|
+
"""
|
|
560
|
+
Decorates a command method of an endpoint.
|
|
561
|
+
|
|
562
|
+
:param func: method to be decorated.
|
|
563
|
+
:return: Decorated method.
|
|
564
|
+
"""
|
|
565
|
+
setattr(func, "__decaf_endpoint_method_command__", True)
|
|
566
|
+
return func
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class BaseResource(BaseModel):
|
|
570
|
+
"""
|
|
571
|
+
Provides a base class for standard DECAF resource models.
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
pass
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class BaseCreateForm(BaseModel):
|
|
578
|
+
"""
|
|
579
|
+
Provides a base class for standard DECAF form models for resource creation.
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
pass
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class BaseUpdateForm(BaseModel):
|
|
586
|
+
"""
|
|
587
|
+
Provides a base class for standard DECAF form models for resource updation.
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class BasePatchForm(BaseModel):
|
|
594
|
+
"""
|
|
595
|
+
Provides a base class for standard DECAF form models for resource patching.
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
pass
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
#: Defines a type variable for :py:class:`BaseResource` implementations.
|
|
602
|
+
_R = TypeVar("_R", bound=BaseResource)
|
|
603
|
+
|
|
604
|
+
#: Defines a type variable for :py:class:`BaseCreateForm` implementations.
|
|
605
|
+
_C = TypeVar("_C", bound=BaseCreateForm)
|
|
606
|
+
|
|
607
|
+
#: Defines a type variable for :py:class:`BaseUpdateForm` implementations.
|
|
608
|
+
_U = TypeVar("_U", bound=BaseUpdateForm)
|
|
609
|
+
|
|
610
|
+
#: Defines a type variable for :py:class:`BasePatchForm` implementations.
|
|
611
|
+
_P = TypeVar("_P", bound=BasePatchForm)
|
|
612
|
+
|
|
613
|
+
#: Defines a type variable for resource identifiers.
|
|
614
|
+
_I = TypeVar("_I", bound=Union[str, int])
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@dataclass(frozen=True)
|
|
618
|
+
class Endpoint:
|
|
619
|
+
"""
|
|
620
|
+
Provides a base endpoint class.
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
__slots__ = ["client"]
|
|
624
|
+
|
|
625
|
+
#: Underlying client.
|
|
626
|
+
client: Client
|
|
627
|
+
|
|
628
|
+
#: Base path segment for the remote resource URI.
|
|
629
|
+
endpoint: ClassVar[str]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class ResourceEndpoint(Generic[_R], Endpoint):
|
|
633
|
+
"""
|
|
634
|
+
Provides a base class for resource endpoints.
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
#: Resource model type.
|
|
638
|
+
resource: ClassVar[Type[_R]] # type: ignore
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class ResourceListEndpoint(ResourceEndpoint[_R]):
|
|
642
|
+
"""
|
|
643
|
+
Provides a base class for resource endpoints with ``list`` query method.
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
@query
|
|
647
|
+
def list(self, params: Optional[RParams] = None) -> Iterable[_R]:
|
|
648
|
+
"""
|
|
649
|
+
Lists resources.
|
|
650
|
+
|
|
651
|
+
:param params: Query parameters.
|
|
652
|
+
:return: An iterable of resources.
|
|
653
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
654
|
+
:raises APIServerError: In case that there is a server generated error reported via HTTP status codes.
|
|
655
|
+
"""
|
|
656
|
+
return (
|
|
657
|
+
self.resource(**i)
|
|
658
|
+
for i in self.client.request("GET", self.endpoint, params={**(params or {}), "page_size": "-1"})
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class ResourceRetrieveEndpoint(Generic[_I, _R], ResourceEndpoint[_R]):
|
|
663
|
+
"""
|
|
664
|
+
Provides a base class for resource endpoints with ``retrieve`` query method.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
@query
|
|
668
|
+
def retrieve(self, ident: _I) -> Optional[_R]:
|
|
669
|
+
"""
|
|
670
|
+
Attempts to retrieve the resource identified by the given identifier.
|
|
671
|
+
|
|
672
|
+
:param ident: Resource identifier.
|
|
673
|
+
:return: Resource if found, ``None`` otherwise.
|
|
674
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
675
|
+
:raises APIServerError: In case that there is a server generated error reported via HTTP status codes.
|
|
676
|
+
"""
|
|
677
|
+
try:
|
|
678
|
+
return self.resource(**self.client.request("GET", [self.endpoint, ident]))
|
|
679
|
+
except APIServerErrorNotFound:
|
|
680
|
+
return None
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class ResourceCreateEndpoint(Generic[_C, _R], ResourceEndpoint[_R]):
|
|
684
|
+
"""
|
|
685
|
+
Provides a base class for resource endpoints with ``create`` command method.
|
|
686
|
+
"""
|
|
687
|
+
|
|
688
|
+
@command
|
|
689
|
+
def create(self, form: _C) -> _R:
|
|
690
|
+
"""
|
|
691
|
+
Attempts to create a new resource with the given form data.
|
|
692
|
+
|
|
693
|
+
:param form: Resource create form instance.
|
|
694
|
+
:return: Resource if successfully created.
|
|
695
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
696
|
+
:raises APIServerError: In case that there is a server generated error reported via HTTP status codes.
|
|
697
|
+
"""
|
|
698
|
+
return self.resource(**self.client.request("POST", self.endpoint, json=_form_payload(form)))
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class ResourceUpdateEndpoint(Generic[_I, _U, _R], ResourceEndpoint[_R]):
|
|
702
|
+
"""
|
|
703
|
+
Provides a base class for resource endpoints with ``update`` command method.
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
@command
|
|
707
|
+
def update(self, ident: _I, form: _U) -> _R:
|
|
708
|
+
"""
|
|
709
|
+
Attempts to update an existing resource with the given form data.
|
|
710
|
+
|
|
711
|
+
:param ident: Identifier of the resource to update.
|
|
712
|
+
:param form: Resource update form instance.
|
|
713
|
+
:return: Resource if successfully updated.
|
|
714
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
715
|
+
:raises APIServerError: In case that there is a server generated error reported via HTTP status codes.
|
|
716
|
+
"""
|
|
717
|
+
return self.resource(**self.client.request("PUT", [self.endpoint, ident], json=_form_payload(form)))
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
class ResourcePatchEndpoint(Generic[_I, _P, _R], ResourceEndpoint[_R]):
|
|
721
|
+
"""
|
|
722
|
+
Provides a base class for resource endpoints with ``patch`` command method.
|
|
723
|
+
"""
|
|
724
|
+
|
|
725
|
+
@command
|
|
726
|
+
def patch(self, ident: _I, form: _P) -> _R:
|
|
727
|
+
"""
|
|
728
|
+
Attempts to patch an existing resource with the given form data.
|
|
729
|
+
|
|
730
|
+
:param ident: Identifier of the resource to patch.
|
|
731
|
+
:param form: Resource patch form instance.
|
|
732
|
+
:return: Resource if successfully patched.
|
|
733
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
734
|
+
:raises APIServerError: In case that there is a server generated error reported via HTTP status codes.
|
|
735
|
+
"""
|
|
736
|
+
return self.resource(**self.client.request("PATCH", [self.endpoint, ident], json=_form_payload(form)))
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class DeleteEndpoint(Generic[_I], Endpoint):
|
|
740
|
+
"""
|
|
741
|
+
Provides a base class for resource endpoints with ``delete`` command method.
|
|
742
|
+
"""
|
|
743
|
+
|
|
744
|
+
@command
|
|
745
|
+
def delete(self, ident: _I) -> None:
|
|
746
|
+
"""
|
|
747
|
+
Attempts to delete the resource identified by the given identifier.
|
|
748
|
+
|
|
749
|
+
:param ident: Identifier of the resource to delete.
|
|
750
|
+
:raises APIClientError: In case that there is a problem with client-server communication.
|
|
751
|
+
:raises APIServerError: In case that there is a server generated error reported via HTTP status codes.
|
|
752
|
+
"""
|
|
753
|
+
self.client.request("DELETE", [self.endpoint, ident])
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
class StandardResourceEndpoint(
|
|
757
|
+
Generic[_I, _C, _U, _P, _R],
|
|
758
|
+
ResourceListEndpoint[_R],
|
|
759
|
+
ResourceRetrieveEndpoint[_I, _R],
|
|
760
|
+
ResourceCreateEndpoint[_C, _R],
|
|
761
|
+
ResourceUpdateEndpoint[_I, _U, _R],
|
|
762
|
+
ResourcePatchEndpoint[_I, _P, _R],
|
|
763
|
+
DeleteEndpoint[_I],
|
|
764
|
+
ResourceEndpoint[_R],
|
|
765
|
+
):
|
|
766
|
+
"""
|
|
767
|
+
Provides a generic base endpoint class definition for standard resources with listing, retrieval, creation,
|
|
768
|
+
updation, patching and deletion queries/commands.
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
pass
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _form_payload(form: Union[BaseCreateForm, BaseUpdateForm, BasePatchForm]) -> Dict[str, Any]:
|
|
775
|
+
"""
|
|
776
|
+
Compiles HTTP payload from the given form instance.
|
|
777
|
+
|
|
778
|
+
This function omits missing values.
|
|
779
|
+
|
|
780
|
+
:param form: Form instance.
|
|
781
|
+
:return: A dictionary of form field name and corresponding value.
|
|
782
|
+
"""
|
|
783
|
+
return {k: v for k, v in form if not isinstance(v, Missing)}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
class Missing:
|
|
787
|
+
"""
|
|
788
|
+
Provides a placeholder value for missing field values.
|
|
789
|
+
"""
|
|
790
|
+
|
|
791
|
+
@classmethod
|
|
792
|
+
def field(cls) -> Any:
|
|
793
|
+
"""
|
|
794
|
+
Provides a convenience method for creating a ``pydantic`` field for fields defaulting to missing value.
|
|
795
|
+
|
|
796
|
+
:return: :py:class:`Field` with missing value as default.
|
|
797
|
+
"""
|
|
798
|
+
return Field(cls())
|
|
799
|
+
|
|
800
|
+
@classmethod
|
|
801
|
+
def __get_validators__(cls) -> Generator[Any, None, None]:
|
|
802
|
+
"""
|
|
803
|
+
Makes this class a first class ``pydantic`` field value class.
|
|
804
|
+
|
|
805
|
+
:return: Validators.
|
|
806
|
+
"""
|
|
807
|
+
yield cls.validate
|
|
808
|
+
|
|
809
|
+
@staticmethod
|
|
810
|
+
def validate(v: Any) -> "Missing":
|
|
811
|
+
"""
|
|
812
|
+
Checks if the given value is a :py:class:`Missing` instance and returns it as is if so.
|
|
813
|
+
|
|
814
|
+
:param v: Value to be checked.
|
|
815
|
+
:return: Value.
|
|
816
|
+
:raises TypeError: In case that the given value is not a :py:class:`Missing` instance.
|
|
817
|
+
"""
|
|
818
|
+
if isinstance(v, Missing):
|
|
819
|
+
return v
|
|
820
|
+
raise TypeError("'Missing' instance required")
|
|
File without changes
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides common type definitions along with a comprehensive resource identifier catalogue.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AccountId",
|
|
7
|
+
"AccountMngtFeeSchemeId",
|
|
8
|
+
"AccrualScheduleId",
|
|
9
|
+
"ActionId",
|
|
10
|
+
"ActionTypeId",
|
|
11
|
+
"AgentId",
|
|
12
|
+
"AnalyticalTypeId",
|
|
13
|
+
"ArtifactId",
|
|
14
|
+
"ArtifactTypeId",
|
|
15
|
+
"AssetClassId",
|
|
16
|
+
"ContainerType",
|
|
17
|
+
"CountryId",
|
|
18
|
+
"Currency",
|
|
19
|
+
"Date",
|
|
20
|
+
"DateTime",
|
|
21
|
+
"DateType",
|
|
22
|
+
"GUID",
|
|
23
|
+
"GroupId",
|
|
24
|
+
"InstitutionId",
|
|
25
|
+
"OHLCId",
|
|
26
|
+
"PortfolioGroupId",
|
|
27
|
+
"PortfolioId",
|
|
28
|
+
"QuantId",
|
|
29
|
+
"RiskProfileId",
|
|
30
|
+
"ShareClassId",
|
|
31
|
+
"SharingId",
|
|
32
|
+
"StrategyId",
|
|
33
|
+
"Tags",
|
|
34
|
+
"TeamId",
|
|
35
|
+
"TimeDelta",
|
|
36
|
+
"UserId",
|
|
37
|
+
"_LaterI",
|
|
38
|
+
"_LaterS",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
from datetime import date as Date
|
|
42
|
+
from datetime import datetime as DateTime
|
|
43
|
+
from datetime import timedelta as TimeDelta
|
|
44
|
+
from enum import Enum
|
|
45
|
+
from typing import NewType, Set
|
|
46
|
+
|
|
47
|
+
#: Defines a type alias for globally unique resource identifiers.
|
|
48
|
+
GUID = NewType("GUID", str)
|
|
49
|
+
|
|
50
|
+
#: Defines a new-type for currency codes.
|
|
51
|
+
Currency = NewType("Currency", str)
|
|
52
|
+
|
|
53
|
+
#: Defines a type alias for standard set of tags.
|
|
54
|
+
Tags = Set[str]
|
|
55
|
+
|
|
56
|
+
#: Defines a new-type for DECAF quant resource identifier.
|
|
57
|
+
QuantId = NewType("QuantId", int)
|
|
58
|
+
|
|
59
|
+
#: Defines a new-type for DECAF artifact resource identifier.
|
|
60
|
+
ArtifactId = NewType("ArtifactId", int)
|
|
61
|
+
|
|
62
|
+
#: Defines a new-type for DECAF artifact type resource identifier.
|
|
63
|
+
ArtifactTypeId = NewType("ArtifactTypeId", str)
|
|
64
|
+
|
|
65
|
+
#: Defines a new-type for DECAF action resource identifier.
|
|
66
|
+
ActionId = NewType("ActionId", int)
|
|
67
|
+
|
|
68
|
+
#: Defines a new-type for DECAF action type resource identifier.
|
|
69
|
+
ActionTypeId = NewType("ActionTypeId", int)
|
|
70
|
+
|
|
71
|
+
#: Defines a new-type for DECAF account resource identifier.
|
|
72
|
+
AccountId = NewType("AccountId", int)
|
|
73
|
+
|
|
74
|
+
#: Defines a new-type for DECAF agent resource identifier.
|
|
75
|
+
AgentId = NewType("AgentId", int)
|
|
76
|
+
|
|
77
|
+
#: Defines a new-type for DECAF institution resource identifier.
|
|
78
|
+
InstitutionId = NewType("InstitutionId", int)
|
|
79
|
+
|
|
80
|
+
#: Defines a new-type for DECAF strategy resource identifier.
|
|
81
|
+
StrategyId = NewType("StrategyId", int)
|
|
82
|
+
|
|
83
|
+
#: Defines a new-type for DECAF risk profile resource identifier.
|
|
84
|
+
RiskProfileId = NewType("RiskProfileId", int)
|
|
85
|
+
|
|
86
|
+
#: Defines a new-type for DECAF asset class resource identifier.
|
|
87
|
+
AssetClassId = NewType("AssetClassId", int)
|
|
88
|
+
|
|
89
|
+
#: Defines a new-type for DECAF portfolio resource identifier.
|
|
90
|
+
PortfolioId = NewType("PortfolioId", int)
|
|
91
|
+
|
|
92
|
+
#: Defines a new-type for DECAF account management fee scheme resource identifier.
|
|
93
|
+
AccountMngtFeeSchemeId = NewType("AccountMngtFeeSchemeId", int)
|
|
94
|
+
|
|
95
|
+
#: Defines a new-type for DECAF analytical type resource identifier.
|
|
96
|
+
AnalyticalTypeId = NewType("AnalyticalTypeId", int)
|
|
97
|
+
|
|
98
|
+
#: Defines a new-type for DECAF institution sharing resource identifier.
|
|
99
|
+
SharingId = NewType("SharingId", int)
|
|
100
|
+
|
|
101
|
+
#: Defines a new-type for DECAF user resource identifier.
|
|
102
|
+
UserId = NewType("UserId", int)
|
|
103
|
+
|
|
104
|
+
#: Defines a new-type for DECAF team resource identifier.
|
|
105
|
+
TeamId = NewType("TeamId", int)
|
|
106
|
+
|
|
107
|
+
#: Defines a new-type for DECAF group resource identifier.
|
|
108
|
+
GroupId = NewType("GroupId", int)
|
|
109
|
+
|
|
110
|
+
#: Defines a new-type for DECAF portfolio group resource identifier.
|
|
111
|
+
PortfolioGroupId = NewType("PortfolioGroupId", int)
|
|
112
|
+
|
|
113
|
+
#: Defines a new-type for DECAF country resource identifier.
|
|
114
|
+
CountryId = NewType("CountryId", str)
|
|
115
|
+
|
|
116
|
+
#: Defines a new-type for DECAF OHLC series resource identifier.
|
|
117
|
+
OHLCId = NewType("OHLCId", int)
|
|
118
|
+
|
|
119
|
+
#: Defines a new-type for DECAF share class resource identifier.
|
|
120
|
+
ShareClassId = NewType("ShareClassId", int)
|
|
121
|
+
|
|
122
|
+
#: Defines a new-type for DECAF accrual schedule resource identifier.
|
|
123
|
+
AccrualScheduleId = NewType("AccrualScheduleId", int)
|
|
124
|
+
|
|
125
|
+
#: Defines a new-type for DECAF resource identifiers which are yet to be declared (of :py:class:`int` type).
|
|
126
|
+
_LaterI = NewType("_LaterI", int)
|
|
127
|
+
|
|
128
|
+
#: Defines a new-type for DECAF resource identifiers which are yet to be declared (of :py:class:`str` type).
|
|
129
|
+
_LaterS = NewType("_LaterS", str)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ContainerType(Enum):
|
|
133
|
+
"""
|
|
134
|
+
Provides container types.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
Account = "account"
|
|
138
|
+
Portfolio = "portfolio"
|
|
139
|
+
Institution = "institution"
|
|
140
|
+
Team = "team"
|
|
141
|
+
PortfolioGroup = "portfoliogroup"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class DateType(Enum):
|
|
145
|
+
"""
|
|
146
|
+
Provides date types.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
Commitment = "commitment"
|
|
150
|
+
Settlement = "settlement"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides data, type and function definitions which could be useful on import-side. Some of there are used
|
|
3
|
+
internally in this library, too.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"WithId",
|
|
8
|
+
"identity",
|
|
9
|
+
"make_lookup_table",
|
|
10
|
+
"parse_isodate",
|
|
11
|
+
"percentify",
|
|
12
|
+
"remove_microseconds",
|
|
13
|
+
"skipnone",
|
|
14
|
+
"sort_and_group_by",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
from decimal import Decimal
|
|
18
|
+
from itertools import groupby
|
|
19
|
+
from typing import Callable, Dict, Iterable, Optional, Protocol, Tuple, TypeVar, Union
|
|
20
|
+
|
|
21
|
+
from .types import Date, DateTime
|
|
22
|
+
|
|
23
|
+
#: Defines a generic type alias.
|
|
24
|
+
_T = TypeVar("_T")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def identity(x: _T) -> _T:
|
|
28
|
+
"""
|
|
29
|
+
Provides an identity function.
|
|
30
|
+
|
|
31
|
+
Return value type is inferred the same as the parameter type.
|
|
32
|
+
|
|
33
|
+
:param x: Value.
|
|
34
|
+
:return: Value.
|
|
35
|
+
|
|
36
|
+
>>> identity(1)
|
|
37
|
+
1
|
|
38
|
+
>>> identity("value")
|
|
39
|
+
'value'
|
|
40
|
+
"""
|
|
41
|
+
return x
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def skipnone(func: Callable[[_T], _T]) -> Callable[[Optional[_T]], Optional[_T]]:
|
|
45
|
+
"""
|
|
46
|
+
Provides a function decorator for avoiding function application if the value is ``None``.
|
|
47
|
+
|
|
48
|
+
:param func: Function to decorate.
|
|
49
|
+
:return: Decorated function
|
|
50
|
+
|
|
51
|
+
>>> add1: Callable[[int], int] = lambda x: x + 1
|
|
52
|
+
>>> add1(1)
|
|
53
|
+
2
|
|
54
|
+
>>> add1(None)
|
|
55
|
+
Traceback (most recent call last):
|
|
56
|
+
...
|
|
57
|
+
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
|
|
58
|
+
>>> skipnone(add1)(1)
|
|
59
|
+
2
|
|
60
|
+
>>> skipnone(add1)(None)
|
|
61
|
+
>>>
|
|
62
|
+
|
|
63
|
+
You can define a new function using the decorator (note the function signature):
|
|
64
|
+
|
|
65
|
+
>>> safe_add1: Callable[[Optional[int]], Optional[int]] = skipnone(add1)
|
|
66
|
+
>>> safe_add1(1)
|
|
67
|
+
2
|
|
68
|
+
>>> safe_add1(None)
|
|
69
|
+
>>>
|
|
70
|
+
|
|
71
|
+
... or decorate functions as such:
|
|
72
|
+
|
|
73
|
+
>>> @skipnone
|
|
74
|
+
... def double(x: int) -> int:
|
|
75
|
+
... return x * 2
|
|
76
|
+
>>> double(1)
|
|
77
|
+
2
|
|
78
|
+
>>> double(None)
|
|
79
|
+
>>>
|
|
80
|
+
"""
|
|
81
|
+
return lambda x: x if x is None else func(x)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def remove_microseconds(x: DateTime) -> DateTime:
|
|
85
|
+
"""
|
|
86
|
+
Removes microseconds from the given :py:class:`datetime.datetime` instance.
|
|
87
|
+
|
|
88
|
+
:param x: :py:class:`datetime.datetime` instance
|
|
89
|
+
:return: A new :py:class:`datetime.datetime` instance with microseconds set to ``0``.
|
|
90
|
+
|
|
91
|
+
>>> DateTime(2019, 1, 2, 3, 4, 5, 6789)
|
|
92
|
+
datetime.datetime(2019, 1, 2, 3, 4, 5, 6789)
|
|
93
|
+
>>> remove_microseconds(DateTime(2019, 1, 2, 3, 4, 5, 6789))
|
|
94
|
+
datetime.datetime(2019, 1, 2, 3, 4, 5)
|
|
95
|
+
"""
|
|
96
|
+
return x.replace(microsecond=0)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def percentify(x: Union[float, int, str, Decimal]) -> Decimal:
|
|
100
|
+
"""
|
|
101
|
+
Consumes a value representing a number and returns its percentage points as a :py:class:`Decimal` instance.
|
|
102
|
+
|
|
103
|
+
:param x: A value representing a number.
|
|
104
|
+
:return: Percentage points as a :py:class:`Decimal` instance.
|
|
105
|
+
:raises decimal.InvalidOperation: In case that parameter can not be converted to valid :py:class:`Decimal` instance.
|
|
106
|
+
|
|
107
|
+
>>> percentify(0.01)
|
|
108
|
+
Decimal('1.00')
|
|
109
|
+
>>> percentify(0)
|
|
110
|
+
Decimal('0')
|
|
111
|
+
>>> percentify("0.01")
|
|
112
|
+
Decimal('1.00')
|
|
113
|
+
>>> percentify(Decimal("0.01"))
|
|
114
|
+
Decimal('1.00')
|
|
115
|
+
>>> percentify(1.0)
|
|
116
|
+
Decimal('100.0')
|
|
117
|
+
>>> percentify(1)
|
|
118
|
+
Decimal('100')
|
|
119
|
+
>>> percentify("1")
|
|
120
|
+
Decimal('100')
|
|
121
|
+
>>> percentify(Decimal(1))
|
|
122
|
+
Decimal('100')
|
|
123
|
+
>>> percentify("xyz")
|
|
124
|
+
Traceback (most recent call last):
|
|
125
|
+
...
|
|
126
|
+
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
|
|
127
|
+
"""
|
|
128
|
+
return Decimal(str(x)) * Decimal(100)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_isodate(x: str) -> Date:
|
|
132
|
+
"""
|
|
133
|
+
Attempts to parse an ISO formatted date string and returns it as a :py:class:`datetime.date` instance.
|
|
134
|
+
|
|
135
|
+
:param x: ISO formatted date string
|
|
136
|
+
:return: A :py:class:`datetime.date` instance, if parsing is successful.
|
|
137
|
+
:raises ValueError: If the parameter does not represent a valid date or is not formatted as a valid ISO date string.
|
|
138
|
+
|
|
139
|
+
>>> parse_isodate("2019-01-02")
|
|
140
|
+
datetime.date(2019, 1, 2)
|
|
141
|
+
>>> parse_isodate("2019-02-29")
|
|
142
|
+
Traceback (most recent call last):
|
|
143
|
+
...
|
|
144
|
+
ValueError: day is out of range for month
|
|
145
|
+
>>> parse_isodate("20190228")
|
|
146
|
+
Traceback (most recent call last):
|
|
147
|
+
...
|
|
148
|
+
ValueError: time data '20190228' does not match format '%Y-%m-%d'
|
|
149
|
+
"""
|
|
150
|
+
return DateTime.strptime(x, "%Y-%m-%d").date()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class WithId(Protocol[_T]):
|
|
154
|
+
"""
|
|
155
|
+
Provides a generic protocol for models with ``id`` attribute.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
#: Unique identifier of the instance.
|
|
159
|
+
id: _T
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def make_lookup_table(records: Iterable[WithId[_T]]) -> Dict[_T, WithId[_T]]:
|
|
163
|
+
"""
|
|
164
|
+
Creates a lookup table from a iterable of records following :py:class:`WithId` protocol.
|
|
165
|
+
|
|
166
|
+
:param records: Iterable of records.
|
|
167
|
+
:return: A lookup table of record id to record.
|
|
168
|
+
|
|
169
|
+
>>> make_lookup_table([])
|
|
170
|
+
{}
|
|
171
|
+
>>> from dataclasses import dataclass
|
|
172
|
+
>>> @dataclass
|
|
173
|
+
... class A:
|
|
174
|
+
... id: int
|
|
175
|
+
... value: bool
|
|
176
|
+
>>> records = [A(1, True), A(2, False)]
|
|
177
|
+
>>> table = make_lookup_table(records)
|
|
178
|
+
>>> table[1].value
|
|
179
|
+
True
|
|
180
|
+
>>> table[2].value
|
|
181
|
+
False
|
|
182
|
+
"""
|
|
183
|
+
return {r.id: r for r in records}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
#: Defines a generic type alias.
|
|
187
|
+
_K = TypeVar("_K")
|
|
188
|
+
|
|
189
|
+
#: Defines a generic type alias.
|
|
190
|
+
_V = TypeVar("_V")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def sort_and_group_by(x: Iterable[_V], key: Callable[[_V], _K]) -> Iterable[Tuple[_K, Iterable[_V]]]:
|
|
194
|
+
"""
|
|
195
|
+
Sorts and groups an iterable of values by the same key.
|
|
196
|
+
|
|
197
|
+
:param x: Iterable of values.
|
|
198
|
+
:param key: Function to compute the key from a given value.
|
|
199
|
+
:return: An iterable of tuples of key and iterable of values for the key.
|
|
200
|
+
"""
|
|
201
|
+
return groupby(sorted(x, key=key), key=key) # type: ignore
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Copyright (2017-2019) Vehbi Sinan Tunalioglu.
|
|
2
|
+
Copyright (2019-2024) Teloscube Pte Ltd.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are
|
|
6
|
+
met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright
|
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright
|
|
12
|
+
notice, this list of conditions and the following disclaimer in the
|
|
13
|
+
documentation and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
20
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
21
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
22
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
23
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
24
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
25
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
26
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
27
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
28
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: decaf-api-client
|
|
3
|
+
Version: 0.0.12
|
|
4
|
+
Summary: DECAF API Client
|
|
5
|
+
Author-email: Vehbi Sinan Tunalioglu <vst@vsthost.com>
|
|
6
|
+
Maintainer-email: Vehbi Sinan Tunalioglu <vst@vsthost.com>
|
|
7
|
+
License: Copyright (2017-2019) Vehbi Sinan Tunalioglu.
|
|
8
|
+
Copyright (2019-2024) Teloscube Pte Ltd.
|
|
9
|
+
|
|
10
|
+
Redistribution and use in source and binary forms, with or without
|
|
11
|
+
modification, are permitted provided that the following conditions are
|
|
12
|
+
met:
|
|
13
|
+
|
|
14
|
+
1. Redistributions of source code must retain the above copyright
|
|
15
|
+
notice, this list of conditions and the following disclaimer.
|
|
16
|
+
|
|
17
|
+
2. Redistributions in binary form must reproduce the above copyright
|
|
18
|
+
notice, this list of conditions and the following disclaimer in the
|
|
19
|
+
documentation and/or other materials provided with the distribution.
|
|
20
|
+
|
|
21
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
22
|
+
contributors may be used to endorse or promote products derived from
|
|
23
|
+
this software without specific prior written permission.
|
|
24
|
+
|
|
25
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
26
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
27
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
28
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
29
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
30
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
31
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
32
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
33
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
34
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
35
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
+
|
|
37
|
+
Project-URL: homepage, https://github.com/telostat/decaf-api-client-python
|
|
38
|
+
Project-URL: documentation, https://github.com/telostat/decaf-api-client-python
|
|
39
|
+
Project-URL: repository, https://github.com/telostat/decaf-api-client-python
|
|
40
|
+
Project-URL: changelog, https://github.com/teloscube/decaf-client-python/blob/main/CHANGELOG.md
|
|
41
|
+
Keywords: decaf,api,client,finance
|
|
42
|
+
Classifier: Development Status :: 3 - Alpha
|
|
43
|
+
Classifier: Intended Audience :: Developers
|
|
44
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
45
|
+
Classifier: Programming Language :: Python :: 3
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
49
|
+
Requires-Python: >=3.10
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
License-File: LICENSE
|
|
52
|
+
Requires-Dist: pydantic <3,>=2
|
|
53
|
+
Requires-Dist: requests <3,>=2
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: build ; extra == 'dev'
|
|
56
|
+
Requires-Dist: ipython ; extra == 'dev'
|
|
57
|
+
Requires-Dist: pip ; extra == 'dev'
|
|
58
|
+
Requires-Dist: pyls-flake8 ; extra == 'dev'
|
|
59
|
+
Requires-Dist: pyls-isort ; extra == 'dev'
|
|
60
|
+
Requires-Dist: pylsp-mypy ; extra == 'dev'
|
|
61
|
+
Requires-Dist: pylsp-rope ; extra == 'dev'
|
|
62
|
+
Requires-Dist: python-lsp-black ; extra == 'dev'
|
|
63
|
+
Requires-Dist: python-lsp-server ; extra == 'dev'
|
|
64
|
+
Requires-Dist: rope ; extra == 'dev'
|
|
65
|
+
Requires-Dist: twine ; extra == 'dev'
|
|
66
|
+
Requires-Dist: wheel ; extra == 'dev'
|
|
67
|
+
Provides-Extra: test
|
|
68
|
+
Requires-Dist: black ; extra == 'test'
|
|
69
|
+
Requires-Dist: flake8 ; extra == 'test'
|
|
70
|
+
Requires-Dist: isort ; extra == 'test'
|
|
71
|
+
Requires-Dist: mypy ; extra == 'test'
|
|
72
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
73
|
+
Requires-Dist: types-requests ; extra == 'test'
|
|
74
|
+
|
|
75
|
+
# DECAF API Client (Python)
|
|
76
|
+
|
|
77
|
+
[](https://github.com/teloscube/decaf-client-python/actions/workflows/test.yml)
|
|
78
|
+
|
|
79
|
+
> **TODO:** Provide a complete README file.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
A Nix shell is provided for development:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
nix develop
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Build to run tests:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
nix build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Build and Publish
|
|
96
|
+
|
|
97
|
+
Build the source distribution:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
python -m build --sdist --wheel
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Check the distribution(s):
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
twine check dist/*
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Upload the distribution:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
twine upload -s dist/*
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License and Copyrights
|
|
116
|
+
|
|
117
|
+
This software is licensed under [The 3-Clause BSD License].
|
|
118
|
+
|
|
119
|
+
<!-- REFERENCES -->
|
|
120
|
+
|
|
121
|
+
[The 3-Clause BSD License]: https://opensource.org/licenses/BSD-3-Clause
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
decaf/api/client/__init__.py,sha256=WnlLgUAn6tUogLJNIlC1S2OVOLRJEEUFi5ZjJIKYxsA,111
|
|
2
|
+
decaf/api/client/_internal.py,sha256=p6m7MXUCr8SCZK_Lhc1BqO_mK3I7-Mlk0sGFTsPD_Dw,229
|
|
3
|
+
decaf/api/client/machinery.py,sha256=tNIS3bXqrjIAVDiLqC0zMwA4VZFAPYXtdHeGcJjx9iE,25068
|
|
4
|
+
decaf/api/client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
decaf/api/client/types.py,sha256=uur23IO8tNm09m1H6jblMhZk4g7OPHzn7bwnIlhmq6k,4231
|
|
6
|
+
decaf/api/client/utils.py,sha256=DmyuXOMo4wKpigklVjY8vPEXjoErcxjhROI3jfVLmE8,5458
|
|
7
|
+
decaf_api_client-0.0.12.dist-info/LICENSE,sha256=Xs-bwjHHT2CraHEgXAz_amdE2z9nZK-f3YozmxtztEY,1516
|
|
8
|
+
decaf_api_client-0.0.12.dist-info/METADATA,sha256=kvi-zkxOqFAJSu9iuRbXV1NOA9Oucp3LoHe3AW50CDw,4364
|
|
9
|
+
decaf_api_client-0.0.12.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
10
|
+
decaf_api_client-0.0.12.dist-info/top_level.txt,sha256=jBvLsYzSXodl4Brh1J9a99Rp7xluwM7Nelyml6gzdXU,6
|
|
11
|
+
decaf_api_client-0.0.12.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
decaf
|