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,124 @@
1
+ api_schema = {
2
+ "accounts": {
3
+ "properties": {
4
+ "name": {"type": ["string"]},
5
+ "display_name": {"type": ["string"]},
6
+ "description": {"type": ["string"]},
7
+ "created_at": {"type": ["string", "null"]},
8
+ "updated_at": {"type": ["string", "null"]},
9
+ "account_type": {"type": ["string", "null"]},
10
+ "account_account_id": {"type": ["string", "null"]},
11
+ "account_account_relation": {"type": ["string", "null"]},
12
+ "company": {"relation": "to-one", "resource": "companies"},
13
+ "users": {"relation": "to-many", "resource": "users"},
14
+ }
15
+ },
16
+ "addresses": {
17
+ "properties": {
18
+ "street": {"type": ["string"]},
19
+ "number": {"type": ["string"]},
20
+ "suffix": {"type": ["string"]},
21
+ "zipcode": {"type": ["string"]},
22
+ "city": {"type": ["string"]},
23
+ "state": {"type": ["string"]},
24
+ "country": {"type": ["string"]},
25
+ "user": {"relation": "to-one", "resource": "users"},
26
+ "company": {"relation": "to-one", "resource": "companies"},
27
+ }
28
+ },
29
+ "companies": {
30
+ "properties": {
31
+ "name": {"type": ["string"]},
32
+ "kvknr": {"type": ["string", "null"]},
33
+ "phone": {"type": ["string", "null"]},
34
+ "email": {"type": ["string", "null"]},
35
+ "email_invoice": {"type": ["string", "null"]},
36
+ "vat_number": {"type": ["string", "null"]},
37
+ "created_at": {"type": ["string", "null"]},
38
+ "updated_at": {"type": ["string", "null"]},
39
+ "account": {"relation": "to-one", "resource": "accounts"},
40
+ "addresses": {"relation": "to-many", "resource": "addresses"},
41
+ }
42
+ },
43
+ "dns-records": {
44
+ "properties": {
45
+ "external_service_provider_status": {"type": ["string", "null"]},
46
+ "dns_record_type": {"type": ["string"]},
47
+ "name": {"type": ["string"]},
48
+ "content": {"type": ["string"]},
49
+ "ttl": {"type": ["integer"]},
50
+ "priority": {"type": ["integer", "null"]},
51
+ "created_at": {"type": ["string", "null"]},
52
+ "updated_at": {"type": ["string", "null"]},
53
+ "dns-zone": {"relation": "to-one", "resource": "dns-zones"},
54
+ "dns-template": {"relation": "to-one", "resource": "dns-templates"},
55
+ }
56
+ },
57
+ "dns-templates": {
58
+ "properties": {
59
+ "name": {"type": ["string"]},
60
+ "created_at": {"type": ["string", "null"]},
61
+ "updated_at": {"type": ["string", "null"]},
62
+ "account": {"relation": "to-one", "resource": "accounts"},
63
+ "dns-records": {"relation": "to-many", "resource": "dns-records"},
64
+ }
65
+ },
66
+ "dns-zones": {
67
+ "properties": {
68
+ "external_service_provider_status": {"type": ["string", "null"]},
69
+ "domain": {"type": ["string"]},
70
+ "active": {"type": ["boolean"]},
71
+ "administratively_disabled": {"type": ["boolean", "null"]},
72
+ "dnssec": {"type": ["boolean"]},
73
+ "dnssec_status": {"type": ["string", "null"]},
74
+ "dnssec_flags": {"type": ["number", "null"]},
75
+ "dnssec_algorithm": {"type": ["number", "null"]},
76
+ "dnssec_public_key": {"type": ["string", "null"]},
77
+ "created_at": {"type": ["string", "null"]},
78
+ "updated_at": {"type": ["string", "null"]},
79
+ "account": {"relation": "to-one", "resource": "accounts"},
80
+ "name-server-group": {
81
+ "relation": "to-one",
82
+ "resource": "name-server-groups",
83
+ },
84
+ "purchase": {"relation": "to-one", "resource": "purchases"},
85
+ "product": {"relation": "to-one", "resource": "products"},
86
+ "dns-records": {"relation": "to-many", "resource": "dns-records"},
87
+ }
88
+ },
89
+ "products": {
90
+ "properties": {
91
+ "name": {"type": ["string"]},
92
+ "product_type": {"type": ["string"]},
93
+ "price": {"type": ["number", "null"]},
94
+ "default_bill_sequence": {"type": ["string"]},
95
+ "invoice_description": {"type": ["string"]},
96
+ "publish_start": {"type": ["string"]},
97
+ "publish_end": {"type": ["string"]},
98
+ "created_at": {"type": ["string", "null"]},
99
+ "updated_at": {"type": ["string", "null"]},
100
+ "children": {"relation": "to-many", "resource": "products"},
101
+ "parent": {"relation": "to-one", "resource": "products"},
102
+ "service-bundles": {"relation": "to-many", "resource": "service-bundles"},
103
+ "tlds": {"relation": "to-many", "resource": "tlds"},
104
+ }
105
+ },
106
+ "purchases": {
107
+ "properties": {
108
+ "product_type": {"type": ["string", "null"]},
109
+ "description": {"type": ["string"]},
110
+ "units": {"type": ["integer"]},
111
+ "date_purchase": {"type": ["string", "null"]},
112
+ "date_activation": {"type": ["string", "null"]},
113
+ "date_deactivation": {"type": ["string", "null"]},
114
+ "date_next_bill": {"type": ["string", "null"]},
115
+ "bill_sequence": {"type": ["string", "null"]},
116
+ "created_at": {"type": ["string", "null"]},
117
+ "updated_at": {"type": ["string", "null"]},
118
+ "account": {"relation": "to-one", "resource": "accounts"},
119
+ "children": {"relation": "to-many", "resource": "purchases"},
120
+ "parent": {"relation": "to-one", "resource": "purchases"},
121
+ "product": {"relation": "to-one", "resource": "products"},
122
+ }
123
+ },
124
+ }
@@ -0,0 +1,119 @@
1
+ from typing import Generic, TypeVar, Optional, Any, Union, Awaitable
2
+ from jsonapi_client.resourceobject import ResourceObject
3
+ from jsonapi_client import Modifier, Filter
4
+ from jsonapi_client.exceptions import DocumentError
5
+ from .exception import ValidationException
6
+
7
+ from rich.console import Console, ConsoleOptions, RenderResult
8
+ from rich.json import JSON
9
+ from uuid import UUID
10
+ import json
11
+
12
+
13
+ class UUIDEncoder(json.JSONEncoder):
14
+ def default(self, obj):
15
+ if isinstance(obj, UUID):
16
+ return str(obj)
17
+ return json.JSONEncoder.default(self, obj)
18
+
19
+
20
+ class BaseModel:
21
+ def __init__(self, obj: ResourceObject):
22
+ self._obj = obj
23
+
24
+ @property
25
+ def id(self):
26
+ return self._obj.id
27
+
28
+ @id.setter
29
+ def id(self, value):
30
+ self._obj.id = value
31
+
32
+ @property
33
+ def json(self):
34
+ return self._obj.json
35
+
36
+ @property
37
+ def url(self) -> str:
38
+ return self._obj.url
39
+
40
+ def __rich_console__(
41
+ self, console: Console, options: ConsoleOptions
42
+ ) -> RenderResult:
43
+ yield JSON(json.dumps(self.json, cls=UUIDEncoder), indent=4)
44
+
45
+ def __getitem__(self, item):
46
+ return self._obj[item]
47
+
48
+ def __setitem__(self, item, value):
49
+ self._obj[item] = value
50
+
51
+ def commit(
52
+ self, custom_url: str = "", meta: Optional[dict] = None
53
+ ) -> Awaitable[ResourceObject]:
54
+ return self._obj.commit(custom_url, meta)
55
+
56
+
57
+ Model = TypeVar("Model", bound=BaseModel)
58
+
59
+
60
+ class BaseController(Generic[Model]):
61
+ _class: Any
62
+ _resource: str
63
+
64
+ def __init__(self, connection) -> None:
65
+ self._connection = connection
66
+ if not issubclass(self._class, BaseModel):
67
+ raise ValueError("_class attribute is not set")
68
+
69
+ @property
70
+ def resource(self):
71
+ return self._resource
72
+
73
+ async def fetch(self, resource_id) -> Model:
74
+ doc = await self._connection.get(self._resource, resource_id)
75
+ return self._class(doc.resource)
76
+
77
+ async def fetch_all(self, modifier: Modifier = None):
78
+ async for resource in self._connection.iterate(self._resource, modifier):
79
+ yield self._class(resource)
80
+
81
+ def create(self) -> Model:
82
+ model = self._class(self._connection.create(self._resource))
83
+ return model
84
+
85
+ async def store(
86
+ self, model: Model, create_issue: bool = False, meta: Optional[dict] = None
87
+ ):
88
+ custom_url = ""
89
+ if create_issue:
90
+ custom_url = model._obj.post_url
91
+ custom_url = custom_url + "?issue=1"
92
+ await model.commit(custom_url, meta)
93
+
94
+ async def update(
95
+ self, model: Model, create_issue: bool = False, meta: Optional[dict] = None
96
+ ):
97
+ custom_url = ""
98
+ if create_issue:
99
+ custom_url = model._obj.url
100
+ custom_url = custom_url + "?issue=1"
101
+ await model.commit(custom_url, meta)
102
+
103
+ async def destroy(self, resource_id, create_issue: bool = False):
104
+ resource = self._connection.create(self._resource)
105
+ resource.id = resource_id
106
+ resource.delete()
107
+ try:
108
+ custom_url = ""
109
+ if create_issue:
110
+ custom_url = resource.url
111
+ custom_url = custom_url + "?issue=1"
112
+ await resource.commit(custom_url)
113
+ except KeyError:
114
+ # Because we didn't fetch the record first it is not stored in
115
+ # the cache, just handle the exception and that's it. The resource
116
+ # is succesfully removed from the server.
117
+ pass
118
+ except DocumentError as e:
119
+ raise ValidationException("Validation failed", e.errors)
@@ -0,0 +1,14 @@
1
+ from .common import BaseController, BaseModel
2
+
3
+
4
+ class CompanyModel(BaseModel):
5
+ pass
6
+
7
+
8
+ class Company(BaseController[CompanyModel]):
9
+ _class = CompanyModel
10
+
11
+ def __init__(self, connection):
12
+ self._resource = "companies"
13
+
14
+ super().__init__(connection)
@@ -0,0 +1,22 @@
1
+ import importlib.metadata
2
+
3
+ from jsonapi_client import Session
4
+ from .api_schema import api_schema
5
+
6
+
7
+ class connection(Session):
8
+ def __init__(self, api_uri, api_key, extra_headers={}) -> None:
9
+
10
+ # Call parent constructor
11
+ basic_headers = {
12
+ "Accept": "application/vnd.api+json",
13
+ "Authorization": "Bearer " + api_key,
14
+ "User-Agent": "ccloud/" + importlib.metadata.version("ccloud"),
15
+ }
16
+ headers = basic_headers | extra_headers
17
+ super().__init__(
18
+ api_uri,
19
+ enable_async=True,
20
+ schema=api_schema,
21
+ request_kwargs={"headers": headers},
22
+ )
@@ -0,0 +1,14 @@
1
+ from .common import BaseController, BaseModel
2
+
3
+
4
+ class DnsRecordModel(BaseModel):
5
+ pass
6
+
7
+
8
+ class DnsRecord(BaseController[DnsRecordModel]):
9
+ _class = DnsRecordModel
10
+
11
+ def __init__(self, connection):
12
+ self._resource = "dns-records"
13
+
14
+ super().__init__(connection)
@@ -0,0 +1,14 @@
1
+ from .common import BaseController, BaseModel
2
+
3
+
4
+ class DnsTemplateModel(BaseModel):
5
+ pass
6
+
7
+
8
+ class DnsTemplate(BaseController[DnsTemplateModel]):
9
+ _class = DnsTemplateModel
10
+
11
+ def __init__(self, connection):
12
+ self._resource = "dns-templates"
13
+
14
+ super().__init__(connection)
@@ -0,0 +1,14 @@
1
+ from .common import BaseController, BaseModel
2
+
3
+
4
+ class DnsZoneModel(BaseModel):
5
+ pass
6
+
7
+
8
+ class DnsZone(BaseController[DnsZoneModel]):
9
+ _class = DnsZoneModel
10
+
11
+ def __init__(self, connection):
12
+ self._resource = "dns-zones"
13
+
14
+ super().__init__(connection)
@@ -0,0 +1,7 @@
1
+ class ValidationException(Exception):
2
+ def __init__(self, message, errors):
3
+ self.message = message
4
+ self.errors = errors
5
+
6
+ # call parent constructor
7
+ super().__init__(self.message)
@@ -0,0 +1,14 @@
1
+ from .common import BaseController, BaseModel
2
+
3
+
4
+ class ProductModel(BaseModel):
5
+ pass
6
+
7
+
8
+ class Product(BaseController[ProductModel]):
9
+ _class = ProductModel
10
+
11
+ def __init__(self, connection):
12
+ self._resource = "products"
13
+
14
+ super().__init__(connection)
@@ -0,0 +1,14 @@
1
+ from .common import BaseController, BaseModel
2
+
3
+
4
+ class PurchaseModel(BaseModel):
5
+ pass
6
+
7
+
8
+ class Purchase(BaseController[PurchaseModel]):
9
+ _class = PurchaseModel
10
+
11
+ def __init__(self, connection):
12
+ self._resource = "purchases"
13
+
14
+ super().__init__(connection)
@@ -0,0 +1,42 @@
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
+ import importlib.metadata
33
+
34
+ from .session import Session
35
+ from .filter import Filter, Inclusion, Modifier, Sort, SparseField
36
+ from .common import ResourceTuple
37
+
38
+ __version__ = "0.9.9"
39
+ try:
40
+ __version__ = importlib.metadata.version("jsonapi-client")
41
+ except:
42
+ pass
@@ -0,0 +1,186 @@
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 asyncio
34
+ import logging
35
+ from typing import Union, TYPE_CHECKING, NamedTuple
36
+
37
+ if TYPE_CHECKING:
38
+ from .session import Session
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class HttpStatus:
44
+ OK_200 = 200
45
+ CREATED_201 = 201
46
+ ACCEPTED_202 = 202
47
+ NO_CONTENT_204 = 204
48
+ FORBIDDEN_403 = 403
49
+ NOT_FOUND_404 = 404
50
+ CONFLICT_409 = 409
51
+
52
+ HAS_RESOURCES = (OK_200, CREATED_201, ACCEPTED_202)
53
+ ALL_OK = (OK_200, CREATED_201, ACCEPTED_202, NO_CONTENT_204)
54
+
55
+
56
+ class HttpMethod:
57
+ POST = 'post'
58
+ PATCH = 'patch'
59
+ DELETE = 'delete'
60
+
61
+
62
+ class RelationType:
63
+ TO_ONE = 'to-one'
64
+ TO_MANY = 'to-many'
65
+
66
+
67
+ class AbstractJsonObject:
68
+ """
69
+ Base for all JSON API specific objects
70
+ """
71
+ def __init__(self, session: 'Session', data: Union[dict, list]) -> None:
72
+ self._invalid = False
73
+ self._session = session
74
+ self._handle_data(data)
75
+
76
+ @property
77
+ def session(self):
78
+ return self._session
79
+
80
+ def _handle_data(self, data: Union[dict, list]) -> None:
81
+ """
82
+ Store data
83
+ """
84
+ raise NotImplementedError
85
+
86
+ def __repr__(self):
87
+ return f'<{self.__class__.__name__}: {str(self)} ({id(self)})>'
88
+
89
+ def __str__(self):
90
+ raise NotImplementedError
91
+
92
+ @property
93
+ def url(self) -> str:
94
+ raise NotImplementedError
95
+
96
+ def mark_invalid(self):
97
+ self._invalid = True
98
+
99
+
100
+ def error_from_response(response_content):
101
+ try:
102
+ error_str = repr([ e['detail'] for e in response_content['errors'] ])
103
+ except Exception:
104
+ error_str = '?'
105
+ return error_str
106
+
107
+
108
+ def jsonify_attribute_name(name):
109
+ return name.replace('__', '.').replace('_', '-')
110
+
111
+
112
+ def dejsonify_attribute_name(name):
113
+ return name.replace('.', '__').replace('-', '_')
114
+
115
+
116
+ def jsonify_attribute_names(iterable):
117
+ for i in iterable:
118
+ yield jsonify_attribute_name(i)
119
+
120
+
121
+ def dejsonify_attribute_names(iterable):
122
+ for i in iterable:
123
+ yield dejsonify_attribute_name(i)
124
+
125
+
126
+ async def execute_async(func, *args):
127
+ """Shortcut to asynchronize normal blocking function"""
128
+ loop = asyncio.get_event_loop()
129
+ return await loop.run_in_executor(None, func, *args)
130
+
131
+
132
+ class cached_property(object):
133
+ """
134
+ From Django code
135
+
136
+ Decorator that converts a method with a single self argument into a
137
+ property cached on the instance.
138
+
139
+ Optional ``name`` argument allows you to make cached properties of other
140
+ methods. (e.g. url = cached_property(get_absolute_url, name='url') )
141
+ """
142
+ def __init__(self, func, name=None):
143
+ self.func = func
144
+ self.__doc__ = getattr(func, '__doc__')
145
+ self.name = name or func.__name__
146
+
147
+ def __get__(self, instance, type=None):
148
+ if instance is None:
149
+ return self
150
+ res = instance.__dict__[self.name] = self.func(instance)
151
+ return res
152
+
153
+
154
+ class AttributeProxy:
155
+ """
156
+ Attribute proxy used in ResourceObject.fields etc.
157
+ """
158
+ def __init__(self, target_object=None):
159
+ self._target_object = target_object
160
+
161
+ def __getitem__(self, item):
162
+ return self._target_object[item]
163
+
164
+ def __setitem__(self, key, value):
165
+ self._target_object[key] = value
166
+
167
+ def __getattr__(self, item):
168
+ try:
169
+ return self[jsonify_attribute_name(item)]
170
+ except KeyError:
171
+ raise AttributeError
172
+
173
+ def __setattr__(self, key, value):
174
+ if key == '_target_object':
175
+ return super().__setattr__(key, value)
176
+ try:
177
+ self[jsonify_attribute_name(key)] = value
178
+ except KeyError:
179
+ raise AttributeError
180
+
181
+
182
+ class ResourceTuple(NamedTuple):
183
+ id: str
184
+ type: str
185
+ meta: dict = None
186
+