Skip to content

Commit af9d2a8

Browse files
committed
Predefined settings for any endpoint
Problem: Creating assets usually requires some common settings and it is tedious to repeatedly specify them when manipulating entities. Solution: Add a fixtures property that allows the user to 'accumulate' predefined fixed settings. Signed-off-by: Paul Hewlett <phewlett76@gmail.com>
1 parent 042f4eb commit af9d2a8

16 files changed

Lines changed: 305 additions & 49 deletions

README.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ One can then use the examples code to create assets (see examples directory):
2626
2727
from archivist.archivist import Archivist
2828
from archivist.errors import ArchivistError
29+
from archivist.storage_integrity import StorageIntegrity
2930
3031
# Oauth2 token that grants access
3132
with open(".auth_token", mode='r') as tokenfile:
@@ -54,9 +55,16 @@ One can then use the examples code to create assets (see examples directory):
5455
"some_custom_attribute": "value" # You can add any custom value as long as
5556
# it does not start with arc_
5657
}
57-
behaviours = ["Attachments", "RecordEvidence"]
58-
59-
# The first argument is the behaviours of the asset
58+
#
59+
# store asset on the DLT or not. If DLT is not enabled for the user an error will occur if
60+
# StorageIntegrity.LEDGER is specified. If unspecified then TENANT_STORAGE is used
61+
# i.e. not stored on the DLT...
62+
# storage_integrity = StorageIntegrity.TENANT_STORAGE
63+
props = {
64+
"storage_integrity": StorageIntegrity.LEDGER.name,
65+
}
66+
67+
# The first argument is the properties of the asset
6068
# The second argument is the attributes of the asset
6169
# The third argument is wait for confirmation:
6270
# If @confirm@ is True then this function will not
@@ -66,7 +74,7 @@ One can then use the examples code to create assets (see examples directory):
6674
# it will be in the "Pending" status.
6775
# Once it is added to the blockchain, the status will be changed to "Confirmed"
6876
try:
69-
asset = arch.assets.create(behaviours, attrs=attrs, confirm=True)
77+
asset = arch.assets.create(props=props, attrs=attrs, confirm=True)
7078
except Archivisterror as ex:
7179
print("error", ex)
7280
else:

archivist/access_policies.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
ACCESS_POLICIES_LABEL,
3636
ASSETS_LABEL,
3737
)
38+
from .dictmerge import _deepmerge
3839

3940
from .assets import Asset
4041

@@ -43,6 +44,9 @@
4344
#: be changed.
4445
DEFAULT_PAGE_SIZE = 500
4546

47+
FIXTURE_LABEL = "access_policies"
48+
49+
4650
LOGGER = logging.getLogger(__name__)
4751

4852

@@ -169,8 +173,8 @@ def delete(self, identity: str) -> Dict:
169173
"""
170174
return self._archivist.delete(ACCESS_POLICIES_SUBPATH, identity)
171175

172-
@staticmethod
173176
def __query(
177+
self,
174178
props: Optional[Dict],
175179
*,
176180
filters: Optional[List] = None,
@@ -183,7 +187,7 @@ def __query(
183187
if access_permissions is not None:
184188
query["access_permissions"] = access_permissions
185189

186-
return query
190+
return _deepmerge(self._archivist.fixtures.get(FIXTURE_LABEL), query)
187191

188192
def count(self, *, display_name: Optional[str] = None) -> int:
189193
"""Count access policies.

archivist/archivist.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
from collections import deque
3939
from requests.models import Response
4040

41-
from flatten_dict import flatten
4241
import requests
4342
from requests_toolbelt.multipart.encoder import MultipartEncoder
4443

@@ -48,6 +47,7 @@
4847
ROOT,
4948
SEP,
5049
)
50+
from .dictmerge import _deepmerge, _dotstring
5151
from .errors import (
5252
_parse_response,
5353
ArchivistBadFieldError,
@@ -105,6 +105,7 @@ def __init__(
105105
*,
106106
auth: Optional[str] = None,
107107
cert: Optional[str] = None,
108+
fixtures: Optional[str] = None,
108109
verify: bool = True,
109110
max_time: int = MAX_TIME,
110111
):
@@ -131,6 +132,7 @@ def __init__(
131132
self._response_ring_buffer = deque(maxlen=self.RING_BUFFER_MAX_LEN)
132133
self._session = requests.Session()
133134
self._max_time = max_time
135+
self._fixtures = fixtures or {}
134136

135137
# keep these in sync with CLIENTS map above
136138
self.assets: _AssetsClient
@@ -176,8 +178,17 @@ def cert(self) -> Optional[str]:
176178
"""str: filepath containing authorisation certificate."""
177179
return self._cert
178180

181+
@property
182+
def fixtures(self) -> Optional[dict]:
183+
"""dict: Contains predefined attributes for each endpoint"""
184+
return self._fixtures
185+
186+
@fixtures.setter
187+
def fixtures(self, fixtures: dict):
188+
"""dict: Contains predefined attributes for each endpoint"""
189+
self._fixtures = _deepmerge(self._fixtures, fixtures)
190+
179191
def __add_headers(self, headers: Optional[Dict]) -> Dict:
180-
"""docstring"""
181192
if headers is not None:
182193
newheaders = {**self.headers, **headers}
183194
else:
@@ -429,9 +440,7 @@ def last_response(self, *, responses: int = 1) -> List[Response]:
429440

430441
@staticmethod
431442
def __query(query: Optional[Dict]):
432-
return query and "&".join(
433-
sorted(f"{k}={v}" for k, v in flatten(query, reducer="dot").items())
434-
)
443+
return query and "&".join(sorted(f"{k}={v}" for k, v in _dotstring(query)))
435444

436445
def get_by_signature(
437446
self, path: str, field: str, query: Dict, *, headers: Optional[Dict] = None

archivist/assets.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
CONFIRMATION_STATUS,
3939
)
4040
from . import confirmer
41+
from .dictmerge import _deepmerge
4142
from .errors import ArchivistNotFoundError
4243

4344
#: Default page size - number of entities fetched in one REST GET in the
@@ -102,6 +103,9 @@ def name(self) -> NoneOnError[str]:
102103
return None
103104

104105

106+
FIXTURE_LABEL = "assets"
107+
108+
105109
class _AssetsClient:
106110
"""AssetsClient
107111
@@ -118,9 +122,9 @@ def __init__(self, archivist: "type_helper.Archivist"):
118122

119123
def create(
120124
self,
121-
props: Dict,
122-
attrs: Dict,
123125
*,
126+
props: Optional[Dict] = None,
127+
attrs: Optional[Dict] = None,
124128
confirm: bool = False,
125129
) -> Asset:
126130
"""Create asset
@@ -196,13 +200,12 @@ def read(self, identity: str) -> Asset:
196200
"""
197201
return Asset(**self._archivist.get(ASSETS_SUBPATH, identity))
198202

199-
@staticmethod
200-
def __query(props: Optional[Dict], attrs: Optional[Dict]) -> Dict:
203+
def __query(self, props: Optional[Dict], attrs: Optional[Dict]) -> Dict:
201204
query = deepcopy(props) if props else {}
202205
if attrs:
203206
query["attributes"] = attrs
204207

205-
return query
208+
return _deepmerge(self._archivist.fixtures.get(FIXTURE_LABEL), query)
206209

207210
def count(
208211
self, *, props: Optional[Dict] = None, attrs: Optional[Dict] = None

archivist/dictmerge.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Archivist dict deep merge
2+
"""
3+
4+
from copy import deepcopy
5+
6+
from flatten_dict import flatten, unflatten
7+
8+
9+
def _deepmerge(dct1: dict, dct2: dict) -> dict:
10+
"""Deep merge 2 dictionaries
11+
12+
The settings from dct2 overwrite or add to dct1
13+
"""
14+
if dct1 is None:
15+
return deepcopy(dct2)
16+
17+
return unflatten({**flatten(dct1), **flatten(dct2)})
18+
19+
20+
def _dotstring(dct: dict) -> str:
21+
"""Emit nested dictionary as dot delimited string"""
22+
return flatten(dct, reducer="dot").items()

archivist/events.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
EVENTS_LABEL,
3939
)
4040
from . import confirmer
41+
from .dictmerge import _deepmerge
4142
from .errors import ArchivistNotFoundError
4243

4344

@@ -46,6 +47,8 @@
4647
#: be changed.
4748
DEFAULT_PAGE_SIZE = 500
4849

50+
FIXTURE_LABEL = "events"
51+
4952
LOGGER = logging.getLogger(__name__)
5053

5154

@@ -211,17 +214,16 @@ def read(self, identity: str) -> Event:
211214
)
212215
)
213216

214-
@staticmethod
215217
def __query(
216-
props: Optional[Dict], attrs: Optional[Dict], asset_attrs: Optional[Dict]
218+
self, props: Optional[Dict], attrs: Optional[Dict], asset_attrs: Optional[Dict]
217219
) -> Dict:
218220
query = deepcopy(props) if props else {}
219221
if attrs:
220222
query["event_attributes"] = attrs
221223
if asset_attrs:
222224
query["asset_attributes"] = asset_attrs
223225

224-
return query
226+
return _deepmerge(self._archivist.fixtures.get(FIXTURE_LABEL), query)
225227

226228
def count(
227229
self,

archivist/locations.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@
3030
from archivist import archivist as type_helper
3131

3232
from .constants import LOCATIONS_SUBPATH, LOCATIONS_LABEL
33+
from .dictmerge import _deepmerge
3334

3435

3536
#: Default page size - number of entities fetched in one REST GET in the
3637
#: :func:`~_LocationsClient.list` method. This can be overridden but should rarely
3738
#: be changed.
3839
DEFAULT_PAGE_SIZE = 500
3940

41+
FIXTURE_LABEL = "locations"
42+
43+
4044
LOGGER = logging.getLogger(__name__)
4145

4246

@@ -117,13 +121,12 @@ def read(self, identity: str) -> Location:
117121
)
118122
)
119123

120-
@staticmethod
121-
def __query(props: Optional[Dict], attrs: Optional[Dict]) -> Dict:
124+
def __query(self, props: Optional[Dict], attrs: Optional[Dict]) -> Dict:
122125
query = props or {}
123126
if attrs:
124127
query["attributes"] = attrs
125128

126-
return query
129+
return _deepmerge(self._archivist.fixtures.get(FIXTURE_LABEL), query)
127130

128131
def count(
129132
self, *, props: Optional[Dict] = None, attrs: Optional[Dict] = None

archivist/subjects.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@
3232
SUBJECTS_SUBPATH,
3333
SUBJECTS_LABEL,
3434
)
35+
from .dictmerge import _deepmerge
3536

3637
#: Default page size - number of entities fetched in one REST GET in the
3738
#: :func:`~_SubjectsClient.list` method. This can be overridden but should rarely
3839
#: be changed.
3940
DEFAULT_PAGE_SIZE = 500
4041

42+
FIXTURE_LABEL = "subjects"
43+
44+
4145
LOGGER = logging.getLogger(__name__)
4246

4347

@@ -171,8 +175,8 @@ def delete(self, identity: str) -> Dict:
171175
"""
172176
return self._archivist.delete(SUBJECTS_SUBPATH, identity)
173177

174-
@staticmethod
175178
def __query(
179+
self,
176180
*,
177181
display_name: Optional[str] = None,
178182
wallet_pub_keys: Optional[List[str]] = None,
@@ -190,7 +194,7 @@ def __query(
190194
if tessera_pub_keys is not None:
191195
query["tessera_pub_key"] = tessera_pub_keys
192196

193-
return query
197+
return _deepmerge(self._archivist.fixtures.get(FIXTURE_LABEL), query)
194198

195199
def count(self, *, display_name: Optional[str] = None) -> int:
196200
"""Count subjects.

docs/features.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ REST api (in any language):
2525
certain criteria to become confirmed.
2626
* a **read_by_signature()** method that allows one to retrieve an asset or event with a
2727
unique signature without knowing the identity.
28+
* predefined **fixtures** that allow specifying common attributes of assets/events
2829
* comprehensive exception handling - clear specific exceptions.
2930
* easily extensible - obeys the open-closed principle of SOLID where new endpoints
3031
can be implemented by **extending** the package as opposed to modifying it.

docs/fixtures.rst

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
.. _fixtures:
2+
3+
Fixtures
4+
=============================================
5+
6+
One can specify common attributes when creating/counting/querying assets, events
7+
and locations.
8+
9+
.. code-block:: python
10+
11+
from copy import deepcopy
12+
13+
from archivist.archivist import Archivist
14+
from archivist.errors import ArchivistError
15+
from archivist.storage_integrity import StorageIntegrity
16+
17+
# Oauth2 token that grants access
18+
with open(".auth_token", mode='r') as tokenfile:
19+
authtoken = tokenfile.read().strip()
20+
21+
# Initialize connection to Archivist - for assets on DLT.
22+
ledger = Archivist(
23+
"https://app.rkvst.io",
24+
auth=authtoken,
25+
fixtures = {
26+
"assets": {
27+
"storage_integrity": StorageIntegrity.LEDGER.name,
28+
}
29+
},
30+
)
31+
32+
# lets define doors in our namespace that reside on the ledger...
33+
doors = deepcopy(ledger)
34+
doors.fixtures = {
35+
"assets": {
36+
"attributes": {
37+
"arc_display_type": "door",
38+
"arc_namespace": "project xyz",
39+
},
40+
},
41+
}
42+
43+
# a red front door
44+
door = doors.assets.create(
45+
attrs={
46+
"arc_display_name": "front door",
47+
"colour": "red",
48+
},
49+
confirm=True,
50+
)
51+
52+
# a green back door
53+
door = doors.assets.create(
54+
attrs={
55+
"arc_display_name": "back door",
56+
"colour": "green",
57+
},
58+
confirm=True,
59+
)
60+
61+
# no need to specify arc_display_type...
62+
no_of_doors = doors.assets.count()
63+
for d in doors.assets.list():
64+
print(d)
65+
66+

0 commit comments

Comments
 (0)