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.
- fustor_registry_client-0.1.7/PKG-INFO +9 -0
- fustor_registry_client-0.1.7/README.md +41 -0
- fustor_registry_client-0.1.7/pyproject.toml +23 -0
- fustor_registry_client-0.1.7/setup.cfg +4 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client/__init__.py +0 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client/client.py +95 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client/models.py +13 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client.egg-info/PKG-INFO +9 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client.egg-info/SOURCES.txt +12 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client.egg-info/dependency_links.txt +1 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client.egg-info/requires.txt +3 -0
- fustor_registry_client-0.1.7/src/fustor_registry_client.egg-info/top_level.txt +1 -0
- fustor_registry_client-0.1.7/tests/test_client.py +256 -0
- fustor_registry_client-0.1.7/tests/test_registry_client_models.py +14 -0
|
@@ -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",]
|
|
File without changes
|
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fustor_registry_client
|
|
@@ -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
|