Skip to content

Commit 4cfa586

Browse files
committed
fix json utilty methods, and add tests to cover
1 parent fbd5360 commit 4cfa586

File tree

4 files changed

+93
-25
lines changed

4 files changed

+93
-25
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ Each `cloud-mapping` keeps an internal dict of [etags](https://en.wikipedia.org/
6969

7070
### Serialisation
7171

72-
If you don't call `.with_pickle()` and instead pass your providers configuration directly to the `cloud-mapping` class, you will get a "raw" `cloud-mapping` which only accepts byte-likes as values. You may build your own serialisation either using [zict](https://zict.readthedocs.io/en/latest/); or by calling `.with_buffers([dumps_1, loads_1, dumps_2, loads_2, ...])` where `dumps` and `loads` are the ordered functions to serialise and deserialise your data respectively.
72+
If you don't call `.with_pickle()` and instead pass your providers configuration directly to the `CloudMapping` class, you will get a "raw" `cloud-mapping` which accepts only byte-likes as values. Along with the `.with_pickle()` serialisation utility, `.with_json()` and `.with_json_zlib()` also exist.
7373

74-
The following exist as common starting points: `.with_pickle()`, `.with_json()`, `.with_json_zlib()`.
74+
You may build your own serialisation either using [zict](https://zict.readthedocs.io/en/latest/); or by calling `.with_buffers([dumps_1, dumps_2, ..., dumps_N], [loads_1, loads_2, ..., loads_N])`, where `dumps` and `loads` are the ordered functions to serialise and parse your data respectively.
7575

7676
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
# replace with your username:
33
name = cloud-mappings
4-
version = 0.7.5
4+
version = 0.7.6
55
author = Lucas Sargent
66
author_email = lucas.sargent@eliiza.com.au
77
description = MutableMapping interfaces for common cloud storage providers
Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
from typing import MutableMapping, Dict
23
from urllib.parse import quote, unquote
34

@@ -15,90 +16,103 @@ def _unsafe_key(key: str) -> str:
1516

1617

1718
class CloudMapping(MutableMapping):
18-
etags: Dict[str, str]
19+
_etags: Dict[str, str]
1920

2021
def __init__(
2122
self,
2223
storageprovider: StorageProvider,
2324
sync_initially: bool = True,
2425
) -> None:
2526
self._storageprovider = storageprovider
26-
self.etags = {}
27+
self._etags = {}
2728
if self._storageprovider.create_if_not_exists() and sync_initially:
2829
self.sync_with_cloud()
2930

3031
def sync_with_cloud(self, key: str = None) -> None:
3132
prefix_key = _safe_key(key) if key is not None else None
32-
self.etags.update({_unsafe_key(k): i for k, i in self._storageprovider.list_keys_and_etags(prefix_key).items()})
33+
self._etags.update(
34+
{_unsafe_key(k): i for k, i in self._storageprovider.list_keys_and_etags(prefix_key).items()}
35+
)
36+
37+
@property
38+
def etags(self):
39+
return self._etags
3340

3441
def __getitem__(self, key: str) -> bytes:
35-
if key not in self.etags:
42+
if key not in self._etags:
3643
raise KeyError(key)
37-
return self._storageprovider.download_data(key=_safe_key(key), etag=self.etags[key])
44+
return self._storageprovider.download_data(key=_safe_key(key), etag=self._etags[key])
3845

3946
def __setitem__(self, key: str, value: bytes) -> None:
4047
if not isinstance(value, bytes):
4148
raise ValueError("Value must be bytes like")
42-
self.etags[key] = self._storageprovider.upload_data(
49+
self._etags[key] = self._storageprovider.upload_data(
4350
key=_safe_key(key),
44-
etag=self.etags.get(key, None),
51+
etag=self._etags.get(key, None),
4552
data=value,
4653
)
4754

4855
def __delitem__(self, key: str) -> None:
49-
if key not in self.etags:
56+
if key not in self._etags:
5057
raise KeyError(key)
51-
self._storageprovider.delete_data(key=_safe_key(key), etag=self.etags[key])
52-
del self.etags[key]
58+
self._storageprovider.delete_data(key=_safe_key(key), etag=self._etags[key])
59+
del self._etags[key]
5360

5461
def __contains__(self, key: str) -> bool:
55-
return key in self.etags
62+
return key in self._etags
5663

5764
def keys(self):
58-
return iter(self.etags.keys())
65+
return iter(self._etags.keys())
5966

6067
__iter__ = keys
6168

6269
def __len__(self) -> int:
63-
return len(self.etags)
70+
return len(self._etags)
6471

6572
def __repr__(self) -> str:
6673
return f"cloudmapping<{self._storageprovider.safe_name()}>"
6774

6875
@classmethod
69-
def with_buffers(cls, io_buffers, *args, **kwargs) -> MutableMapping:
76+
def with_buffers(cls, input_buffers, output_buffers, *args, **kwargs) -> "CloudMapping":
7077
from zict import Func
7178

72-
if len(io_buffers) % 2 != 0:
79+
if len(input_buffers) != len(output_buffers):
7380
raise ValueError("Must have an equal number of input buffers as output buffers")
7481

7582
raw_mapping = cls(*args, **kwargs)
7683
mapping = raw_mapping
7784

78-
for dump, load in zip(io_buffers[::2], io_buffers[1::2]):
85+
for dump, load in zip(input_buffers[::-1], output_buffers):
7986
mapping = Func(dump, load, mapping)
8087

8188
mapping.sync_with_cloud = raw_mapping.sync_with_cloud
89+
mapping.etags = raw_mapping.etags
8290
return mapping
8391

8492
@classmethod
85-
def with_pickle(cls, *args, **kwargs) -> MutableMapping:
93+
def with_pickle(cls, *args, **kwargs) -> "CloudMapping":
8694
import pickle
8795

88-
return cls.with_buffers([pickle.dumps, pickle.loads], *args, **kwargs)
96+
return cls.with_buffers([pickle.dumps], [pickle.loads], *args, **kwargs)
8997

9098
@classmethod
91-
def with_json(cls, *args, **kwargs) -> MutableMapping:
99+
def with_json(cls, encoding="utf-8", *args, **kwargs) -> "CloudMapping":
92100
import json
93101

94-
return cls.with_buffers([json.dumps, json.loads], *args, **kwargs)
102+
return cls.with_buffers(
103+
[json.dumps, partial(bytes, encoding=encoding)],
104+
[partial(str, encoding=encoding), json.loads],
105+
*args,
106+
**kwargs,
107+
)
95108

96109
@classmethod
97-
def with_json_zlib(cls, *args, **kwargs) -> MutableMapping:
110+
def with_json_zlib(cls, encoding="utf-8", *args, **kwargs) -> "CloudMapping":
98111
import json, zlib
99112

100113
return cls.with_buffers(
101-
[json.dumps, json.loads, zlib.compress, zlib.decompress],
114+
[json.dumps, partial(bytes, encoding=encoding), zlib.compress],
115+
[zlib.decompress, partial(str, encoding=encoding), json.loads],
102116
*args,
103117
**kwargs,
104118
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
import pickle
3+
import zlib
4+
5+
from cloudmappings.cloudstoragemapping import CloudMapping
6+
from cloudmappings.errors import KeySyncError
7+
from cloudmappings.storageproviders.storageprovider import StorageProvider
8+
9+
10+
class CloudMappingUtilityTests:
11+
def test_with_buffers_fails_with_uneven_buffers(self, storage_provider: StorageProvider, test_id: str):
12+
with pytest.raises(ValueError, match="equal number of input buffers as output buffers"):
13+
CloudMapping.with_buffers(
14+
[lambda i: i, lambda i: i],
15+
[lambda i: i],
16+
storageprovider=storage_provider,
17+
sync_initially=False,
18+
)
19+
20+
def test_with_pickle(self, storage_provider: StorageProvider, test_id: str):
21+
cm = CloudMapping.with_pickle(storageprovider=storage_provider, sync_initially=False)
22+
23+
key = test_id + "with-pickle"
24+
data = {"picklable": True, "number": 10.01}
25+
26+
cm[key] = data
27+
assert cm[key] == data
28+
# Manual download and deserialisation:
29+
assert pickle.loads(storage_provider.download_data(key, cm.etags[key])) == data
30+
31+
def test_with_json(self, storage_provider: StorageProvider, test_id: str):
32+
cm = CloudMapping.with_json(storageprovider=storage_provider, sync_initially=False)
33+
34+
key = test_id + "with-json"
35+
data = [10, "json-encodable"]
36+
json = b'[10, "json-encodable"]'
37+
38+
cm[key] = data
39+
assert cm[key] == data
40+
# Manual download:
41+
assert storage_provider.download_data(key, cm.etags[key]) == json
42+
43+
def test_with_compressed_json(self, storage_provider: StorageProvider, test_id: str):
44+
cm = CloudMapping.with_json_zlib(storageprovider=storage_provider, sync_initially=False)
45+
46+
key = test_id + "with-compressed-json"
47+
data = {"a": True, "b": {}, "c": 3}
48+
json = b'{"a": true, "b": {}, "c": 3}'
49+
50+
cm[key] = data
51+
assert cm[key] == data
52+
# Manual download:
53+
raw_bytes = storage_provider.download_data(key, cm.etags[key])
54+
assert zlib.decompress(raw_bytes) == json

0 commit comments

Comments
 (0)