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,656 @@
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 logging
34
+ from itertools import chain
35
+ from typing import Set, Optional, Awaitable, Union, Iterable, TYPE_CHECKING
36
+
37
+ from .common import (jsonify_attribute_name, AbstractJsonObject,
38
+ dejsonify_attribute_names, HttpMethod, HttpStatus, AttributeProxy,
39
+ cached_property, RelationType)
40
+ from .exceptions import ValidationError, DocumentInvalid
41
+
42
+ NOT_FOUND = object()
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ if TYPE_CHECKING:
47
+ from .session import Schema, Session
48
+
49
+
50
+ class AttributeDict(dict):
51
+ """
52
+ Container for JSON API attributes in ResourceObjects.
53
+ In addition to standard dictionary this offers:
54
+ - access to attributes via getattr (attribute names jsonified, i.e.
55
+ my_attr -> my-attr)
56
+ - dirty-flagging attributes upon change (keep track of changed attributes)
57
+ and ability to generate diff structure containing only changed data
58
+ (for PATCHing)
59
+ - etc.
60
+ """
61
+ def __init__(self, data: dict,
62
+ resource: 'ResourceObject',
63
+ name: str ='',
64
+ parent: 'AttributeDict'=None) -> None:
65
+ """
66
+ :param data: Input data (dictionary) that is stored here.
67
+ :param resource: root ResourceObject
68
+ :param name: name of this attribute, if this is contained within another
69
+ AttributeDict. Otherwise None.
70
+ :param parent: Parent AttributeDict if this is contained within another
71
+ AttributeDict. Otherwise None
72
+ """
73
+ super().__init__()
74
+ self._parent = parent
75
+ self._name = name
76
+ self._resource = resource
77
+ self._schema: 'Schema' = resource.session.schema
78
+ self._full_name: str = name
79
+ self._invalid = False
80
+ self._dirty_attributes: Set[str] = set()
81
+
82
+ if self._parent is not None and self._parent._full_name:
83
+ self._full_name = f'{parent._full_name}.{name}'
84
+
85
+ specification = self._schema.find_spec(self._resource.type, self._full_name)
86
+
87
+ # Using .pop() below modifies the data, so we make a shallow copy of it first
88
+ data = data.copy()
89
+ # If there's schema for this object, we will use it to construct object.
90
+ if specification:
91
+ for field_name, field_spec in specification['properties'].items():
92
+ if field_spec.get('type') == 'object':
93
+ _data = data.pop(field_name, {})
94
+ # Workaround a strange bug where _data is None instead of
95
+ # default value {}
96
+ if _data is None:
97
+ _data = {}
98
+ self[field_name] = AttributeDict(data=_data,
99
+ name=field_name,
100
+ parent=self,
101
+ resource=resource)
102
+ elif 'relation' in field_spec:
103
+ pass # Special handling for relationships
104
+ else:
105
+ self[field_name] = data.pop(field_name, field_spec.get('default'))
106
+
107
+ if data:
108
+ logger.warning('There was extra data (not specified in schema): %s',
109
+ data)
110
+ # If not, we will use the source data as it is.
111
+ if data:
112
+ self.update(data)
113
+ for key, value in data.items():
114
+ if isinstance(value, dict):
115
+ self[key] = AttributeDict(data=value, name=key, parent=self, resource=resource)
116
+ self._dirty_attributes.clear()
117
+
118
+ def create_map(self, attr_name):
119
+ """
120
+ Create a new map of values (i.e. child AttributeDict) within this AttributeDict
121
+
122
+ :param attr_name: Name of this map object.
123
+ """
124
+ self._check_invalid()
125
+ name = jsonify_attribute_name(attr_name)
126
+ self[name] = AttributeDict(data={}, name=name, parent=self, resource=self._resource)
127
+
128
+ def _check_invalid(self):
129
+ if self._invalid:
130
+ raise DocumentInvalid('Resource has been invalidated.')
131
+
132
+ def __getattr__(self, name):
133
+ name = jsonify_attribute_name(name)
134
+ if name not in self:
135
+ raise AttributeError(f'No such attribute '
136
+ f'{self._resource.type}.{self._full_name}.{name}')
137
+ return self[name]
138
+
139
+ def __setitem__(self, key, value):
140
+ if self.get(key) != value:
141
+ self.mark_dirty(key)
142
+ super().__setitem__(key, value)
143
+
144
+ def __setattr__(self, name, value):
145
+ if name.startswith('_'):
146
+ return super().__setattr__(name, value)
147
+ name = jsonify_attribute_name(name)
148
+ self[name] = value
149
+
150
+ def mark_dirty(self, name: str):
151
+ """
152
+ Mark one attribute within this dictionary as dirty.
153
+
154
+ :param name: Name of the attribute that is to be marked as dirty.
155
+ """
156
+ self._dirty_attributes.add(name)
157
+ if self._parent:
158
+ self._parent.mark_dirty(self._name)
159
+
160
+ def mark_clean(self):
161
+ """
162
+ Mark all attributes recursively as clean..
163
+ """
164
+ for attr in self._dirty_attributes:
165
+ value = self[attr]
166
+ if isinstance(value, AttributeDict):
167
+ value.mark_clean()
168
+ self._dirty_attributes.clear()
169
+
170
+ @property
171
+ def diff(self) -> dict:
172
+ """
173
+ Produce JSON containing only changed elements based on dirty fields.
174
+ """
175
+ self._check_invalid()
176
+ diff = {}
177
+ for name in self._dirty_attributes:
178
+ value = self[name]
179
+ if isinstance(value, AttributeDict) and value.is_dirty:
180
+ diff[name] = value.diff
181
+ else:
182
+ diff[name] = value
183
+ return diff
184
+
185
+ @property
186
+ def post_data(self) -> dict:
187
+ """
188
+ Produce JSON which does not contain values which are null.
189
+ """
190
+ self._check_invalid()
191
+ result = self.copy()
192
+ for key, value in self.items():
193
+ if isinstance(value, AttributeDict):
194
+ result[key] = new_value = value.post_data
195
+ if len(new_value) == 0:
196
+ del result[key]
197
+
198
+ if value is None:
199
+ del result[key]
200
+ return result
201
+
202
+ @property
203
+ def is_dirty(self) -> bool:
204
+ return bool(self._dirty_attributes)
205
+
206
+ def mark_invalid(self):
207
+ """
208
+ Recursively mark this and contained objects as invalid.
209
+ """
210
+ self._invalid = True
211
+ for value in self.values():
212
+ if isinstance(value, AttributeDict):
213
+ value.mark_invalid()
214
+
215
+ def change_resource(self, new_resource: 'ResourceObject') -> None:
216
+ """
217
+ Change parent ResourceObject recursively
218
+ :param new_resource: new resource that is used as a new root ResourceObject
219
+ """
220
+ self._resource = new_resource
221
+ for value in self.values():
222
+ if isinstance(value, AttributeDict):
223
+ value.change_resource(new_resource)
224
+
225
+ def keys_python(self) -> Iterable[str]:
226
+ """
227
+ Pythonized version of contained keys (attribute names).
228
+ """
229
+ yield from dejsonify_attribute_names(self.keys())
230
+
231
+
232
+ class RelationshipDict(dict):
233
+ """
234
+ Container for relationships that is stored in ResourceObject
235
+ """
236
+
237
+ def __init__(self, data: dict, resource: 'ResourceObject'):
238
+ """
239
+ :param data: Raw input data where Relationship objects are built from.
240
+ :param resource: Parent ResourceObject
241
+ """
242
+ super().__init__()
243
+ self._invalid = False
244
+ self._resource = resource
245
+ self.session = resource.session
246
+ self._schema = schema = resource.session.schema
247
+ model_schema = schema.schema_for_model(resource.type)
248
+ if model_schema:
249
+ for rel_name, rel_value in model_schema['properties'].items():
250
+ rel_type = rel_value.get('relation')
251
+ if not rel_type:
252
+ continue
253
+
254
+ resource_types = rel_value['resource']
255
+ self[rel_name] = self._make_relationship(data.pop(rel_name, {}), rel_type,
256
+ resource_types)
257
+ else:
258
+ relationships = {key: self._make_relationship(value)
259
+ for key, value in data.items()}
260
+ self.update(relationships)
261
+
262
+ def mark_invalid(self):
263
+ """
264
+ Mark invalid this dictionary and contained Relationships.
265
+ """
266
+ self._invalid = True
267
+ for value in self.values():
268
+ value.mark_invalid()
269
+
270
+ def change_resource(self, new_resource: 'ResourceObject') -> None:
271
+ """
272
+ :param new_resource: Change parent ResourceObject to new_resource.
273
+ """
274
+ self._resource = new_resource
275
+
276
+ def _determine_class(self, data: dict, relation_type: str=None):
277
+ """
278
+ From data and/or provided relation_type, determine Relationship class
279
+ to be used.
280
+
281
+ :param data: Source data dictionary
282
+ :param relation_type: either 'to-one' or 'to-many'
283
+ """
284
+ from . import relationships as rel
285
+ if 'data' in data:
286
+ relationship_data = data['data']
287
+ if isinstance(relationship_data, list):
288
+ if not (not relation_type or relation_type == RelationType.TO_MANY):
289
+ logger.error('Conflicting information about relationship')
290
+ return rel.MultiRelationship
291
+ elif relationship_data is None or isinstance(relationship_data, dict):
292
+ if not(not relation_type or relation_type == RelationType.TO_ONE):
293
+ logger.error('Conflicting information about relationship')
294
+ return rel.SingleRelationship
295
+ else:
296
+ raise ValidationError('Relationship data key is invalid')
297
+ elif 'links' in data:
298
+ return rel.LinkRelationship
299
+ elif 'meta' in data:
300
+ return rel.MetaRelationship
301
+ elif relation_type == RelationType.TO_MANY:
302
+ return rel.MultiRelationship
303
+ elif relation_type == RelationType.TO_ONE:
304
+ return rel.SingleRelationship
305
+ else:
306
+ raise ValidationError('Must have either links, data or meta in relationship')
307
+
308
+ def _make_relationship(self, data, relation_type=None, resource_types=None):
309
+ cls = self._determine_class(data, relation_type)
310
+ return cls(self.session, data, resource_types=resource_types,
311
+ relation_type=relation_type)
312
+
313
+ def mark_clean(self):
314
+ """
315
+ Mark all relationships as clean (not dirty).
316
+ """
317
+ for attr in self.values():
318
+ attr.mark_clean()
319
+
320
+ def keys_python(self) -> Iterable[str]:
321
+ """
322
+ Pythonized version of contained keys (relationship names)
323
+ """
324
+ yield from dejsonify_attribute_names(self.keys())
325
+
326
+ @property
327
+ def is_dirty(self) -> bool:
328
+ return any(r.is_dirty for r in self.values())
329
+
330
+
331
+ class ResourceObject(AbstractJsonObject):
332
+ """
333
+ Basic JSON API resourceobject type. Field (attribute and relationship) access directly
334
+ via instance attributes (__getattr__). In case of namespace collisions, there is also
335
+ .fields attribute proxy.
336
+
337
+ http://jsonapi.org/format/#document-resource-objects
338
+ """
339
+
340
+ #: Attributes (that are not starting with _) that we want to ignore in __setattr__
341
+ __attributes = ['id', 'type', 'links', 'meta', 'commit_meta']
342
+
343
+ def __init__(self, session: 'Session', data: Union[dict, list]) -> None:
344
+ self._delete = False
345
+ self._commit_metadata = {}
346
+ super().__init__(session, data)
347
+
348
+ @cached_property
349
+ def fields(self):
350
+ """
351
+ Proxy to all fields (both attributes and relationship target resources)
352
+ """
353
+ class Proxy(AttributeProxy):
354
+ def __getitem__(proxy, item):
355
+ rv = self._attributes.get(item, NOT_FOUND)
356
+ if rv is NOT_FOUND:
357
+ return self.relationship_resource[item]
358
+ else:
359
+ return rv
360
+
361
+ def __setitem__(proxy, item, value):
362
+ if item in self._relationships:
363
+ return self._relationships[item].set(value)
364
+ else:
365
+ self._attributes[item] = value
366
+
367
+ def __dir__(proxy):
368
+ return chain(super().__dir__(), self._attributes.keys_python(),
369
+ self._relationships.keys_python())
370
+
371
+ return Proxy()
372
+
373
+ @cached_property
374
+ def attributes(self):
375
+ """
376
+ Proxy to all attributes (not relationships)
377
+ """
378
+ return AttributeProxy(self._attributes)
379
+
380
+ @cached_property
381
+ def relationships(self):
382
+ """
383
+ Proxy to relationship objects
384
+ """
385
+ class Proxy(AttributeProxy):
386
+ def __setitem__(proxy, key, value):
387
+ rel = self._relationships[key]
388
+ rel.set(value)
389
+
390
+ return Proxy(self._relationships)
391
+
392
+ @cached_property
393
+ def relationship_resource(self):
394
+ """
395
+ If async enabled, proxy to relationship objects.
396
+ If async disabled, proxy to resources behind relationships.
397
+ """
398
+ class Proxy(AttributeProxy):
399
+ def __getitem__(proxy, item):
400
+ rel = self.relationships[item]
401
+ if self.session.enable_async:
402
+ # With async it's more convenient to access Relationship object
403
+ return self.relationships[item]
404
+
405
+ if rel.is_single:
406
+ return rel.resource
407
+ else:
408
+ return rel.resources
409
+
410
+ return Proxy()
411
+
412
+ def _handle_data(self, data):
413
+ from .objects import Links, Meta
414
+ self.id = data['id']
415
+ self.type = data['type']
416
+ self.links = Links(self.session, data.get('links', {}))
417
+ self.meta = Meta(self.session, data.get('meta', {}))
418
+
419
+ self._relationships = RelationshipDict(
420
+ data=data.get('relationships', {}),
421
+ resource=self)
422
+ self._attributes = AttributeDict(data=data.get('attributes', {}), resource=self)
423
+
424
+ def create_map(self, name):
425
+ """
426
+ Create a map of values (AttributeDict) with name in attribute container.
427
+ """
428
+ return self._attributes.create_map(name)
429
+
430
+ def __dir__(self):
431
+ return chain(super().__dir__(), self._attributes.keys_python(),
432
+ self._relationships.keys_python())
433
+
434
+ def __str__(self):
435
+ return f'{self.type}: {self.id} ({id(self)})'
436
+
437
+ @property
438
+ def json(self) -> dict:
439
+ """
440
+ Return full JSON API resource object as json-serializable dictionary.
441
+ """
442
+ return self._commit_data(full=True)['data']
443
+
444
+ @property
445
+ def is_dirty(self) -> bool:
446
+ return (self.id is None
447
+ or self._delete
448
+ or self._attributes.is_dirty
449
+ or self._relationships.is_dirty)
450
+
451
+ def __getitem__(self, item):
452
+ return self.fields[item]
453
+
454
+ def __setitem__(self, item, value):
455
+ self.fields[item] = value
456
+
457
+ def __getattr__(self, attr_name):
458
+ return getattr(self.fields, attr_name)
459
+
460
+ def __setattr__(self, attr_name, value):
461
+ if attr_name.startswith('_') or attr_name in self.__attributes:
462
+ return super().__setattr__(attr_name, value)
463
+
464
+ return setattr(self.fields, attr_name, value)
465
+
466
+ @property
467
+ def dirty_fields(self):
468
+ return (self._attributes._dirty_attributes |
469
+ {name for name, rel in self._relationships.items() if rel.is_dirty})
470
+
471
+ @property
472
+ def url(self) -> str:
473
+ url = str(self.links.self)
474
+ return url or self.id and f'{self.session.url_prefix}/{self.type}/{self.id}'
475
+
476
+ @property
477
+ def post_url(self) -> str:
478
+ return f'{self.session.url_prefix}/{self.type}'
479
+
480
+ def validate(self):
481
+ """
482
+ Validate our attributes against schema.
483
+ """
484
+ # TODO: what about relationships? Shouldn't we somehow validate those too?
485
+ self.session.schema.validate(self.type, self._attributes)
486
+
487
+ def _commit_data(self, meta: dict = None, full: bool=False) -> dict:
488
+ """
489
+ Give JSON data for PATCH/POST request, requested by commit
490
+ """
491
+ meta = meta or self._commit_metadata
492
+
493
+ res_json = {'type': self.type}
494
+ if self.id:
495
+ res_json['id'] = self.id
496
+
497
+ if self._http_method == 'post' or full:
498
+ # When creating new resources, we need to specify explicitly all
499
+ # relationships, as SingleRelationships, or MultiRelationships.
500
+
501
+ relationships = {key: {'data': value.as_json_resource_identifiers}
502
+ for key, value in self._relationships.items() if bool(value)}
503
+ res_json.update({
504
+ 'attributes': self._attributes.post_data,
505
+ 'relationships': relationships,
506
+ })
507
+ else:
508
+ changed_relationships = {key: {'data': value.as_json_resource_identifiers}
509
+ for key, value in self._relationships.items()
510
+ if value.is_dirty}
511
+ res_json.update({
512
+ 'attributes': self._attributes.diff,
513
+ 'relationships': changed_relationships,
514
+ })
515
+ if meta:
516
+ res_json['meta'] = meta
517
+ return {'data': res_json}
518
+
519
+ @property
520
+ def _http_method(self):
521
+ return HttpMethod.PATCH if self.id else HttpMethod.POST
522
+
523
+ def _pre_commit(self, custom_url):
524
+ url = custom_url or (self.post_url if self._http_method == HttpMethod.POST else self.url)
525
+ logger.info('Committing %s to %s', self, url)
526
+ self.validate()
527
+ return url
528
+
529
+ def _post_commit(self, status, result, location):
530
+ if status in HttpStatus.HAS_RESOURCES:
531
+ self._update_resource(result, location)
532
+
533
+ # If no resources are returned (which is the case when 202 (Accepted)
534
+ # is received for PATCH, for example).
535
+ self.mark_clean()
536
+
537
+ if status == HttpStatus.ACCEPTED_202:
538
+ return self.session.read(result, location, no_cache=True).resource
539
+
540
+ async def _commit_async(self, url: str= '', meta=None) -> None:
541
+ self.session.assert_async()
542
+ if self._delete:
543
+ return await self._perform_delete_async(url)
544
+
545
+ url = self._pre_commit(url)
546
+ status, result, location = await self.session.http_request_async(
547
+ self._http_method, url,
548
+ self._commit_data(meta))
549
+ return self._post_commit(status, result, location)
550
+
551
+ def _commit_sync(self, url: str= '', meta: dict=None) -> 'None':
552
+ self.session.assert_sync()
553
+ if self._delete:
554
+ return self._perform_delete(url)
555
+
556
+ url = self._pre_commit(url)
557
+ status, result, location = self.session.http_request(self._http_method, url,
558
+ self._commit_data(meta))
559
+ return self._post_commit(status, result, location)
560
+
561
+ def commit(self, custom_url: str = '', meta: dict = None) \
562
+ -> 'Union[None, ResourceObject, Awaitable[Optional[ResourceObject]]':
563
+ """
564
+ Commit (PATCH/POST) this resource to server.
565
+
566
+ :param custom_url: Use this url instead of automatically determined one.
567
+ :param meta: Optional metadata that is passed to server in POST/PATCH request
568
+
569
+ If in async mode, this needs to be awaited.
570
+ """
571
+ if not self._delete:
572
+ self.validate()
573
+
574
+ if self.session.enable_async:
575
+ return self._commit_async(custom_url, meta)
576
+ else:
577
+ return self._commit_sync(custom_url, meta)
578
+
579
+ def _update_resource(self,
580
+ resource_dict: 'Union[dict, ResourceObject]',
581
+ location: str=None) -> None:
582
+ if isinstance(resource_dict, dict):
583
+ new_res = self.session.read(resource_dict, location, no_cache=True).resource
584
+ else:
585
+ new_res = resource_dict
586
+ self.id = new_res.id
587
+ self._attributes.mark_invalid()
588
+ self._relationships.mark_invalid()
589
+
590
+ self._attributes: AttributeDict = new_res._attributes
591
+ self._attributes.change_resource(self)
592
+ self._relationships: RelationshipDict = new_res._relationships
593
+ self._relationships.change_resource(self)
594
+ self.meta = new_res.meta
595
+ self.links = new_res.links
596
+ self.session.add_resources(self)
597
+
598
+ def _refresh_sync(self):
599
+ self.session.assert_sync()
600
+ new_res = self.session.fetch_resource_by_resource_identifier(self, force=True)
601
+ self._update_resource(new_res)
602
+
603
+ async def _refresh_async(self):
604
+ self.session.assert_async()
605
+ new_res = await self.session.fetch_resource_by_resource_identifier_async(
606
+ self,
607
+ force=True)
608
+ self._update_resource(new_res)
609
+
610
+ def refresh(self):
611
+ """
612
+ Manual way to refresh the data contained in this ResourceObject from server.
613
+
614
+ If in async mode, this needs to be awaited.
615
+ """
616
+ if self.session.enable_async:
617
+ return self._refresh_async()
618
+ else:
619
+ return self._refresh_sync()
620
+
621
+ def delete(self):
622
+ """
623
+ Mark resource to be deleted. Resource will be deleted upon commit.
624
+ """
625
+ self._delete = True
626
+
627
+ def _perform_delete(self, url=''):
628
+ url = url or self.url
629
+ self.session.http_request(HttpMethod.DELETE, url, {})
630
+ self.session.remove_resource(self)
631
+
632
+ async def _perform_delete_async(self, url=''):
633
+ url = url or self.url
634
+ await self.session.http_request_async(HttpMethod.DELETE, url, {})
635
+ self.session.remove_resource(self)
636
+
637
+ def mark_clean(self):
638
+ """
639
+ Mark this resource and attributes / relationships as clean (not dirty).
640
+ """
641
+ self._attributes.mark_clean()
642
+ self._relationships.mark_clean()
643
+
644
+ def mark_invalid(self):
645
+ """
646
+ Mark this resource and it's related objects as invalid.
647
+ """
648
+ super().mark_invalid()
649
+ self._attributes.mark_invalid()
650
+ self._relationships.mark_invalid()
651
+ self.meta.mark_invalid()
652
+ self.links.mark_invalid()
653
+
654
+ def as_resource_identifier_dict(self) -> dict:
655
+ return {'id': self.id, 'type': self.type}
656
+