Skip to content

Commit 7b69d1b

Browse files
authored
✨ feat: config serializer #1 (#2)
1 parent 3a38203 commit 7b69d1b

File tree

4 files changed

+170
-35
lines changed

4 files changed

+170
-35
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ packages = [
1717
[tool.poetry.dependencies]
1818
python = "^3.8"
1919
httpx = "^0.25.2"
20+
tomli = { version = "^2.0.0", python = "<3.11" }
2021

2122

2223
[tool.poetry.group.test.dependencies]

src/use_nacos/endpoints/config.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import asyncio
22
import hashlib
3-
import json
43
import logging
54
import threading
6-
from typing import Optional, Any, Callable
5+
from typing import Optional, Any, Callable, Union
76

87
import httpx
98

109
from .endpoint import Endpoint
1110
from ..cache import BaseCache, MemoryCache, memory_cache
1211
from ..exception import HTTPResponseError
12+
from ..serializer import Serializer, AutoSerializer
1313
from ..typings import SyncAsync
1414

1515
logger = logging.getLogger(__name__)
@@ -29,15 +29,25 @@ def _parse_config_key(key: str):
2929
return key.split('#')
3030

3131

32+
def _serialize_config(
33+
config: Any,
34+
serializer: Optional[Union["Serializer", bool]] = None
35+
):
36+
""" Serialize config with serializer """
37+
if isinstance(serializer, bool) and serializer is True:
38+
serializer = AutoSerializer()
39+
if isinstance(serializer, Serializer):
40+
return serializer(config)
41+
return config
42+
43+
3244
class _BaseConfigEndpoint(Endpoint):
3345

3446
def _get(
3547
self,
3648
data_id: str,
3749
group: str,
38-
tenant: Optional[str] = '',
39-
*,
40-
serialized: Optional[bool] = False
50+
tenant: Optional[str] = ''
4151
) -> SyncAsync[Any]:
4252
return self.client.request(
4353
"/nacos/v1/cs/configs",
@@ -46,7 +56,7 @@ def _get(
4656
"group": group,
4757
"tenant": tenant,
4858
},
49-
serialized=serialized
59+
serialized=False
5060
)
5161

5262
def publish(
@@ -121,12 +131,10 @@ def subscriber(
121131
class ConfigOperationMixin:
122132

123133
@staticmethod
124-
def _config_callback(callback, config, serialized):
134+
def _config_callback(callback, config, serializer):
125135
if not callable(callback):
126136
return
127-
128-
if serialized:
129-
config = json.loads(config)
137+
config = _serialize_config(config, serializer)
130138
callback(config)
131139

132140
def get(
@@ -135,20 +143,20 @@ def get(
135143
group: str,
136144
tenant: Optional[str] = '',
137145
*,
138-
serialized: Optional[bool] = False,
146+
serializer: Optional[Union["Serializer", bool]] = None,
139147
cache: Optional[BaseCache] = None,
140148
default: Optional[str] = None
141149
) -> SyncAsync[Any]:
142150
cache = cache or memory_cache
143151
config_key = _get_config_key(data_id, group, tenant)
144152
try:
145-
config = self._get(data_id, group, tenant, serialized=serialized)
153+
config = self._get(data_id, group, tenant)
146154
# todo: this function need to be optimized
147155
cache.set(config_key, config)
148-
return config
156+
return _serialize_config(config, serializer)
149157
except (httpx.ConnectError, httpx.TimeoutException) as exc:
150158
logger.error("Failed to get config from server, try to get from cache. %s", exc)
151-
return cache.get(config_key)
159+
return _serialize_config(cache.get(config_key), serializer)
152160
except HTTPResponseError as exc:
153161
logger.debug("Failed to get config from server. %s", exc)
154162
if exc.status == 404 and default is not None:
@@ -161,7 +169,7 @@ def subscribe(
161169
group: str,
162170
tenant: Optional[str] = '',
163171
timeout: Optional[int] = 30_000,
164-
serialized: Optional[bool] = False,
172+
serializer: Optional[Union["Serializer", bool]] = None,
165173
cache: Optional[BaseCache] = None,
166174
callback: Optional[Callable] = None
167175
) -> SyncAsync[Any]:
@@ -179,10 +187,10 @@ def _subscriber():
179187
if not response:
180188
continue
181189
logging.info("Configuration update detected.")
182-
last_config = self.get(data_id, group, tenant, serialized=False)
190+
last_config = self._get(data_id, group, tenant)
183191
last_md5 = _get_md5(last_config)
184192
cache.set(config_key, last_config)
185-
self._config_callback(callback, last_config, serialized)
193+
self._config_callback(callback, last_config, serializer)
186194
except Exception as exc:
187195
logging.error(exc)
188196
stop_event.wait(1)
@@ -195,12 +203,11 @@ def _subscriber():
195203
class ConfigAsyncOperationMixin:
196204

197205
@staticmethod
198-
async def _config_callback(callback, config, serialized):
206+
async def _config_callback(callback, config, serializer):
199207
if not callable(callback):
200208
return
201209

202-
if serialized:
203-
config = json.loads(config)
210+
config = _serialize_config(config, serializer)
204211
if asyncio.iscoroutinefunction(callback):
205212
await callback(config)
206213
else:
@@ -212,19 +219,19 @@ async def get(
212219
group: str,
213220
tenant: Optional[str] = '',
214221
*,
215-
serialized: Optional[bool] = False,
222+
serializer: Optional[Union["Serializer", bool]] = None,
216223
cache: Optional[BaseCache] = None,
217224
default: Optional[str] = None
218225
) -> SyncAsync[Any]:
219226
cache = cache or memory_cache
220227
config_key = _get_config_key(data_id, group, tenant)
221228
try:
222-
config = await self._get(data_id, group, tenant, serialized=serialized)
229+
config = await self._get(data_id, group, tenant)
223230
cache.set(config_key, config)
224-
return config
231+
return _serialize_config(config, serializer)
225232
except (httpx.ConnectError, httpx.TimeoutException) as exc:
226233
logger.error("Failed to get config from server, try to get from cache. %s", exc)
227-
return cache.get(config_key)
234+
return _serialize_config(cache.get(config_key), serializer)
228235
except HTTPResponseError as exc:
229236
logger.debug("Failed to get config from server. %s", exc)
230237
if exc.status == 404 and default is not None:
@@ -237,7 +244,7 @@ async def subscribe(
237244
group: str,
238245
tenant: Optional[str] = '',
239246
timeout: Optional[int] = 30_000,
240-
serialized: Optional[bool] = False,
247+
serializer: Optional[Union["Serializer", bool]] = None,
241248
cache: Optional[BaseCache] = None,
242249
callback: Optional[Callable] = None,
243250
) -> SyncAsync[Any]:
@@ -257,10 +264,10 @@ async def _async_subscriber():
257264
if not response:
258265
continue
259266
logging.info("Configuration update detected.")
260-
last_config = await self.get(data_id, group, tenant, serialized=False)
267+
last_config = await self._get(data_id, group, tenant)
261268
last_md5 = _get_md5(last_config)
262269
cache.set(config_key, last_config)
263-
await self._config_callback(callback, last_config, serialized)
270+
await self._config_callback(callback, last_config, serializer)
264271
except asyncio.CancelledError:
265272
break
266273
except Exception as exc:

src/use_nacos/serializer.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import abc
2+
import json
3+
import sys
4+
5+
import yaml
6+
7+
if sys.version_info >= (3, 11):
8+
import tomllib
9+
else:
10+
import tomli as tomllib
11+
12+
13+
class Serializer(abc.ABC):
14+
15+
@abc.abstractmethod
16+
def __call__(self, *args, **kwargs):
17+
raise NotImplementedError
18+
19+
20+
class TextSerializer(Serializer):
21+
"""
22+
>>> text = TextSerializer()
23+
>>> text('a = 1')
24+
'a = 1'
25+
>>> text('a = 1\\n[foo]\\nb = 2')
26+
'a = 1\\n[foo]\\nb = 2'
27+
"""
28+
29+
def __call__(self, data) -> str:
30+
return data
31+
32+
33+
class JsonSerializer(Serializer):
34+
"""
35+
>>> json_ = JsonSerializer()
36+
>>> json_('{"a": 1}')
37+
{'a': 1}
38+
>>> json_('{"a": 1, "foo": {"b": 2}}')
39+
{'a': 1, 'foo': {'b': 2}}
40+
"""
41+
42+
def __call__(self, data) -> dict:
43+
try:
44+
return json.loads(data)
45+
except json.JSONDecodeError:
46+
raise SerializerException(f"Cannot parse data: {data!r}")
47+
48+
49+
class YamlSerializer(Serializer):
50+
"""
51+
>>> yaml_ = YamlSerializer()
52+
>>> yaml_('a: 1')
53+
{'a': 1}
54+
>>> yaml_('a: 1\\nfoo:\\n b: 2')
55+
{'a': 1, 'foo': {'b': 2}}
56+
"""
57+
58+
def __call__(self, data) -> dict:
59+
try:
60+
return yaml.safe_load(data)
61+
except yaml.YAMLError:
62+
raise SerializerException(f"Cannot parse data: {data!r}")
63+
64+
65+
class TomlSerializer(Serializer):
66+
"""
67+
>>> toml = TomlSerializer()
68+
>>> toml('a = 1')
69+
{'a': 1}
70+
>>> toml('a = 1\\n[foo]\\nb = 2')
71+
{'a': 1, 'foo': {'b': 2}}
72+
"""
73+
74+
def __call__(self, data) -> dict:
75+
try:
76+
return tomllib.loads(data)
77+
except Exception:
78+
raise SerializerException(f"Cannot parse data: {data!r}")
79+
80+
81+
class SerializerException(Exception):
82+
pass
83+
84+
85+
class AutoSerializer(Serializer):
86+
"""
87+
>>> auto = AutoSerializer()
88+
>>> auto('a = 1')
89+
{'a': 1}
90+
>>> auto('a = 1\\n[foo]\\nb = 2')
91+
{'a': 1, 'foo': {'b': 2}}
92+
>>> auto('{"a": 1}')
93+
{'a': 1}
94+
>>> auto('{"a": 1, "foo": {"b": 2}}')
95+
{'a': 1, 'foo': {'b': 2}}
96+
>>> auto('a: 1')
97+
{'a': 1}
98+
>>> auto('a: 1\\nfoo:\\n b: 2')
99+
{'a': 1, 'foo': {'b': 2}}
100+
"""
101+
102+
def __init__(self):
103+
self.serializers = (
104+
JsonSerializer(),
105+
TomlSerializer(),
106+
YamlSerializer(),
107+
TextSerializer(),
108+
)
109+
110+
def __call__(self, data) -> dict:
111+
for serializer in self.serializers:
112+
try:
113+
return serializer(data)
114+
except:
115+
pass
116+
raise SerializerException(f"Cannot parse data: {data!r}")

tests/test_config.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99
from use_nacos.cache import MemoryCache
1010
from use_nacos.client import NacosClient, NacosAsyncClient
11-
from use_nacos.endpoints import ConfigEndpoint, config as conf
11+
from use_nacos.endpoints import ConfigEndpoint, ConfigAsyncEndpoint, config as conf
1212
from use_nacos.exception import HTTPResponseError
13+
from use_nacos.serializer import JsonSerializer, AutoSerializer, YamlSerializer, TomlSerializer
1314

1415
server_addr = os.environ.get('SERVER_ADDR')
1516

@@ -31,7 +32,7 @@ def config(client):
3132

3233
@pytest.fixture
3334
def async_config(async_client):
34-
yield ConfigEndpoint(async_client)
35+
yield ConfigAsyncEndpoint(async_client)
3536

3637

3738
def test_config_get_not_found(config):
@@ -48,7 +49,7 @@ def test_config_get_not_found_default_value(config, mocker):
4849
('test_config', 'DEFAULT_GROUP'),
4950
])
5051
@pytest.mark.parametrize('content ,tenant, type, serialized, expected', [
51-
('test_config', '', None, False, 'test_config'),
52+
('test_config', '', None, None, 'test_config'),
5253
(json.dumps({"a": "b"}), '', 'json', True, {"a": "b"}),
5354
('<p>hello nacos</p>', '', 'html', False, '<p>hello nacos</p>'),
5455
(1234, '', None, True, 1234),
@@ -59,7 +60,7 @@ def test_config_publish_get(config, data_id, group, content, tenant, type, seria
5960
data_id,
6061
group,
6162
tenant,
62-
serialized=serialized
63+
serializer=serialized
6364
) == expected
6465

6566

@@ -101,20 +102,20 @@ async def test_async_config_get_not_found(async_config):
101102
@pytest.mark.parametrize('data_id, group', [
102103
('test_config', 'DEFAULT_GROUP'),
103104
])
104-
@pytest.mark.parametrize('content ,tenant, type, serialized, expected', [
105+
@pytest.mark.parametrize('content ,tenant, type, serializer, expected', [
105106
('test_config', '', None, False, 'test_config'),
106-
(json.dumps({"a": "b"}), '', 'json', True, {"a": "b"}),
107+
(json.dumps({"a": "b"}), '', 'json', JsonSerializer(), {"a": "b"}),
107108
('<p>hello nacos</p>', '', 'html', False, '<p>hello nacos</p>'),
108109
(1234, '', None, True, 1234),
109110
])
110111
@pytest.mark.asyncio
111-
async def test_async_config_publish_get(async_config, data_id, group, content, tenant, type, serialized, expected):
112+
async def test_async_config_publish_get(async_config, data_id, group, content, tenant, type, serializer, expected):
112113
assert await async_config.publish(data_id, group, content, tenant, type)
113114
assert await async_config.get(
114115
data_id,
115116
group,
116117
tenant,
117-
serialized=serialized
118+
serializer=serializer
118119
) == expected
119120

120121

@@ -165,3 +166,13 @@ def test_config_from_cache(config, mocker):
165166
# mock timeout
166167
mocker.patch.object(ConfigEndpoint, '_get', side_effect=httpx.TimeoutException(""))
167168
assert config.get('test_config_cache', 'DEFAULT_GROUP', cache=mc) == "abc"
169+
170+
171+
@pytest.mark.parametrize("conf_str, serializer, expected", [
172+
("123", AutoSerializer(), 123),
173+
('{"a": 2}', JsonSerializer(), {"a": 2}),
174+
('a: 1\nfoo:\n b: 2', YamlSerializer(), {'a': 1, 'foo': {'b': 2}}),
175+
('a = 1\n[foo]\nb = 2', TomlSerializer(), {'a': 1, 'foo': {'b': 2}}),
176+
])
177+
def test_config_serializer(conf_str, serializer, expected):
178+
assert conf._serialize_config(conf_str, serializer) == expected

0 commit comments

Comments
 (0)