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.
- ronds_datastore_client-0.0.1/PKG-INFO +7 -0
- ronds_datastore_client-0.0.1/pyproject.toml +16 -0
- ronds_datastore_client-0.0.1/setup.cfg +4 -0
- ronds_datastore_client-0.0.1/src/datastore_client/__init__.py +37 -0
- ronds_datastore_client-0.0.1/src/datastore_client/adapter/__init__.py +11 -0
- ronds_datastore_client-0.0.1/src/datastore_client/adapter/default_client.py +162 -0
- ronds_datastore_client-0.0.1/src/datastore_client/adapter/direct_url_resolver.py +158 -0
- ronds_datastore_client-0.0.1/src/datastore_client/adapter/driver_registry.py +73 -0
- ronds_datastore_client-0.0.1/src/datastore_client/client.py +50 -0
- ronds_datastore_client-0.0.1/src/datastore_client/common/__init__.py +11 -0
- ronds_datastore_client-0.0.1/src/datastore_client/common/json_codec.py +43 -0
- ronds_datastore_client-0.0.1/src/datastore_client/common/schema_validator.py +129 -0
- ronds_datastore_client-0.0.1/src/datastore_client/common/url_parser.py +164 -0
- ronds_datastore_client-0.0.1/src/datastore_client/exceptions.py +36 -0
- ronds_datastore_client-0.0.1/src/datastore_client/model/__init__.py +19 -0
- ronds_datastore_client-0.0.1/src/datastore_client/model/message.py +83 -0
- ronds_datastore_client-0.0.1/src/datastore_client/model/request.py +291 -0
- ronds_datastore_client-0.0.1/src/datastore_client/model/resource.py +130 -0
- ronds_datastore_client-0.0.1/src/datastore_client/model/response.py +217 -0
- ronds_datastore_client-0.0.1/src/datastore_client/spi/__init__.py +14 -0
- ronds_datastore_client-0.0.1/src/datastore_client/spi/config.py +204 -0
- ronds_datastore_client-0.0.1/src/datastore_client/spi/connection.py +90 -0
- ronds_datastore_client-0.0.1/src/datastore_client/spi/driver.py +41 -0
- ronds_datastore_client-0.0.1/src/datastore_client/spi/resolver.py +23 -0
- ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/PKG-INFO +7 -0
- ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/SOURCES.txt +27 -0
- ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/dependency_links.txt +1 -0
- ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/requires.txt +2 -0
- ronds_datastore_client-0.0.1/src/ronds_datastore_client.egg-info/top_level.txt +1 -0
|
@@ -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,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,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)
|