fustor-registry-client 0.1.7__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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: fustor-registry-client
3
+ Version: 0.1.7
4
+ Summary: Client for Fustor Registry service
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: pydantic>=2.11.7
9
+ Requires-Dist: fustor-common
@@ -0,0 +1,41 @@
1
+ # fustor-registry-client
2
+
3
+ This package provides a Python client for interacting with the Fustor Registry service. It offers a convenient way to programmatically access and manage metadata, storage environments, data stores, users, API keys, and datasets within the Fustor platform.
4
+
5
+ ## Features
6
+
7
+ * **Registry Client**: A Python client for making requests to the Fustor Registry API.
8
+ * **Models**: Provides Pydantic data models for requests, responses, and other data structures used by the Fustor Registry API.
9
+
10
+ ## Installation
11
+
12
+ This package is part of the Fustor monorepo and is typically installed in editable mode within the monorepo's development environment using `uv sync`.
13
+
14
+ ## Usage
15
+
16
+ Developers can use this client to integrate with the Fustor Registry service from other Fustor components or external applications. It simplifies the process of interacting with the Registry's RESTful API.
17
+
18
+ Example (conceptual):
19
+
20
+ ```python
21
+ from fustor_registry_client.client import RegistryClient
22
+ from fustor_registry_client.models import UserCreate, UserUpdate
23
+
24
+ # Assuming RegistryClient is initialized with the Registry service URL
25
+ client = RegistryClient(base_url="http://localhost:8101")
26
+
27
+ # Example: Create a new user
28
+ new_user = UserCreate(username="testuser", email="test@example.com", password="securepassword")
29
+ created_user = client.create_user(new_user)
30
+ print(f"Created user: {created_user.username}")
31
+
32
+ # Example: Get a user by ID
33
+ user = client.get_user(user_id=created_user.id)
34
+ print(f"Retrieved user: {user.email}")
35
+ ```
36
+
37
+ ## Dependencies
38
+
39
+ * `httpx`: A next-generation HTTP client for Python.
40
+ * `pydantic`: For defining and validating data models.
41
+ * `fustor-common`: Provides foundational elements and shared components.
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "fustor-registry-client"
3
+ dynamic = ["version"]
4
+ description = "Client for Fustor Registry service"
5
+ requires-python = ">=3.11"
6
+ license = "MIT"
7
+ dependencies = [ "httpx>=0.27.0", "pydantic>=2.11.7", "fustor-common",]
8
+
9
+ [build-system]
10
+ requires = [ "setuptools>=61.0", "setuptools-scm>=8.0"]
11
+ build-backend = "setuptools.build_meta"
12
+
13
+ [tool.setuptools_scm]
14
+ root = "../.."
15
+ version_scheme = "post-release"
16
+ local_scheme = "dirty-tag"
17
+
18
+ ["project.urls"]
19
+ Homepage = "https://github.com/excelwang/fustor/tree/master/packages/fustor_registry_client"
20
+ "Bug Tracker" = "https://github.com/excelwang/fustor/issues"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = [ "src",]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,95 @@
1
+ import httpx
2
+ import os
3
+ from typing import Optional, Dict, Any, List
4
+
5
+ from fustor_common.models import TokenResponse, MessageResponse, DatastoreBase, ApiKeyBase
6
+ from fustor_registry_client.models import ClientApiKeyResponse, ClientDatastoreConfigResponse
7
+
8
+ class RegistryClient:
9
+ def __init__(self, base_url: str, token: Optional[str] = None, client: Optional[httpx.AsyncClient] = None):
10
+ self.base_url = base_url
11
+ self.headers = {}
12
+ if token:
13
+ self.headers["Authorization"] = f"Bearer {token}"
14
+ if client:
15
+ self.client = client
16
+ self.client.headers.update(self.headers)
17
+ else:
18
+ self.client = httpx.AsyncClient(base_url=self.base_url, headers=self.headers)
19
+
20
+ async def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]:
21
+ response = await self.client.request(method, path, **kwargs)
22
+ response.raise_for_status()
23
+ return response.json()
24
+
25
+ async def login(self, email: str, password: str) -> TokenResponse:
26
+ data = {"username": email, "password": password}
27
+ response = await self.client.post("/v1/auth/login", data=data)
28
+ response.raise_for_status()
29
+ return TokenResponse(**response.json())
30
+
31
+ async def list_datastores(self) -> List[DatastoreBase]:
32
+ response_data = await self._request("GET", "/v1/datastores/")
33
+ return [DatastoreBase(**data) for data in response_data]
34
+
35
+ async def create_datastore(self, name: str, meta: Optional[Dict] = None, visible: bool = False, allow_concurrent_push: bool = False, session_timeout_seconds: int = 30) -> DatastoreBase:
36
+ payload = {
37
+ "name": name,
38
+ "meta": meta,
39
+ "visible": visible,
40
+ "allow_concurrent_push": allow_concurrent_push,
41
+ "session_timeout_seconds": session_timeout_seconds
42
+ }
43
+ response_data = await self._request("POST", "/v1/datastores/", json=payload)
44
+ return DatastoreBase(**response_data)
45
+
46
+ async def get_datastore(self, datastore_id: int) -> DatastoreBase:
47
+ response_data = await self._request("GET", f"/v1/datastores/{datastore_id}")
48
+ return DatastoreBase(**response_data)
49
+
50
+ async def update_datastore(self, datastore_id: int, name: Optional[str] = None, meta: Optional[Dict] = None, visible: Optional[bool] = None, allow_concurrent_push: Optional[bool] = None, session_timeout_seconds: Optional[int] = None) -> DatastoreBase:
51
+ payload = {}
52
+ if name is not None: payload["name"] = name
53
+ if meta is not None: payload["meta"] = meta
54
+ if visible is not None: payload["visible"] = visible
55
+ if allow_concurrent_push is not None: payload["allow_concurrent_push"] = allow_concurrent_push
56
+ if session_timeout_seconds is not None: payload["session_timeout_seconds"] = session_timeout_seconds
57
+ response_data = await self._request("PUT", f"/v1/datastores/{datastore_id}", json=payload)
58
+ return DatastoreBase(**response_data)
59
+
60
+ async def delete_datastore(self, datastore_id: int) -> MessageResponse:
61
+ response_data = await self._request("DELETE", f"/v1/datastores/{datastore_id}")
62
+ return MessageResponse(**response_data)
63
+
64
+ async def list_api_keys(self) -> List[ApiKeyBase]:
65
+ response_data = await self._request("GET", "/v1/keys/")
66
+ return [ApiKeyBase(**data) for data in response_data]
67
+
68
+ async def create_api_key(self, name: str, datastore_id: int) -> ApiKeyBase:
69
+ payload = {"name": name, "datastore_id": datastore_id}
70
+ response_data = await self._request("POST", "/v1/keys/", json=payload)
71
+ return ApiKeyBase(**response_data)
72
+
73
+ async def get_api_key(self, key_id: int) -> ApiKeyBase:
74
+ # The original API doesn't have a GET /v1/keys/{key_id} endpoint.
75
+ # For now, we'll simulate by listing and filtering, or raise an error.
76
+ # A better approach would be to add this endpoint to the registry API.
77
+ raise NotImplementedError("GET /v1/keys/{key_id} is not implemented in the Registry API.")
78
+
79
+ async def delete_api_key(self, key_id: int) -> MessageResponse:
80
+ response_data = await self._request("DELETE", f"/v1/keys/{key_id}")
81
+ return MessageResponse(**response_data)
82
+
83
+ async def get_client_api_keys(self) -> List[ClientApiKeyResponse]:
84
+ response_data = await self._request("GET", "/client/api-keys")
85
+ return [ClientApiKeyResponse(**data) for data in response_data]
86
+
87
+ async def get_client_datastores_config(self) -> List[ClientDatastoreConfigResponse]:
88
+ response_data = await self._request("GET", "/client/datastores-config")
89
+ return [ClientDatastoreConfigResponse(**data) for data in response_data]
90
+
91
+ async def __aenter__(self):
92
+ return self
93
+
94
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
95
+ await self.client.aclose()
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+ from typing import List, Optional
3
+
4
+ class ClientApiKeyResponse(BaseModel):
5
+ key: str
6
+ datastore_id: int
7
+ model_config = ConfigDict(from_attributes=True)
8
+
9
+ class ClientDatastoreConfigResponse(BaseModel):
10
+ datastore_id: int
11
+ allow_concurrent_push: bool
12
+ session_timeout_seconds: int
13
+ model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: fustor-registry-client
3
+ Version: 0.1.7
4
+ Summary: Client for Fustor Registry service
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: pydantic>=2.11.7
9
+ Requires-Dist: fustor-common
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/fustor_registry_client/__init__.py
4
+ src/fustor_registry_client/client.py
5
+ src/fustor_registry_client/models.py
6
+ src/fustor_registry_client.egg-info/PKG-INFO
7
+ src/fustor_registry_client.egg-info/SOURCES.txt
8
+ src/fustor_registry_client.egg-info/dependency_links.txt
9
+ src/fustor_registry_client.egg-info/requires.txt
10
+ src/fustor_registry_client.egg-info/top_level.txt
11
+ tests/test_client.py
12
+ tests/test_registry_client_models.py
@@ -0,0 +1,3 @@
1
+ httpx>=0.27.0
2
+ pydantic>=2.11.7
3
+ fustor-common
@@ -0,0 +1,256 @@
1
+ import pytest
2
+ from fastapi import FastAPI, APIRouter, Depends, HTTPException, status
3
+ from fastapi.testclient import TestClient
4
+ from typing import List, Dict, Any, Optional
5
+ from fustor_registry_client.client import RegistryClient
6
+ from fustor_registry_client.models import ClientApiKeyResponse, ClientDatastoreConfigResponse
7
+ from fustor_common.models import TokenResponse, MessageResponse, DatastoreBase, ApiKeyBase, Password, LoginRequest
8
+
9
+ # --- Mock FastAPI App for Registry Service ---
10
+ mock_app = FastAPI()
11
+ mock_router_v1 = APIRouter(prefix="/v1")
12
+ mock_client_router = APIRouter(prefix="/client")
13
+
14
+ # Mock data
15
+ MOCK_DATASTORES = {
16
+ 1: {"name": "test_datastore_1", "visible": False, "meta": {}, "allow_concurrent_push": False, "session_timeout_seconds": 30},
17
+ 2: {"name": "test_datastore_2", "visible": True, "meta": {"env": "prod"}, "allow_concurrent_push": True, "session_timeout_seconds": 60},
18
+ }
19
+ MOCK_API_KEYS = {
20
+ 101: {"id": 101, "name": "api_key_1", "key": "key123", "datastore_id": 1},
21
+ 102: {"id": 102, "name": "api_key_2", "key": "key456", "datastore_id": 2},
22
+ }
23
+ MOCK_USERS = {
24
+ "admin@example.com": {"password": "hashed_password", "token": "mock_jwt_token"},
25
+ }
26
+
27
+ import copy
28
+
29
+ _INITIAL_MOCK_DATASTORES = {
30
+ 1: {"name": "test_datastore_1", "visible": False, "meta": {}, "allow_concurrent_push": False, "session_timeout_seconds": 30},
31
+ 2: {"name": "test_datastore_2", "visible": True, "meta": {"env": "prod"}, "allow_concurrent_push": True, "session_timeout_seconds": 60},
32
+ }
33
+ _INITIAL_MOCK_API_KEYS = {
34
+ 101: {"id": 101, "name": "api_key_1", "key": "key123", "datastore_id": 1},
35
+ 102: {"id": 102, "name": "api_key_2", "key": "key456", "datastore_id": 2},
36
+ }
37
+ _INITIAL_MOCK_USERS = {
38
+ "admin@example.com": {"password": "hashed_password", "token": "mock_jwt_token"},
39
+ }
40
+
41
+ @pytest.fixture(autouse=True)
42
+ def reset_mock_data():
43
+ global MOCK_DATASTORES
44
+ global MOCK_API_KEYS
45
+ global MOCK_USERS
46
+
47
+ MOCK_DATASTORES = copy.deepcopy(_INITIAL_MOCK_DATASTORES)
48
+ MOCK_API_KEYS = copy.deepcopy(_INITIAL_MOCK_API_KEYS)
49
+ MOCK_USERS = copy.deepcopy(_INITIAL_MOCK_USERS)
50
+ yield
51
+
52
+ # Mock Auth Endpoints
53
+ from fastapi import Form
54
+ @mock_router_v1.post("/auth/login", response_model=TokenResponse)
55
+ async def mock_login(username: str = Form(), password: str = Form()):
56
+ if username == "admin@example.com" and password == "admin_password": # Simplified mock password check
57
+ return TokenResponse(access_token=MOCK_USERS[username]["token"], token_type="bearer")
58
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
59
+
60
+ # Mock Datastore Endpoints
61
+ @mock_router_v1.get("/datastores/", response_model=List[DatastoreBase])
62
+ async def mock_list_datastores():
63
+ return [DatastoreBase(**data) for data in MOCK_DATASTORES.values()]
64
+
65
+ @mock_router_v1.post("/datastores/", response_model=DatastoreBase)
66
+ async def mock_create_datastore(datastore: DatastoreBase):
67
+ new_id = max(MOCK_DATASTORES.keys()) + 1 if MOCK_DATASTORES else 1
68
+ datastore_data = datastore.model_dump()
69
+ datastore_data["id"] = new_id
70
+ MOCK_DATASTORES[new_id] = datastore_data
71
+ return DatastoreBase(**datastore_data)
72
+
73
+ @mock_router_v1.get("/datastores/{datastore_id}", response_model=DatastoreBase)
74
+ async def mock_get_datastore(datastore_id: int):
75
+ if datastore_id not in MOCK_DATASTORES:
76
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Datastore not found")
77
+ return DatastoreBase(**MOCK_DATASTORES[datastore_id])
78
+
79
+ @mock_router_v1.put("/datastores/{datastore_id}", response_model=DatastoreBase)
80
+ async def mock_update_datastore(datastore_id: int, updated_data: Dict[str, Any]):
81
+ if datastore_id not in MOCK_DATASTORES:
82
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Datastore not found")
83
+ MOCK_DATASTORES[datastore_id].update(updated_data)
84
+ return DatastoreBase(**MOCK_DATASTORES[datastore_id])
85
+
86
+ @mock_router_v1.delete("/datastores/{datastore_id}", response_model=MessageResponse)
87
+ async def mock_delete_datastore(datastore_id: int):
88
+ if datastore_id not in MOCK_DATASTORES:
89
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Datastore not found")
90
+ del MOCK_DATASTORES[datastore_id]
91
+ return MessageResponse(message="Datastore deleted successfully")
92
+
93
+ # Mock API Key Endpoints
94
+ @mock_router_v1.get("/keys/", response_model=List[ApiKeyBase])
95
+ async def mock_list_api_keys():
96
+ return [ApiKeyBase(**data) for data in MOCK_API_KEYS.values()]
97
+
98
+ @mock_router_v1.post("/keys/", response_model=ApiKeyBase)
99
+ async def mock_create_api_key(api_key: ApiKeyBase):
100
+ new_id = max(MOCK_API_KEYS.keys()) + 1 if MOCK_API_KEYS else 1
101
+ api_key_data = api_key.model_dump()
102
+ api_key_data["id"] = new_id
103
+ MOCK_API_KEYS[new_id] = api_key_data
104
+ return ApiKeyBase(**api_key_data)
105
+
106
+ @mock_router_v1.delete("/keys/{key_id}", response_model=MessageResponse)
107
+ async def mock_delete_api_key(key_id: int):
108
+ if key_id not in MOCK_API_KEYS:
109
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found")
110
+ del MOCK_API_KEYS[key_id]
111
+ return MessageResponse(message="API Key deleted successfully")
112
+
113
+ # Mock Client Endpoints
114
+ @mock_client_router.get("/api-keys", response_model=List[ClientApiKeyResponse])
115
+ async def mock_get_client_api_keys():
116
+ return [ClientApiKeyResponse(key=data["key"], name=data["name"], datastore_id=data["datastore_id"]) for data in MOCK_API_KEYS.values()]
117
+
118
+ @mock_client_router.get("/datastores-config", response_model=List[ClientDatastoreConfigResponse])
119
+ async def mock_get_client_datastores_config():
120
+ configs = []
121
+ for ds_id, ds_data in MOCK_DATASTORES.items():
122
+ configs.append(ClientDatastoreConfigResponse(
123
+ datastore_id=ds_id,
124
+ allow_concurrent_push=ds_data["allow_concurrent_push"],
125
+ session_timeout_seconds=ds_data["session_timeout_seconds"]
126
+ ))
127
+ return configs
128
+
129
+ mock_app.include_router(mock_router_v1)
130
+ mock_app.include_router(mock_client_router)
131
+
132
+ @pytest.fixture
133
+ def test_client():
134
+ with TestClient(mock_app) as client:
135
+ yield client
136
+
137
+ import httpx
138
+ from httpx import Request, Response, AsyncBaseTransport
139
+
140
+ class _ClientTransport(AsyncBaseTransport):
141
+ def __init__(self, test_client: TestClient):
142
+ self._test_client = test_client
143
+
144
+ async def handle_async_request(self, request: Request) -> Response:
145
+ method = request.method
146
+ url = str(request.url)
147
+ headers = dict(request.headers)
148
+ content = request.content
149
+
150
+ response = self._test_client.request(
151
+ method,
152
+ url,
153
+ headers=headers,
154
+ content=content,
155
+ )
156
+ return httpx.Response(
157
+ status_code=response.status_code,
158
+ headers=response.headers,
159
+ content=response.content,
160
+ request=request,
161
+ )
162
+
163
+ @pytest.fixture
164
+ def registry_client(test_client: TestClient):
165
+ transport = _ClientTransport(test_client)
166
+ async_client = httpx.AsyncClient(transport=transport, base_url="http://mock-registry")
167
+ return RegistryClient(base_url="http://mock-registry", client=async_client)
168
+
169
+ @pytest.mark.asyncio
170
+ async def test_login(registry_client):
171
+ token_response = await registry_client.login(email="admin@example.com", password="admin_password")
172
+ assert isinstance(token_response, TokenResponse)
173
+ assert token_response.access_token is not None
174
+ assert token_response.token_type == "bearer"
175
+
176
+ with pytest.raises(httpx.HTTPStatusError) as exc_info:
177
+ await registry_client.login(email="admin@example.com", password="wrong_password")
178
+ assert exc_info.value.response.status_code == status.HTTP_401_UNAUTHORIZED
179
+
180
+ @pytest.mark.asyncio
181
+ async def test_list_datastores(registry_client: RegistryClient):
182
+ datastores = await registry_client.list_datastores()
183
+ assert len(datastores) == len(MOCK_DATASTORES)
184
+ assert all(isinstance(ds, DatastoreBase) for ds in datastores)
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_create_datastore(registry_client: RegistryClient):
188
+ new_datastore = await registry_client.create_datastore(name="new_ds", visible=True)
189
+ assert isinstance(new_datastore, DatastoreBase)
190
+ assert new_datastore.name == "new_ds"
191
+ assert new_datastore.visible == True
192
+ # Check if it's added to mock data (TestClient doesn't persist state across requests by default,
193
+ # but our mock app uses global MOCK_DATASTORES, so it will)
194
+ assert new_datastore.id in MOCK_DATASTORES
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_get_datastore(registry_client: RegistryClient):
198
+ datastore = await registry_client.get_datastore(datastore_id=1)
199
+ assert isinstance(datastore, DatastoreBase)
200
+ assert datastore.name == "test_datastore_1"
201
+
202
+ with pytest.raises(httpx.HTTPStatusError) as exc_info:
203
+ await registry_client.get_datastore(datastore_id=999)
204
+ assert exc_info.value.response.status_code == status.HTTP_404_NOT_FOUND
205
+
206
+ @pytest.mark.asyncio
207
+ async def test_update_datastore(registry_client: RegistryClient):
208
+ updated_ds = await registry_client.update_datastore(datastore_id=1, name="updated_name", session_timeout_seconds=100)
209
+ assert isinstance(updated_ds, DatastoreBase)
210
+ assert updated_ds.name == "updated_name"
211
+ assert updated_ds.session_timeout_seconds == 100
212
+ assert MOCK_DATASTORES[1]["name"] == "updated_name"
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_delete_datastore(registry_client: RegistryClient):
216
+ initial_count = len(MOCK_DATASTORES)
217
+ response = await registry_client.delete_datastore(datastore_id=1)
218
+ assert isinstance(response, MessageResponse)
219
+ assert response.message == "Datastore deleted successfully"
220
+ assert len(MOCK_DATASTORES) == initial_count - 1
221
+ assert 1 not in MOCK_DATASTORES
222
+
223
+ @pytest.mark.asyncio
224
+ async def test_list_api_keys(registry_client: RegistryClient):
225
+ api_keys = await registry_client.list_api_keys()
226
+ assert len(api_keys) == len(MOCK_API_KEYS)
227
+ assert all(isinstance(ak, ApiKeyBase) for ak in api_keys)
228
+
229
+ @pytest.mark.asyncio
230
+ async def test_create_api_key(registry_client: RegistryClient):
231
+ new_api_key = await registry_client.create_api_key(name="new_api_key", datastore_id=1)
232
+ assert isinstance(new_api_key, ApiKeyBase)
233
+ assert new_api_key.name == "new_api_key"
234
+ assert new_api_key.datastore_id == 1
235
+ assert new_api_key.id in MOCK_API_KEYS
236
+
237
+ @pytest.mark.asyncio
238
+ async def test_delete_api_key(registry_client: RegistryClient):
239
+ initial_count = len(MOCK_API_KEYS)
240
+ response = await registry_client.delete_api_key(key_id=101)
241
+ assert isinstance(response, MessageResponse)
242
+ assert response.message == "API Key deleted successfully"
243
+ assert len(MOCK_API_KEYS) == initial_count - 1
244
+ assert 101 not in MOCK_API_KEYS
245
+
246
+ @pytest.mark.asyncio
247
+ async def test_get_client_api_keys(registry_client: RegistryClient):
248
+ client_keys = await registry_client.get_client_api_keys()
249
+ assert len(client_keys) == len(MOCK_API_KEYS)
250
+ assert all(isinstance(ik, ClientApiKeyResponse) for ik in client_keys)
251
+
252
+ @pytest.mark.asyncio
253
+ async def test_get_client_datastores_config(registry_client: RegistryClient):
254
+ client_configs = await registry_client.get_client_datastores_config()
255
+ assert len(client_configs) == len(MOCK_DATASTORES)
256
+ assert all(isinstance(ic, ClientDatastoreConfigResponse) for ic in client_configs)
@@ -0,0 +1,14 @@
1
+ import pytest
2
+ from pydantic import ValidationError
3
+ from fustor_registry_client.models import ClientApiKeyResponse, ClientDatastoreConfigResponse
4
+
5
+ def test_client_api_key_response():
6
+ response = ClientApiKeyResponse(key="test_key", datastore_id=1)
7
+ assert response.key == "test_key"
8
+ assert response.datastore_id == 1
9
+
10
+ def test_client_datastore_config_response():
11
+ response = ClientDatastoreConfigResponse(datastore_id=1, allow_concurrent_push=True, session_timeout_seconds=60)
12
+ assert response.datastore_id == 1
13
+ assert response.allow_concurrent_push == True
14
+ assert response.session_timeout_seconds == 60