cacholong-cloud-cli 0.3.0__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.
Files changed (39) hide show
  1. cacholong_cli/AsyncTyper.py +17 -0
  2. cacholong_cli/__init__.py +0 -0
  3. cacholong_cli/cli.py +110 -0
  4. cacholong_cli/commands/accounts.py +105 -0
  5. cacholong_cli/commands/addresses.py +280 -0
  6. cacholong_cli/commands/companies.py +100 -0
  7. cacholong_cli/commands/dns_records.py +275 -0
  8. cacholong_cli/commands/dns_templates.py +141 -0
  9. cacholong_cli/commands/dns_zones.py +277 -0
  10. cacholong_cli/commands/products.py +121 -0
  11. cacholong_cli/commands/purchases.py +127 -0
  12. cacholong_cli/common.py +179 -0
  13. cacholong_cli/connection.py +18 -0
  14. cacholong_cloud_cli-0.3.0.dist-info/METADATA +115 -0
  15. cacholong_cloud_cli-0.3.0.dist-info/RECORD +39 -0
  16. cacholong_cloud_cli-0.3.0.dist-info/WHEEL +4 -0
  17. cacholong_cloud_cli-0.3.0.dist-info/entry_points.txt +3 -0
  18. cacholong_sdk/__init__.py +18 -0
  19. cacholong_sdk/account.py +14 -0
  20. cacholong_sdk/address.py +14 -0
  21. cacholong_sdk/api_schema.py +124 -0
  22. cacholong_sdk/common.py +119 -0
  23. cacholong_sdk/company.py +14 -0
  24. cacholong_sdk/connection.py +22 -0
  25. cacholong_sdk/dns_record.py +14 -0
  26. cacholong_sdk/dns_template.py +14 -0
  27. cacholong_sdk/dns_zone.py +14 -0
  28. cacholong_sdk/exception.py +7 -0
  29. cacholong_sdk/product.py +14 -0
  30. cacholong_sdk/purchase.py +14 -0
  31. jsonapi_client/__init__.py +42 -0
  32. jsonapi_client/common.py +186 -0
  33. jsonapi_client/document.py +161 -0
  34. jsonapi_client/exceptions.py +62 -0
  35. jsonapi_client/filter.py +155 -0
  36. jsonapi_client/objects.py +187 -0
  37. jsonapi_client/relationships.py +447 -0
  38. jsonapi_client/resourceobject.py +656 -0
  39. jsonapi_client/session.py +755 -0
@@ -0,0 +1,755 @@
1
+ """
2
+ JSON API Python client
3
+ https://github.com/qvantel/jsonapi-client
4
+
5
+ (see JSON API specification in http://jsonapi.org/)
6
+
7
+ Copyright (c) 2017, Qvantel
8
+ All rights reserved.
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+ * Redistributions of source code must retain the above copyright
13
+ notice, this list of conditions and the following disclaimer.
14
+ * Redistributions in binary form must reproduce the above copyright
15
+ notice, this list of conditions and the following disclaimer in the
16
+ documentation and/or other materials provided with the distribution.
17
+ * Neither the name of the Qvantel nor the
18
+ names of its contributors may be used to endorse or promote products
19
+ derived from this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL QVANTEL BE LIABLE FOR ANY
25
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
28
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ """
32
+
33
+ import collections
34
+ import json
35
+ import logging
36
+ from itertools import chain
37
+ from typing import (TYPE_CHECKING, Set, Optional, Tuple, Dict, Union, Iterable,
38
+ AsyncIterable, Awaitable, AsyncIterator, Iterator, List)
39
+ from urllib.parse import ParseResult, urlparse
40
+
41
+ import jsonschema
42
+
43
+ from .common import jsonify_attribute_name, error_from_response, \
44
+ HttpStatus, HttpMethod
45
+ from .exceptions import DocumentError, AsyncError
46
+
47
+ if TYPE_CHECKING:
48
+ from asyncio import AbstractEventLoop
49
+ from .objects import ResourceIdentifier
50
+ from .document import Document
51
+ from .resourceobject import ResourceObject
52
+ from .relationships import ResourceTuple
53
+ from .filter import Modifier
54
+
55
+ import json
56
+ from uuid import UUID
57
+
58
+ class UUIDEncoder(json.JSONEncoder):
59
+ def default(self, obj):
60
+ if isinstance(obj, UUID):
61
+ return str(obj)
62
+ return json.JSONEncoder.default(self, obj)
63
+
64
+ logger = logging.getLogger(__name__)
65
+ NOT_FOUND = object()
66
+
67
+
68
+ class SessionHistory(list):
69
+ @property
70
+ def latest(self):
71
+ if len(self):
72
+ return self[-1]
73
+ else:
74
+ return None
75
+
76
+
77
+ class SessionHistoryItem:
78
+ def __init__(self, session: 'Session', url: str, http_method: str, response, send_json: dict=None):
79
+ self.session = session
80
+ self.response = response
81
+ self.url = url
82
+ self.send_json = send_json
83
+ self.http_method = http_method.upper()
84
+
85
+ def __repr__(self):
86
+ content = self.content
87
+ content_cut_after = 100
88
+ if len(content) > content_cut_after:
89
+ content = f"{content[:content_cut_after]}..."
90
+ request_str = f"Request: {self.url}\n method: {self.http_method}\n"
91
+ if self.send_json:
92
+ request_content = json.dumps(self.send_json)
93
+ if len(request_content) > content_cut_after:
94
+ request_content = f"{request_content[:content_cut_after]}..."
95
+ request_str += f" payload: {request_content}\n"
96
+ r = f"{request_str}" \
97
+ f"Response: \n status code: {self.status_code}\n" \
98
+ f" content length: {self.content_length}\n" \
99
+ f" content: {content}"
100
+ return r
101
+
102
+ @property
103
+ def content(self):
104
+ if self.session.enable_async:
105
+ return self.response._body
106
+ else:
107
+ return self.response.content
108
+
109
+ @property
110
+ def response_content(self):
111
+ """
112
+ This is used to pretty print the contents for debugging purposes.
113
+ If you don't want pretty print, please use self.response.content directly
114
+ Example: If session is s, you can pretty print out the latest content by
115
+ print(s.history.latest.content)
116
+ """
117
+ loaded = json.loads(self.content)
118
+ return json.dumps(loaded, indent=4, sort_keys=True)
119
+
120
+ @property
121
+ def payload(self):
122
+ return json.dumps(self.send_json, indent=4, sort_keys=True)
123
+
124
+ @property
125
+ def content_length(self):
126
+ return len(self.content)
127
+
128
+ @property
129
+ def headers(self):
130
+ return self.response.headers
131
+
132
+ @property
133
+ def status_code(self):
134
+ if self.session.enable_async:
135
+ return self.response.status
136
+ else:
137
+ return self.response.status_code
138
+
139
+
140
+ class Schema:
141
+ """
142
+ Container for model schemas with associated methods.
143
+ Session contains Schema.
144
+ """
145
+
146
+ def __init__(self, schema_data: dict=None) -> None:
147
+ self._schema_data = schema_data
148
+
149
+ def find_spec(self, model_name: str, attribute_name: str) -> dict:
150
+ """
151
+ Find specification from model_name for attribute_name which can
152
+ be nested format, i.e. 'attribute-group1.attribute-group2.attribute-item'
153
+ """
154
+
155
+ # We need to support meta, which can contain whatever schemaless metadata
156
+ if attribute_name == 'meta' or attribute_name.endswith('.meta'):
157
+ return {}
158
+
159
+ model = self.schema_for_model(model_name)
160
+ if not model:
161
+ return {}
162
+ if not attribute_name:
163
+ return model
164
+ attr_struct = attribute_name.split('.')
165
+ for a in attr_struct:
166
+ model = model['properties'].get(a, NOT_FOUND)
167
+ if model is NOT_FOUND:
168
+ return {}
169
+ return model
170
+
171
+ def add_model_schema(self, data: dict) -> None:
172
+ self._schema_data.update(data)
173
+
174
+ @property
175
+ def is_enabled(self):
176
+ return bool(self._schema_data)
177
+
178
+ def schema_for_model(self, model_type: str) -> dict:
179
+ return self._schema_data.get(model_type) if self.is_enabled else {}
180
+
181
+ def validate(self, model_type: str, data: dict) -> None:
182
+ """
183
+ Validate model data against schema.
184
+ """
185
+ schema = self.schema_for_model(model_type)
186
+ if not schema:
187
+ return
188
+ jsonschema.validate(data, schema)
189
+
190
+
191
+ class Session:
192
+ """
193
+ Resources are fetched and cached in a session.
194
+
195
+ :param server_url: Server base url
196
+ :param enable_async: Toggle AsyncIO mode for session
197
+ :param schema: Schema in jsonschema format. See example from :ref:`usage-schema`.
198
+ :param request_kwargs: Additional keyword arguments that are passed to requests.request or
199
+ aiohttp.request functions (such as authentication object)
200
+
201
+ """
202
+ def __init__(self, server_url: str=None,
203
+ enable_async: bool=False,
204
+ schema: dict=None,
205
+ request_kwargs: dict=None,
206
+ loop: 'AbstractEventLoop'=None,
207
+ use_relationship_iterator: bool=False,
208
+ enable_history: bool=False,
209
+ event_hooks: dict=None) -> None:
210
+ self._server: ParseResult
211
+ self.enable_async = enable_async
212
+
213
+ self._request_kwargs: dict = request_kwargs or {}
214
+
215
+ if server_url:
216
+ self._server = urlparse(server_url)
217
+ else:
218
+ self._server = None
219
+
220
+ self.resources_by_resource_identifier: \
221
+ 'Dict[Tuple[str, str], ResourceObject]' = {}
222
+ self.resources_by_link: 'Dict[str, ResourceObject]' = {}
223
+ self.documents_by_link: 'Dict[str, Document]' = {}
224
+ self.schema: Schema = Schema(schema)
225
+ if enable_async:
226
+ import aiohttp
227
+ self._prepare_async_event_hooks(event_hooks)
228
+ self._aiohttp_session = aiohttp.ClientSession(
229
+ loop=loop,
230
+ trace_configs=[self.trace_config]
231
+ )
232
+ else:
233
+ if event_hooks is not None:
234
+ hooks = self._request_kwargs.get('hooks', {})
235
+ hooks.update(**event_hooks)
236
+ self._request_kwargs['hooks'] = hooks
237
+ self.use_relationship_iterator = use_relationship_iterator
238
+ self.enable_history = enable_history
239
+ self.history = SessionHistory()
240
+
241
+ def _prepare_async_event_hooks(self, event_hooks: dict=None) -> None:
242
+ import aiohttp
243
+ self.trace_config = aiohttp.TraceConfig()
244
+ if event_hooks is None:
245
+ return
246
+
247
+ for event, hook in event_hooks.items():
248
+ getattr(self.trace_config, event).append(hook)
249
+
250
+ def add_resources(self, *resources: 'ResourceObject') -> None:
251
+ """
252
+ Add resources to session cache.
253
+ """
254
+ for res in resources:
255
+ self.resources_by_resource_identifier[(res.type, res.id)] = res
256
+ lnk = res.links.self.url if res.links.self else res.url
257
+ if lnk:
258
+ self.resources_by_link[lnk] = res
259
+
260
+ def remove_resource(self, res: 'ResourceObject') -> None:
261
+ """
262
+ Remove resource from session cache.
263
+
264
+ :param res: Resource to be removed
265
+ """
266
+ del self.resources_by_resource_identifier[(res.type, res.id)]
267
+ del self.resources_by_link[res.url]
268
+
269
+ @staticmethod
270
+ def _value_to_dict(value: 'Union[ResourceObject, ResourceIdentifier, ResourceTuple]',
271
+ res_types: 'List[str]') -> dict:
272
+ from .objects import RESOURCE_TYPES
273
+
274
+ res_type = res_types[0] if len(res_types) == 1 else None
275
+
276
+ if isinstance(value, RESOURCE_TYPES):
277
+ if res_type and value.type != res_type:
278
+ raise TypeError(f'Invalid resource type {value.type}. '
279
+ f'Should be {res_type}')
280
+ elif res_types and value.type not in res_types:
281
+ raise TypeError(f'Invalid resource type {value.type}. '
282
+ f'Should be one of {res_types}')
283
+ return {'id': value.id, 'type': value.type}
284
+ else:
285
+ if not res_type:
286
+ raise ValueError('Use ResourceTuple to identify types '
287
+ 'if there are more than 1 type')
288
+ return {'id': value, 'type': res_types[0]}
289
+
290
+ def create(self, _type: str, fields: dict=None, **more_fields) -> 'ResourceObject':
291
+ """
292
+ Create a new ResourceObject of model _type. This requires that schema is defined
293
+ for model.
294
+
295
+ If you have field names that have underscores, you can pass those fields
296
+ in fields dictionary.
297
+
298
+ """
299
+ from .objects import RESOURCE_TYPES
300
+ from .resourceobject import ResourceObject
301
+
302
+ if fields is None:
303
+ fields = {}
304
+
305
+ attrs: dict = {}
306
+ rels: dict = {}
307
+ schema = self.schema.schema_for_model(_type)
308
+ more_fields.update(fields)
309
+
310
+ for key, value in more_fields.items():
311
+ if key not in fields:
312
+ key = jsonify_attribute_name(key)
313
+ props = schema['properties'].get(key, {})
314
+ if 'relation' in props:
315
+ res_types = props['resource']
316
+ if isinstance(value, RESOURCE_TYPES + (str,)):
317
+ value = self._value_to_dict(value, res_types)
318
+ elif isinstance(value, collections.abc.Iterable):
319
+ value = [self._value_to_dict(id_, res_types) for id_ in value]
320
+ rels[key] = {'data': value}
321
+ else:
322
+ key = key.split('.')
323
+ a = attrs
324
+ for k in key[:-1]:
325
+ a_ = a[k] = a.get(k, {})
326
+ a = a_
327
+
328
+ a[key[-1]] = value
329
+
330
+ data = {'type': _type,
331
+ 'id': None,
332
+ 'attributes': attrs,
333
+ 'relationships': rels,
334
+ }
335
+
336
+ res = ResourceObject(self, data)
337
+ return res
338
+
339
+ def _create_and_commit_sync(self, type_: str, fields: dict=None, **more_fields) -> 'ResourceObject':
340
+ res = self.create(type_, fields, **more_fields)
341
+ res.commit()
342
+ return res
343
+
344
+ async def _create_and_commit_async(self, type_: str, fields: dict=None, **more_fields) -> 'ResourceObject':
345
+ res = self.create(type_, fields, **more_fields)
346
+ await res.commit()
347
+ return res
348
+
349
+ def create_and_commit(self, type_: str, fields: dict=None, **more_fields) \
350
+ -> 'Union[Awaitable[ResourceObject], ResourceObject]':
351
+ """
352
+ Create resource and commit (PUSH) it into server.
353
+ If session is used with enable_async=True, this needs
354
+ to be awaited.
355
+ """
356
+
357
+ if self.enable_async:
358
+ return self._create_and_commit_async(type_, fields, **more_fields)
359
+ else:
360
+ return self._create_and_commit_sync(type_, fields, **more_fields)
361
+
362
+ def __enter__(self):
363
+ self.assert_sync()
364
+ logger.info('Entering session')
365
+ return self
366
+
367
+ async def __aenter__(self):
368
+ self.assert_async()
369
+ logger.info('Entering session')
370
+ return self
371
+
372
+ def __exit__(self, exc_type, exc_val, exc_tb):
373
+ self.assert_sync()
374
+ logger.info('Exiting session')
375
+ if not exc_type:
376
+ self.commit()
377
+ self.close()
378
+
379
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
380
+ self.assert_async()
381
+ logger.info('Exiting session')
382
+ if not exc_type:
383
+ await self.commit()
384
+ await self.close()
385
+
386
+ def close(self):
387
+ """
388
+ Close session and invalidate resources.
389
+ """
390
+ self.invalidate()
391
+ if self.enable_async:
392
+ return self._aiohttp_session.close()
393
+
394
+ def invalidate(self):
395
+ """
396
+ Invalidate resources and documents associated with this Session.
397
+ """
398
+ for resource in chain(self.documents_by_link.values(),
399
+ self.resources_by_link.values(),
400
+ self.resources_by_resource_identifier.values()):
401
+ resource.mark_invalid()
402
+
403
+ self.documents_by_link.clear()
404
+ self.resources_by_link.clear()
405
+ self.resources_by_resource_identifier.clear()
406
+
407
+ @property
408
+ def server_url(self) -> str:
409
+ return f'{self._server.scheme}://{self._server.netloc}'
410
+
411
+ @property
412
+ def url_prefix(self) -> str:
413
+ return self._server.geturl().rstrip('/')
414
+
415
+ def _url_for_resource(self, resource_type: str,
416
+ resource_id: str=None,
417
+ filter: 'Modifier'=None) -> str:
418
+ url = f'{self.url_prefix}/{resource_type}'
419
+ if resource_id is not None:
420
+ url = f'{url}/{resource_id}'
421
+ if filter:
422
+ url = filter.url_with_modifiers(url)
423
+ return url
424
+
425
+ @staticmethod
426
+ def _resource_type_and_filter(
427
+ resource_id_or_filter: 'Union[Modifier, str]'=None)\
428
+ -> 'Tuple[Optional[str], Optional[Modifier]]':
429
+ from .filter import Modifier
430
+ if isinstance(resource_id_or_filter, Modifier):
431
+ resource_id = None
432
+ filter = resource_id_or_filter
433
+ else:
434
+ resource_id = resource_id_or_filter
435
+ filter = None
436
+ return resource_id, filter
437
+
438
+ def _get_sync(self, resource_type: str,
439
+ resource_id_or_filter: 'Union[Modifier, str]'=None) -> 'Document':
440
+ resource_id, filter_ = self._resource_type_and_filter(
441
+ resource_id_or_filter)
442
+ url = self._url_for_resource(resource_type, resource_id, filter_)
443
+ return self.fetch_document_by_url(url)
444
+
445
+ async def _get_async(self, resource_type: str,
446
+ resource_id_or_filter: 'Union[Modifier, str]'=None) -> 'Document':
447
+ resource_id, filter_ = self._resource_type_and_filter(
448
+ resource_id_or_filter)
449
+ url = self._url_for_resource(resource_type, resource_id, filter_)
450
+ return await self.fetch_document_by_url_async(url)
451
+
452
+ def get(self, resource_type: str,
453
+ resource_id_or_filter: 'Union[Modifier, str]'=None) \
454
+ -> 'Union[Awaitable[Document], Document]':
455
+ """
456
+ Request (GET) Document from server.
457
+
458
+ :param resource_id_or_filter: Resource id or Modifier instance to filter
459
+ resulting resources.
460
+
461
+ If session is used with enable_async=True, this needs
462
+ to be awaited.
463
+ """
464
+ if self.enable_async:
465
+ return self._get_async(resource_type, resource_id_or_filter)
466
+ else:
467
+ return self._get_sync(resource_type, resource_id_or_filter)
468
+
469
+ def _iterate_sync(self, resource_type: str, filter: 'Modifier'=None) \
470
+ -> 'Iterator[ResourceObject]':
471
+ doc = self.get(resource_type, filter)
472
+ yield from doc._iterator_sync()
473
+
474
+ async def _iterate_async(self, resource_type: str, filter: 'Modifier'=None) \
475
+ -> 'AsyncIterator[ResourceObject]':
476
+ doc = await self._get_async(resource_type, filter)
477
+ async for res in doc._iterator_async():
478
+ yield res
479
+
480
+ def iterate(self, resource_type: str, filter: 'Modifier'=None) \
481
+ -> 'Union[AsyncIterator[ResourceObject], Iterator[ResourceObject]]':
482
+ """
483
+ Request (GET) Document from server and iterate through resources.
484
+ If Document uses pagination, fetch results as long as there are new
485
+ results.
486
+
487
+ If session is used with enable_async=True, this needs to iterated with
488
+ async for.
489
+
490
+ :param filter: Modifier instance to filter resulting resources.
491
+ """
492
+ if self.enable_async:
493
+ return self._iterate_async(resource_type, filter)
494
+ else:
495
+ return self._iterate_sync(resource_type, filter)
496
+
497
+ def read(self, json_data: dict, url='', no_cache=False)-> 'Document':
498
+ """
499
+ Read document from json_data dictionary instead of fetching it from the server.
500
+
501
+ :param json_data: JSON API document as dictionary.
502
+ :param url: Set source url to resulting document.
503
+ :param no_cache: do not store results into Session's cache.
504
+ """
505
+ from .document import Document
506
+ doc = self.documents_by_link[url] = Document(self, json_data, url,
507
+ no_cache=no_cache)
508
+ return doc
509
+
510
+ def fetch_resource_by_resource_identifier(
511
+ self,
512
+ resource: 'Union[ResourceIdentifier, ResourceObject, ResourceTuple]',
513
+ cache_only=False,
514
+ force=False) -> 'Optional[ResourceObject]':
515
+ """
516
+ Internal use.
517
+
518
+ Fetch resource from server by resource identifier.
519
+ """
520
+ type_, id_ = resource.type, resource.id
521
+ new_res = not force and self.resources_by_resource_identifier.get((type_, id_))
522
+ if new_res:
523
+ return new_res
524
+ elif cache_only:
525
+ return None
526
+ else:
527
+ # Note: Document creation will add its resources to cache via .add_resources,
528
+ # no need to do it manually here
529
+ return self._ext_fetch_by_url(resource.url).resource
530
+
531
+ async def fetch_resource_by_resource_identifier_async(
532
+ self,
533
+ resource: 'Union[ResourceIdentifier, ResourceObject, ResourceTuple]',
534
+ cache_only=False,
535
+ force=False) -> 'Optional[ResourceObject]':
536
+ """
537
+ Internal use. Async version.
538
+
539
+ Fetch resource from server by resource identifier.
540
+ """
541
+ type_, id_ = resource.type, resource.id
542
+ new_res = not force and self.resources_by_resource_identifier.get((type_, id_))
543
+ if new_res:
544
+ return new_res
545
+ elif cache_only:
546
+ return None
547
+ else:
548
+ # Note: Document creation will add its resources to cache via .add_resources,
549
+ # no need to do it manually here
550
+ return (await self._ext_fetch_by_url_async(resource.url)).resource
551
+
552
+ def fetch_document_by_url(self, url: str) -> 'Document':
553
+ """
554
+ Internal use.
555
+
556
+ Fetch Document from server by url.
557
+ """
558
+
559
+ # TODO: should we try to guess type, id from url?
560
+ return self.documents_by_link.get(url) or self._ext_fetch_by_url(url)
561
+
562
+ async def fetch_document_by_url_async(self, url: str) -> 'Document':
563
+ """
564
+ Internal use. Async version.
565
+
566
+ Fetch Document from server by url.
567
+ """
568
+
569
+ # TODO: should we try to guess type, id from url?
570
+ return (self.documents_by_link.get(url) or
571
+ await self._ext_fetch_by_url_async(url))
572
+
573
+ def _ext_fetch_by_url(self, url: str) -> 'Document':
574
+ json_data = self._fetch_json(url)
575
+ return self.read(json_data, url)
576
+
577
+ async def _ext_fetch_by_url_async(self, url: str) -> 'Document':
578
+ json_data = await self._fetch_json_async(url)
579
+ return self.read(json_data, url)
580
+
581
+ def _append_to_session_history(self, url: str, http_method: str,
582
+ response, send_json: dict=None):
583
+ if self.enable_history:
584
+ self.history.append(
585
+ SessionHistoryItem(self, url, http_method, response, send_json)
586
+ )
587
+
588
+ def _fetch_json(self, url: str) -> dict:
589
+ """
590
+ Internal use.
591
+
592
+ Fetch document raw json from server using requests library.
593
+ """
594
+ self.assert_sync()
595
+ import requests
596
+ parsed_url = urlparse(url)
597
+ logger.info('Fetching document from url %s', parsed_url)
598
+ response = requests.get(parsed_url.geturl(), **self._request_kwargs)
599
+ response_content = response.json()
600
+ self._append_to_session_history(url, 'GET', response)
601
+ if response.status_code == HttpStatus.OK_200:
602
+ return response_content
603
+ else:
604
+
605
+ raise DocumentError(f'Error {response.status_code}: '
606
+ f'{error_from_response(response_content)}',
607
+ errors={'status_code': response.status_code},
608
+ response=response)
609
+
610
+ async def _fetch_json_async(self, url: str) -> dict:
611
+ """
612
+ Internal use. Async version.
613
+
614
+ Fetch document raw json from server using aiohttp library.
615
+ """
616
+ self.assert_async()
617
+ parsed_url = urlparse(url)
618
+ logger.info('Fetching document from url %s', parsed_url)
619
+ async with self._aiohttp_session.get(parsed_url.geturl(),
620
+ **self._request_kwargs) as response:
621
+ response_content = await response.json(content_type='application/vnd.api+json')
622
+ self._append_to_session_history(url, 'GET', response)
623
+ if response.status == HttpStatus.OK_200:
624
+ return response_content
625
+ else:
626
+ raise DocumentError(f'Error {response.status}: '
627
+ f'{error_from_response(response_content)}',
628
+ errors={'status_code': response.status},
629
+ response=response)
630
+
631
+ def http_request(self, http_method: str, url: str, send_json: dict,
632
+ expected_statuses: List[str]=None) -> Tuple[int, dict, str]:
633
+ """
634
+ Internal use.
635
+
636
+ Method to make PATCH/POST requests to server using requests library.
637
+ """
638
+ self.assert_sync()
639
+ import requests
640
+ logger.debug('%s request: %s', http_method.upper(), send_json)
641
+ expected_statuses = expected_statuses or HttpStatus.ALL_OK
642
+ kwargs = {**self._request_kwargs}
643
+ headers = {'Content-Type':'application/vnd.api+json'}
644
+ headers.update(kwargs.pop('headers', {}))
645
+
646
+ response = requests.request(http_method, url, data=json.dumps(send_json, cls=UUIDEncoder),
647
+ headers=headers,
648
+ **kwargs)
649
+ self._append_to_session_history(url, http_method, response, send_json)
650
+
651
+ if (response.status_code == HttpStatus.NO_CONTENT_204):
652
+ response_json = {}
653
+ else:
654
+ response_json = response.json()
655
+ if response.status_code not in expected_statuses:
656
+ raise DocumentError(f'Could not {http_method.upper()} '
657
+ f'({response.status_code}): '
658
+ f'{error_from_response(response_json)}',
659
+ errors={'status_code': response.status_code},
660
+ response=response,
661
+ json_data=send_json)
662
+
663
+ return response.status_code, response_json \
664
+ if response.content \
665
+ else {}, response.headers.get('Location')
666
+
667
+ async def http_request_async(
668
+ self,
669
+ http_method: str,
670
+ url: str,
671
+ send_json: dict,
672
+ expected_statuses: List[str]=None) \
673
+ -> Tuple[int, dict, str]:
674
+ """
675
+ Internal use. Async version.
676
+
677
+ Method to make PATCH/POST requests to server using aiohttp library.
678
+ """
679
+
680
+ self.assert_async()
681
+ logger.debug('%s request: %s', http_method.upper(), send_json)
682
+ expected_statuses = expected_statuses or HttpStatus.ALL_OK
683
+ content_type = '' if http_method == HttpMethod.DELETE else 'application/vnd.api+json'
684
+ kwargs = {**self._request_kwargs}
685
+ headers = {'Content-Type':'application/vnd.api+json'}
686
+ headers.update(kwargs.pop('headers', {}))
687
+ async with self._aiohttp_session.request(
688
+ http_method, url, data=json.dumps(send_json, cls=UUIDEncoder),
689
+ headers=headers,
690
+ **kwargs) as response:
691
+
692
+ response_json = await response.json(content_type=content_type)
693
+ self._append_to_session_history(url, http_method, response, send_json)
694
+ if response.status not in expected_statuses:
695
+ raise DocumentError(f'Could not {http_method.upper()} '
696
+ f'({response.status}): '
697
+ f'{error_from_response(response_json)}',
698
+ errors={'status_code': response.status},
699
+ response=response,
700
+ json_data=send_json)
701
+
702
+ return response.status, response_json or {}, response.headers.get('Location')
703
+
704
+ @property
705
+ def dirty_resources(self) -> 'Set[ResourceObject]':
706
+ """
707
+ Set of all resources in Session cache that are marked as dirty,
708
+ i.e. waiting for commit.
709
+ """
710
+ return {i for i in self.resources_by_resource_identifier.values() if i.is_dirty}
711
+
712
+ @property
713
+ def is_dirty(self) -> bool:
714
+ return bool(self.dirty_resources)
715
+
716
+ def _commit_sync(self) -> None:
717
+ self.assert_sync()
718
+ logger.info('Committing dirty resources')
719
+ for res in self.dirty_resources:
720
+ res.commit()
721
+
722
+ async def _commit_async(self) -> None:
723
+ self.assert_async()
724
+ logger.info('Committing dirty resources')
725
+ for res in self.dirty_resources:
726
+ await res._commit_async()
727
+
728
+ def commit(self) -> Optional[Awaitable]:
729
+ """
730
+ Commit (PATCH) all dirty resources to server.
731
+
732
+ If session is used with enable_async=True, this needs to be awaited.
733
+ """
734
+ if self.enable_async:
735
+ return self._commit_async()
736
+ else:
737
+ return self._commit_sync()
738
+
739
+ def assert_sync(self, msg=''):
740
+ """
741
+ Internal method to assert that async is not enabled.
742
+ """
743
+ msg = msg or 'Async requires manual fetching of resources'
744
+ if self.enable_async:
745
+ logger.error(msg)
746
+ raise AsyncError(msg)
747
+
748
+ def assert_async(self, msg=''):
749
+ """
750
+ Internal method to assert that async is enabled.
751
+ """
752
+ msg = msg or 'Calling this method is needed only when async is enabled'
753
+ if not self.enable_async:
754
+ logger.error(msg)
755
+ raise AsyncError(msg)