ronds-datastore-client 0.0.1__tar.gz

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 (29) hide show
  1. ronds_datastore_client-0.0.1/PKG-INFO +7 -0
  2. ronds_datastore_client-0.0.1/pyproject.toml +16 -0
  3. ronds_datastore_client-0.0.1/setup.cfg +4 -0
  4. ronds_datastore_client-0.0.1/src/datastore_client/__init__.py +37 -0
  5. ronds_datastore_client-0.0.1/src/datastore_client/adapter/__init__.py +11 -0
  6. ronds_datastore_client-0.0.1/src/datastore_client/adapter/default_client.py +162 -0
  7. ronds_datastore_client-0.0.1/src/datastore_client/adapter/direct_url_resolver.py +158 -0
  8. ronds_datastore_client-0.0.1/src/datastore_client/adapter/driver_registry.py +73 -0
  9. ronds_datastore_client-0.0.1/src/datastore_client/client.py +50 -0
  10. ronds_datastore_client-0.0.1/src/datastore_client/common/__init__.py +11 -0
  11. ronds_datastore_client-0.0.1/src/datastore_client/common/json_codec.py +43 -0
  12. ronds_datastore_client-0.0.1/src/datastore_client/common/schema_validator.py +129 -0
  13. ronds_datastore_client-0.0.1/src/datastore_client/common/url_parser.py +164 -0
  14. ronds_datastore_client-0.0.1/src/datastore_client/exceptions.py +36 -0
  15. ronds_datastore_client-0.0.1/src/datastore_client/model/__init__.py +19 -0
  16. ronds_datastore_client-0.0.1/src/datastore_client/model/message.py +83 -0
  17. ronds_datastore_client-0.0.1/src/datastore_client/model/request.py +291 -0
  18. ronds_datastore_client-0.0.1/src/datastore_client/model/resource.py +130 -0
  19. ronds_datastore_client-0.0.1/src/datastore_client/model/response.py +217 -0
  20. ronds_datastore_client-0.0.1/src/datastore_client/spi/__init__.py +14 -0
  21. ronds_datastore_client-0.0.1/src/datastore_client/spi/config.py +204 -0
  22. ronds_datastore_client-0.0.1/src/datastore_client/spi/connection.py +90 -0
  23. ronds_datastore_client-0.0.1/src/datastore_client/spi/driver.py +41 -0
  24. ronds_datastore_client-0.0.1/src/datastore_client/spi/resolver.py +23 -0
  25. ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/PKG-INFO +7 -0
  26. ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/SOURCES.txt +27 -0
  27. ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/dependency_links.txt +1 -0
  28. ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/requires.txt +2 -0
  29. ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/top_level.txt +1 -0
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: ronds-datastore-client
3
+ Version: 0.0.1
4
+ Summary: Unified DataStore SDK client
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: requests
7
+ Requires-Dist: jsonschema
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ronds-datastore-client"
7
+ version = "0.0.1"
8
+ description = "Unified DataStore SDK client"
9
+ requires-python = ">=3.8"
10
+ dependencies = ["requests", "jsonschema"]
11
+
12
+ [tool.setuptools.packages.find]
13
+ where = ["src"]
14
+
15
+ [tool.setuptools.package-dir]
16
+ "" = "src"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ """
2
+ Unified DataStore SDK client for Python 3.8+
3
+ """
4
+
5
+ from datastore_client.client import DataStoreClient
6
+ from datastore_client.exceptions import (
7
+ DataStoreException,
8
+ NoResultException,
9
+ NonUniqueResultException,
10
+ )
11
+ from datastore_client.adapter.default_client import DefaultDataStoreClient
12
+ from datastore_client.adapter.driver_registry import DriverRegistry
13
+ from datastore_client.adapter.direct_url_resolver import DirectUrlResourceResolver
14
+ from datastore_client.model.request import ReadRequest, WriteRequest, DeleteRequest, ExistsRequest
15
+ from datastore_client.model.response import ReadResponse, WriteResponse
16
+ from datastore_client.model.resource import ResourceInfo, ResourceCategory, ResourceType
17
+ from datastore_client.model.message import DataMessage
18
+
19
+ __all__ = [
20
+ "DataStoreClient",
21
+ "DefaultDataStoreClient",
22
+ "DriverRegistry",
23
+ "DirectUrlResourceResolver",
24
+ "DataStoreException",
25
+ "NoResultException",
26
+ "NonUniqueResultException",
27
+ "ReadRequest",
28
+ "WriteRequest",
29
+ "DeleteRequest",
30
+ "ExistsRequest",
31
+ "ReadResponse",
32
+ "WriteResponse",
33
+ "ResourceInfo",
34
+ "ResourceCategory",
35
+ "ResourceType",
36
+ "DataMessage",
37
+ ]
@@ -0,0 +1,11 @@
1
+ """Adapter implementations for DataStore SDK."""
2
+
3
+ from .default_client import DefaultDataStoreClient
4
+ from .driver_registry import DriverRegistry
5
+ from .direct_url_resolver import DirectUrlResourceResolver
6
+
7
+ __all__ = [
8
+ "DefaultDataStoreClient",
9
+ "DriverRegistry",
10
+ "DirectUrlResourceResolver",
11
+ ]
@@ -0,0 +1,162 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 默认 DataStore 客户端实现
4
+ 支持多连接,connect 返回 DataStoreConnection 供调用方持有
5
+ """
6
+ from typing import Optional, List
7
+ import threading
8
+ import atexit
9
+
10
+ from datastore_client.exceptions import DataStoreException
11
+ from datastore_client.model.resource import ResourceInfo
12
+ from datastore_client.spi.connection import DataStoreConnection
13
+ from datastore_client.spi.driver import DataStoreDriver
14
+ from datastore_client.spi.resolver import ResourceResolver
15
+ from datastore_client.common.url_parser import UrlParser
16
+ from datastore_client.adapter.driver_registry import DriverRegistry
17
+ from datastore_client.adapter.direct_url_resolver import DirectUrlResourceResolver
18
+
19
+
20
+ class ConnectionEntry:
21
+ def __init__(self, urn: str, resource: ResourceInfo, connection: DataStoreConnection):
22
+ self.urn = urn
23
+ self.resource = resource
24
+ self.connection = connection
25
+
26
+
27
+ class DefaultDataStoreClient:
28
+ """默认 DataStore 客户端"""
29
+
30
+ def __init__(
31
+ self,
32
+ resource_resolver: Optional[ResourceResolver] = None,
33
+ driver_registry: Optional[DriverRegistry] = None,
34
+ url_parser: Optional[UrlParser] = None,
35
+ ):
36
+ self._resource_resolver = resource_resolver
37
+ self._driver_registry = driver_registry or DriverRegistry()
38
+ self._url_parser = url_parser or UrlParser()
39
+ self._connections: List[ConnectionEntry] = []
40
+ self._lock = threading.Lock()
41
+ atexit.register(self._shutdown)
42
+
43
+ @classmethod
44
+ def builder(cls) -> "DefaultDataStoreClientBuilder":
45
+ return DefaultDataStoreClientBuilder()
46
+
47
+ def connect(self, urn: str) -> DataStoreConnection:
48
+ """建立与指定数据资源的连接;每次调用均新增连接,同一 URN 可存在多个连接。"""
49
+ info = self._resource_resolver.resolve(urn)
50
+ if info.driver_name:
51
+ self._driver_registry.ensure_driver_loaded(info.driver_name)
52
+ config = self._url_parser.parse(info.url)
53
+ scheme = info.driver_scheme or (config.scheme.lower() if config.scheme else None)
54
+ if not scheme:
55
+ raise DataStoreException("Cannot determine driver scheme")
56
+ driver = self._driver_registry.find_driver(scheme)
57
+ if not driver:
58
+ raise DataStoreException(f"No driver found for scheme: {scheme}")
59
+ conn = driver.connect(config, info.data_schema)
60
+ with self._lock:
61
+ self._connections.append(ConnectionEntry(urn, info, conn))
62
+ return conn
63
+
64
+ def disconnect(self, urn: Optional[str] = None) -> None:
65
+ """断开指定 URN 的全部连接,或关闭所有连接"""
66
+ with self._lock:
67
+ if urn:
68
+ remaining: List[ConnectionEntry] = []
69
+ for entry in self._connections:
70
+ if entry.urn == urn:
71
+ try:
72
+ entry.connection.close()
73
+ except Exception:
74
+ pass
75
+ else:
76
+ remaining.append(entry)
77
+ self._connections = remaining
78
+ else:
79
+ for entry in self._connections:
80
+ try:
81
+ entry.connection.close()
82
+ except Exception:
83
+ pass
84
+ self._connections.clear()
85
+
86
+ def is_connected(self, urn: str) -> bool:
87
+ """是否已为指定 URN 建立过至少一条仍被本 client 追踪的连接"""
88
+ with self._lock:
89
+ return any(e.urn == urn for e in self._connections)
90
+
91
+ def get_resource(self, urn: str) -> Optional[ResourceInfo]:
92
+ """获取指定 URN 的资源信息(与 Java 一致:返回最近一次 connect 对应条目)"""
93
+ with self._lock:
94
+ for entry in reversed(self._connections):
95
+ if entry.urn == urn:
96
+ return entry.resource
97
+ return None
98
+
99
+ def get_connection(self, urn: str) -> Optional[DataStoreConnection]:
100
+ """获取指定 URN 的连接(与 Java 一致:返回最近一次 connect 的实例)"""
101
+ with self._lock:
102
+ for entry in reversed(self._connections):
103
+ if entry.urn == urn:
104
+ return entry.connection
105
+ return None
106
+
107
+ def close(self) -> None:
108
+ """关闭所有连接"""
109
+ atexit.unregister(self._shutdown)
110
+ self.disconnect()
111
+
112
+ def _shutdown(self) -> None:
113
+ self.disconnect()
114
+
115
+ def __enter__(self) -> "DefaultDataStoreClient":
116
+ return self
117
+
118
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
119
+ self.close()
120
+
121
+
122
+ class DefaultDataStoreClientBuilder:
123
+ def __init__(self):
124
+ self._resource_resolver = None
125
+ self._driver_registry = None
126
+ self._url_parser = None
127
+ self._os_url = None
128
+ self._resolve_path = None
129
+
130
+ def os_url(self, os_url: str) -> "DefaultDataStoreClientBuilder":
131
+ self._os_url = os_url
132
+ return self
133
+
134
+ def resolve_path(self, path: str) -> "DefaultDataStoreClientBuilder":
135
+ self._resolve_path = path
136
+ return self
137
+
138
+ def resource_resolver(self, r: ResourceResolver) -> "DefaultDataStoreClientBuilder":
139
+ self._resource_resolver = r
140
+ return self
141
+
142
+ def driver_registry(self, r: DriverRegistry) -> "DefaultDataStoreClientBuilder":
143
+ self._driver_registry = r
144
+ return self
145
+
146
+ def url_parser(self, u: UrlParser) -> "DefaultDataStoreClientBuilder":
147
+ self._url_parser = u
148
+ return self
149
+
150
+ def build(self) -> DefaultDataStoreClient:
151
+ if self._resource_resolver is None:
152
+ if not self._os_url or not self._os_url.strip():
153
+ raise ValueError("osUrl is required when resource_resolver is not set")
154
+ self._resource_resolver = DirectUrlResourceResolver(
155
+ os_url=self._os_url,
156
+ resolve_path=self._resolve_path,
157
+ )
158
+ return DefaultDataStoreClient(
159
+ resource_resolver=self._resource_resolver,
160
+ driver_registry=self._driver_registry,
161
+ url_parser=self._url_parser,
162
+ )
@@ -0,0 +1,158 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 资源解析器
4
+ - URN:通过 osUrl 调用 HTTP 接口获取资源对象
5
+ - 直接 URL:解析 URL 得到连接信息
6
+ """
7
+ from typing import Any, Dict, List, Optional
8
+ from urllib.parse import quote
9
+
10
+ from datastore_client.exceptions import DataStoreException
11
+ from datastore_client.common.json_codec import JsonCodec
12
+ from datastore_client.common.url_parser import UrlParser
13
+ from datastore_client.model.resource import ResourceInfo, ResourceCategory, ResourceType
14
+
15
+
16
+ def _get_string(m: Optional[Dict], key: str) -> Optional[str]:
17
+ if m is None:
18
+ return None
19
+ v = m.get(key)
20
+ return str(v) if v is not None else None
21
+
22
+
23
+ def _map_driver_to_type(driver: Optional[str]) -> ResourceType:
24
+ if not driver:
25
+ return ResourceType.PGSQL
26
+ d = driver.lower()
27
+ mapping = {
28
+ "kafka": ResourceType.KAFKA,
29
+ "mqtt": ResourceType.MQTT,
30
+ "s3": ResourceType.S3,
31
+ "minio": ResourceType.MINIO,
32
+ "cassandra": ResourceType.CASSANDRA,
33
+ "redis": ResourceType.REDIS,
34
+ "pgsql": ResourceType.POSTGRESQL,
35
+ "postgresql": ResourceType.POSTGRESQL,
36
+ }
37
+ return mapping.get(d, ResourceType.POSTGRESQL)
38
+
39
+
40
+ def _map_type_to_category(rt: ResourceType) -> ResourceCategory:
41
+ mapping = {
42
+ ResourceType.KAFKA: ResourceCategory.QUEUE,
43
+ ResourceType.MQTT: ResourceCategory.QUEUE,
44
+ ResourceType.S3: ResourceCategory.OBJECT,
45
+ ResourceType.MINIO: ResourceCategory.OBJECT,
46
+ ResourceType.CASSANDRA: ResourceCategory.DATABASE,
47
+ ResourceType.REDIS: ResourceCategory.CACHE,
48
+ ResourceType.PGSQL: ResourceCategory.DATABASE,
49
+ ResourceType.POSTGRESQL: ResourceCategory.DATABASE,
50
+ }
51
+ return mapping.get(rt, ResourceCategory.DATABASE)
52
+
53
+
54
+ class DirectUrlResourceResolver:
55
+ """资源解析器"""
56
+
57
+ DEFAULT_RESOLVE_PATH = "/ui/system/datastore/urn/detail"
58
+
59
+ def __init__(
60
+ self,
61
+ os_url: Optional[str] = None,
62
+ resolve_path: Optional[str] = None,
63
+ url_parser: Optional[UrlParser] = None,
64
+ json_codec: Optional[JsonCodec] = None,
65
+ ):
66
+ self._os_url = os_url
67
+ self._resolve_path = resolve_path or self.DEFAULT_RESOLVE_PATH
68
+ self._url_parser = url_parser or UrlParser()
69
+ self._json_codec = json_codec or JsonCodec()
70
+
71
+ def resolve(self, urn: str) -> ResourceInfo:
72
+ if not urn or not urn.strip():
73
+ raise DataStoreException("URN or URL cannot be null or empty")
74
+ if urn.startswith("urn:"):
75
+ return self._resolve_by_urn(urn)
76
+ return self._resolve_by_direct_url(urn)
77
+
78
+ def _resolve_by_urn(self, urn: str) -> ResourceInfo:
79
+ if not self._os_url or not self._os_url.strip():
80
+ raise DataStoreException("URN resolution requires osUrl. Set osUrl when building DataStoreClient.")
81
+ try:
82
+ import requests
83
+ encoded_urn = quote(urn, safe="")
84
+ base = self._os_url.rstrip("/")
85
+ path = "/" + self._resolve_path.lstrip("/").rstrip("/")
86
+ if not path.endswith("/"):
87
+ path += "/"
88
+ url = base + path + encoded_urn
89
+ resp = requests.get(url, timeout=10)
90
+ if resp.status_code != 200:
91
+ raise DataStoreException(f"Failed to resolve URN: HTTP {resp.status_code}, {resp.text}")
92
+ return self._parse_get_with_details_response(urn, resp.text)
93
+ except DataStoreException:
94
+ raise
95
+ except Exception as e:
96
+ raise DataStoreException(f"Failed to resolve URN: {urn}", cause=e)
97
+
98
+ def _parse_get_with_details_response(self, urn: str, json_body: str) -> ResourceInfo:
99
+ root = self._json_codec.deserialize(json_body)
100
+ if root is None:
101
+ raise DataStoreException("Empty response from getWithDetails API")
102
+ data = root.get("data") if isinstance(root, dict) else None
103
+ if data is None or not isinstance(data, dict):
104
+ raise DataStoreException("API response missing data field")
105
+ data_store = data.get("dataStore")
106
+ if data_store is None:
107
+ raise DataStoreException("API response missing dataStore")
108
+ spec = data_store.get("spec") if isinstance(data_store, dict) else None
109
+ identify = data_store.get("identify") if isinstance(data_store, dict) else None
110
+ url = _get_string(data, "resolvedUrl")
111
+ if not url:
112
+ url = _get_string(spec, "url") if isinstance(spec, dict) else None
113
+ data_schema = _get_string(spec, "dataSchema") if isinstance(spec, dict) else None
114
+ display_name = urn
115
+ if isinstance(identify, dict):
116
+ display_name = _get_string(identify, "displayName") or _get_string(identify, "name") or urn
117
+ driver_type = None
118
+ driver_name = None
119
+ drivers = data.get("drivers")
120
+ if isinstance(drivers, list) and drivers:
121
+ first = drivers[0]
122
+ if isinstance(first, dict):
123
+ driver_spec = first.get("spec")
124
+ if isinstance(driver_spec, dict):
125
+ driver_type = _get_string(driver_spec, "type")
126
+ driver_name = _get_string(driver_spec, "driverName")
127
+ if not url or not url.strip():
128
+ raise DataStoreException("API response missing url in dataStore.spec")
129
+ if not driver_type or not driver_type.strip():
130
+ config = self._url_parser.parse(url)
131
+ driver_type = config.scheme
132
+ rt = _map_driver_to_type(driver_type)
133
+ category = _map_type_to_category(rt)
134
+ return ResourceInfo.builder() \
135
+ .urn(urn) \
136
+ .url(url) \
137
+ .data_schema(data_schema) \
138
+ .display_name(display_name or urn) \
139
+ .category(category) \
140
+ .type_(rt) \
141
+ .driver_scheme(driver_type.lower() if driver_type else None) \
142
+ .driver_name(driver_name) \
143
+ .build()
144
+
145
+ def _resolve_by_direct_url(self, url: str) -> ResourceInfo:
146
+ config = self._url_parser.parse(url)
147
+ rt = _map_driver_to_type(config.scheme)
148
+ category = _map_type_to_category(rt)
149
+ scheme_lower = config.scheme.lower() if config.scheme else None
150
+ return ResourceInfo.builder() \
151
+ .urn(url) \
152
+ .url(url) \
153
+ .data_schema(None) \
154
+ .display_name(config.entity or config.namespace or "") \
155
+ .category(category) \
156
+ .type_(rt) \
157
+ .driver_scheme(scheme_lower) \
158
+ .build()
@@ -0,0 +1,73 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 驱动注册表
4
+ 使用 entry_points (datastore.drivers) 发现驱动,类似 Java ServiceLoader
5
+ """
6
+ from typing import Optional
7
+ import sys
8
+
9
+ from datastore_client.exceptions import DataStoreException
10
+ from datastore_client.spi.driver import DataStoreDriver
11
+
12
+
13
+ def _iter_entry_points(group: str):
14
+ """Python 3.8 兼容:使用 pkg_resources"""
15
+ try:
16
+ import pkg_resources
17
+ return pkg_resources.iter_entry_points(group=group)
18
+ except ImportError:
19
+ pass
20
+ if sys.version_info >= (3, 10):
21
+ from importlib.metadata import entry_points
22
+ eps = entry_points()
23
+ if hasattr(eps, "get"):
24
+ return eps.get(group, [])
25
+ return getattr(eps, "select", lambda **k: [])(group=group) if hasattr(eps, "select") else []
26
+ return []
27
+
28
+
29
+ class DriverRegistry:
30
+ """驱动注册表"""
31
+
32
+ def __init__(self):
33
+ self._drivers: list = []
34
+ self._reload()
35
+
36
+ def _reload(self) -> None:
37
+ self._drivers = []
38
+ seen_types = set() # 按驱动类型去重,避免同一类被多个 entry_point 重复加载
39
+ for ep in _iter_entry_points("datastore.drivers"):
40
+ try:
41
+ loaded = ep.load()
42
+ if isinstance(loaded, DataStoreDriver):
43
+ driver = loaded
44
+ elif isinstance(loaded, type) and issubclass(loaded, DataStoreDriver):
45
+ driver = loaded()
46
+ else:
47
+ continue
48
+ driver_type = type(driver)
49
+ if driver_type not in seen_types:
50
+ seen_types.add(driver_type)
51
+ self._drivers.append(driver)
52
+ except Exception as e:
53
+ raise DataStoreException(f"Failed to load driver {ep.name}: {e}", cause=e)
54
+
55
+ def find_driver(self, scheme: str) -> Optional[DataStoreDriver]:
56
+ """根据 scheme 查找驱动"""
57
+ scheme_lower = (scheme or "").lower()
58
+ for d in self._drivers:
59
+ if d.accepts(scheme_lower):
60
+ return d
61
+ return None
62
+
63
+ def ensure_driver_loaded(self, driver_name: str) -> None:
64
+ """按需加载驱动(Python 无 JAR,可尝试 import 扩展包)"""
65
+ try:
66
+ __import__(driver_name.replace("-", "_"))
67
+ self._reload()
68
+ except ImportError:
69
+ pass
70
+
71
+ def reload(self) -> None:
72
+ """重新加载所有驱动"""
73
+ self._reload()
@@ -0,0 +1,50 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ DataStoreClient 统一接口
4
+ 仅负责连接管理,CRUD 操作通过 connect 返回的 DataStoreConnection 进行
5
+ """
6
+ from abc import ABC, abstractmethod
7
+ from typing import Optional, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from datastore_client.model.resource import ResourceInfo
11
+ from datastore_client.spi.connection import DataStoreConnection
12
+
13
+
14
+ class DataStoreClient(ABC):
15
+ """统一数据存储客户端接口"""
16
+
17
+ @abstractmethod
18
+ def connect(self, urn: str) -> "DataStoreConnection":
19
+ """建立与指定数据资源的连接"""
20
+ pass
21
+
22
+ @abstractmethod
23
+ def disconnect(self, urn: Optional[str] = None) -> None:
24
+ """断开指定 URN 或所有连接"""
25
+ pass
26
+
27
+ @abstractmethod
28
+ def is_connected(self, urn: str) -> bool:
29
+ """是否已连接指定 URN"""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def get_resource(self, urn: str) -> Optional["ResourceInfo"]:
34
+ """获取指定 URN 的资源信息"""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def get_connection(self, urn: str) -> Optional["DataStoreConnection"]:
39
+ """获取指定 URN 的连接"""
40
+ pass
41
+
42
+ def close(self) -> None:
43
+ """关闭所有连接"""
44
+ self.disconnect()
45
+
46
+ def __enter__(self) -> "DataStoreClient":
47
+ return self
48
+
49
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
50
+ self.close()
@@ -0,0 +1,11 @@
1
+ """Common utilities for DataStore SDK."""
2
+
3
+ from .url_parser import UrlParser
4
+ from .json_codec import JsonCodec
5
+ from .schema_validator import SchemaValidator
6
+
7
+ __all__ = [
8
+ "UrlParser",
9
+ "JsonCodec",
10
+ "SchemaValidator",
11
+ ]
@@ -0,0 +1,43 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ JSON 编解码器
4
+ """
5
+ import json
6
+ from typing import Any, Type, TypeVar, Optional
7
+
8
+ from datastore_client.exceptions import DataStoreException
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class JsonCodec:
14
+ def __init__(self, default=None):
15
+ self._default = default
16
+
17
+ def serialize(self, value: Any) -> Optional[str]:
18
+ if value is None:
19
+ return None
20
+ try:
21
+ return json.dumps(value, default=self._default, ensure_ascii=False)
22
+ except Exception as e:
23
+ raise DataStoreException("JSON serialize failed", cause=e)
24
+
25
+ def serialize_to_bytes(self, value: Any) -> Optional[bytes]:
26
+ s = self.serialize(value)
27
+ return s.encode("utf-8") if s else None
28
+
29
+ def deserialize(self, json_str: Optional[str], type_ref: Type[T] = None) -> Optional[T]:
30
+ if not json_str or not json_str.strip():
31
+ return None
32
+ try:
33
+ data = json.loads(json_str)
34
+ if type_ref and type_ref != dict and type_ref != list:
35
+ return data
36
+ return data
37
+ except Exception as e:
38
+ raise DataStoreException("JSON deserialize failed", cause=e)
39
+
40
+ def deserialize_bytes(self, data: Optional[bytes], type_ref: Type[T] = None) -> Optional[T]:
41
+ if not data:
42
+ return None
43
+ return self.deserialize(data.decode("utf-8"), type_ref)