Skip to content

Commit 28f4fd7

Browse files
committed
Add support for new fields
1 parent ee23524 commit 28f4fd7

File tree

7 files changed

+156
-21
lines changed

7 files changed

+156
-21
lines changed

.github/workflows/main.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ env:
2424
WEAVIATE_127: 1.27.14
2525
WEAVIATE_128: 1.28.8
2626
WEAVIATE_129: 1.29.1
27-
WEAVIATE_130: 1.30.0-rc.0-c1830a7-amd64
27+
WEAVIATE_130: preview-db-users-add-last-used-time-0184fce.amd64
2828

2929
jobs:
3030
lint-and-format:

integration/test_users.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import random
23
import pytest
34

@@ -82,16 +83,37 @@ def test_create_user_and_get(client_factory: ClientFactory) -> None:
8283
if client._connection._weaviate_version.is_lower_than(1, 30, 0):
8384
pytest.skip("This test requires Weaviate 1.30.0 or higher")
8485

86+
before = datetime.datetime.now(tz=datetime.timezone.utc)
87+
8588
randomUserName = "new-user" + str(random.randint(1, 1000))
8689
apiKey = client.users.db.create(user_id=randomUserName)
90+
91+
after_creation = datetime.datetime.now(tz=datetime.timezone.utc)
92+
8793
with weaviate.connect_to_local(
8894
port=RBAC_PORTS[0], grpc_port=RBAC_PORTS[1], auth_credentials=Auth.api_key(apiKey)
8995
) as client2:
9096
user = client2.users.get_my_user()
9197
assert user.user_id == randomUserName
98+
99+
after_login = datetime.datetime.now(tz=datetime.timezone.utc)
100+
92101
user = client.users.db.get(user_id=randomUserName)
93102
assert user.user_id == randomUserName
94103
assert user.user_type == UserTypes.DB_DYNAMIC
104+
assert user.last_used is None
105+
106+
user = client.users.db.get(user_id=randomUserName, include_last_used_at_time=True)
107+
assert user.active
108+
assert user.last_used is not None
109+
assert user.last_used > after_creation
110+
assert user.last_used < after_login
111+
112+
assert len(user.apikey_first_letters) == 3
113+
assert user.apikey_first_letters == apiKey[:3]
114+
assert user.created_at < after_creation
115+
assert user.created_at > before
116+
95117
assert client.users.db.delete(user_id=randomUserName)
96118

97119

@@ -150,6 +172,11 @@ def test_de_activate(client_factory: ClientFactory) -> None:
150172
) # second activation returns a conflict => false
151173
user = client.users.db.get(user_id=randomUserName)
152174
assert user.active
175+
assert user.last_used is None
176+
177+
user = client.users.db.get(user_id=randomUserName, include_last_used_at_time=True)
178+
assert user.active
179+
assert user.last_used is not None
153180

154181
client.users.db.delete(user_id=randomUserName)
155182

@@ -206,12 +233,27 @@ def test_list_all_users(client_factory: ClientFactory) -> None:
206233
if client._connection._weaviate_version.is_lower_than(1, 30, 0):
207234
pytest.skip("This test requires Weaviate 1.30.0 or higher")
208235

236+
before = datetime.datetime.now(tz=datetime.timezone.utc)
237+
209238
for i in range(5):
210239
client.users.db.delete(user_id=f"list-all-user-{i}")
211240
client.users.db.create(user_id=f"list-all-user-{i}")
212241

213-
users = client.users.db.list_all()
214-
dynamic_users = [user for user in users if user.user_id.startswith("list-all-")]
215-
assert len(dynamic_users) == 5
242+
after = datetime.datetime.now(tz=datetime.timezone.utc)
243+
244+
for include in [True, False]:
245+
users = client.users.db.list_all(include_last_used_at_time=include)
246+
dynamic_users = [user for user in users if user.user_id.startswith("list-all-")]
247+
assert len(dynamic_users) == 5
248+
assert all(user.user_type == UserTypes.DB_DYNAMIC for user in dynamic_users)
249+
assert all(user.active for user in dynamic_users)
250+
assert all(len(user.apikey_first_letters) == 3 for user in dynamic_users)
251+
assert all(user.created_at < after for user in dynamic_users)
252+
assert all(user.created_at > before for user in dynamic_users)
253+
if include:
254+
assert all(user.last_used is not None for user in dynamic_users)
255+
else:
256+
assert all(user.last_used is None for user in dynamic_users)
257+
216258
for i in range(5):
217259
client.users.db.delete(user_id=f"list-all-{i}")

weaviate/rbac/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ class WeaviateDBUserRoleNames(TypedDict):
104104
groups: List[str]
105105
active: bool
106106
dbUserType: str
107+
lastUsedAt: Optional[str]
108+
createdAt: str
109+
apikeyFirstLetters: Optional[str]
107110

108111

109112
class _Action:

weaviate/users/async_.pyi

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from typing import Dict, List, Literal, Union, overload
23
from weaviate.connect.v4 import ConnectionAsync
34
from weaviate.users.executor import _DeprecatedExecutor, _DBExecutor, _OIDCExecutor
@@ -42,8 +43,30 @@ class _UsersDBAsync(_DBExecutor[ConnectionAsync]):
4243
async def rotate_key(self, *, user_id: str) -> str: ...
4344
async def deactivate(self, *, user_id: str, revoke_key: bool = False) -> bool: ...
4445
async def activate(self, *, user_id: str) -> bool: ...
45-
async def get(self, *, user_id: str) -> UserDB: ...
46-
async def list_all(self) -> List[UserDB]: ...
46+
@overload
47+
async def get(
48+
self, *, user_id: str, include_last_used_at_time: Literal[True]
49+
) -> UserDB[datetime.datetime]: ...
50+
@overload
51+
async def get(
52+
self, *, user_id: str, include_last_used_at_time: Literal[False] = False
53+
) -> UserDB[None]: ...
54+
@overload
55+
async def get(
56+
self, *, user_id: str, include_last_used_at_time: bool = False
57+
) -> Union[UserDB[None], UserDB[datetime.datetime]]: ...
58+
@overload
59+
async def list_all(
60+
self, *, include_last_used_at_time: Literal[True]
61+
) -> List[UserDB[datetime.datetime]]: ...
62+
@overload
63+
async def list_all(
64+
self, *, include_last_used_at_time: Literal[False] = False
65+
) -> List[UserDB[None]]: ...
66+
@overload
67+
async def list_all(
68+
self, *, include_last_used_at_time: bool = False
69+
) -> Union[List[UserDB[None]], List[UserDB[datetime.datetime]]]: ...
4770

4871
class _UsersAsync(_DeprecatedExecutor[ConnectionAsync]):
4972
async def get_my_user(self) -> OwnUser: ...

weaviate/users/executor.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from typing import Any, Dict, Generic, List, Optional, Union, cast
23

34
from httpx import Response
@@ -17,7 +18,7 @@
1718
UserDB,
1819
OwnUser,
1920
)
20-
from weaviate.util import _decode_json_response_dict
21+
from weaviate.util import _datetime_from_weaviate_str, _decode_json_response_dict
2122

2223

2324
class _BaseExecutor(Generic[ConnectionType]):
@@ -401,53 +402,88 @@ def resp(res: Response) -> bool:
401402
status_codes=_ExpectedStatusCodes(ok_in=[200, 409], error="Deactivate user"),
402403
)
403404

404-
def get(self, *, user_id: str) -> executor.Result[Optional[UserDB]]:
405+
def get(
406+
self, *, user_id: str, include_last_used_at_time: bool = False
407+
) -> executor.Result[Optional[Union[UserDB[None], UserDB[datetime.datetime]]]]:
405408
"""Get all information about an user.
406409
407410
Args:
408411
user_id: The id of the user.
409412
"""
410413

411-
def resp(res: Response) -> Optional[UserDB]:
414+
def resp(res: Response) -> Optional[Union[UserDB[None], UserDB[datetime.datetime]]]:
412415
if res.status_code == 404:
413416
return None
414417
parsed = _decode_json_response_dict(res, "Get user")
415418
assert parsed is not None
416-
return UserDB(
417-
user_id=parsed["userId"],
418-
role_names=parsed["roles"],
419-
active=parsed["active"],
420-
user_type=UserTypes(parsed["dbUserType"]),
419+
user = cast(WeaviateDBUserRoleNames, parsed)
420+
ret = UserDB(
421+
user_id=user["userId"],
422+
role_names=user["roles"],
423+
active=user["active"],
424+
user_type=UserTypes(user["dbUserType"]),
425+
created_at=_datetime_from_weaviate_str(user["createdAt"]),
426+
last_used=get_last_used_at_time(user=user) if include_last_used_at_time else None,
427+
apikey_first_letters=get_api_key_first_letters(user=user),
421428
)
429+
if include_last_used_at_time:
430+
return cast(UserDB[datetime.datetime], ret)
431+
return cast(UserDB[None], ret)
422432

423433
return executor.execute(
424434
response_callback=resp,
425435
method=self._connection.get,
436+
params={"includeLastUsedTime": include_last_used_at_time},
426437
path=f"/users/db/{user_id}",
427438
error_msg=f"Could not get user '{user_id}'",
428439
status_codes=_ExpectedStatusCodes(ok_in=[200, 404], error="get user"),
429440
)
430441

431-
def list_all(self) -> executor.Result[List[UserDB]]:
442+
def list_all(
443+
self, *, include_last_used_at_time: bool = False
444+
) -> executor.Result[Union[List[UserDB[None]], List[UserDB[datetime.datetime]]]]:
432445
"""List all DB users."""
433446

434-
def resp(res: Response) -> List[UserDB]:
447+
def resp(res: Response) -> Union[List[UserDB[None]], List[UserDB[datetime.datetime]]]:
435448
parsed = _decode_json_response_dict(res, "Get user")
436449
assert parsed is not None
437-
return [
450+
451+
ret = [
438452
UserDB(
439453
user_id=user["userId"],
440454
role_names=user["roles"],
441455
active=user["active"],
442456
user_type=UserTypes(user["dbUserType"]),
457+
created_at=_datetime_from_weaviate_str(user["createdAt"]),
458+
last_used=(
459+
get_last_used_at_time(user=user) if include_last_used_at_time else None
460+
),
461+
apikey_first_letters=get_api_key_first_letters(user=user),
443462
)
444463
for user in cast(List[WeaviateDBUserRoleNames], parsed)
445464
]
446465

466+
if include_last_used_at_time:
467+
return cast(List[UserDB[datetime.datetime]], ret)
468+
return cast(List[UserDB[None]], ret)
469+
447470
return executor.execute(
448471
response_callback=resp,
449472
method=self._connection.get,
473+
params={"includeLastUsedTime": include_last_used_at_time},
450474
path="/users/db",
451475
error_msg="Could not list all users",
452476
status_codes=_ExpectedStatusCodes(ok_in=[200], error="list all users"),
453477
)
478+
479+
480+
def get_last_used_at_time(user: WeaviateDBUserRoleNames) -> datetime.datetime:
481+
lastused = user.get("lastUsedAt", None)
482+
if lastused is None:
483+
return datetime.datetime.min
484+
return _datetime_from_weaviate_str(lastused)
485+
486+
487+
def get_api_key_first_letters(user: WeaviateDBUserRoleNames) -> str:
488+
first_letters = user.get("apiKeyFirstLetters", "")
489+
return first_letters if first_letters else ""

weaviate/users/sync.pyi

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from typing import Dict, List, Literal, Union, overload
23
from weaviate.connect.v4 import ConnectionSync
34
from weaviate.users.executor import _DeprecatedExecutor, _DBExecutor, _OIDCExecutor
@@ -42,8 +43,30 @@ class _UsersDB(_DBExecutor[ConnectionSync]):
4243
def rotate_key(self, *, user_id: str) -> str: ...
4344
def deactivate(self, *, user_id: str, revoke_key: bool = False) -> bool: ...
4445
def activate(self, *, user_id: str) -> bool: ...
45-
def get(self, *, user_id: str) -> UserDB: ...
46-
def list_all(self) -> List[UserDB]: ...
46+
@overload
47+
def get(
48+
self, *, user_id: str, include_last_used_at_time: Literal[True]
49+
) -> UserDB[datetime.datetime]: ...
50+
@overload
51+
def get(
52+
self, *, user_id: str, include_last_used_at_time: Literal[False] = False
53+
) -> UserDB[None]: ...
54+
@overload
55+
def get(
56+
self, *, user_id: str, include_last_used_at_time: bool = False
57+
) -> Union[UserDB[None], UserDB[datetime.datetime]]: ...
58+
@overload
59+
def list_all(
60+
self, *, include_last_used_at_time: Literal[True]
61+
) -> List[UserDB[datetime.datetime]]: ...
62+
@overload
63+
def list_all(
64+
self, *, include_last_used_at_time: Literal[False] = False
65+
) -> List[UserDB[None]]: ...
66+
@overload
67+
def list_all(
68+
self, *, include_last_used_at_time: bool = False
69+
) -> Union[List[UserDB[None]], List[UserDB[datetime.datetime]]]: ...
4770

4871
class _Users(_DeprecatedExecutor[ConnectionSync]):
4972
def get_my_user(self) -> OwnUser: ...

weaviate/users/users.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from dataclasses import dataclass
2-
from typing import Dict, Final, List, Literal
2+
import datetime
3+
from typing import Dict, Final, Generic, List, Literal, TypeVar
34

45
from weaviate.rbac.models import (
56
Role,
@@ -24,10 +25,17 @@ class UserBase:
2425
user_type: UserTypes
2526

2627

28+
# generic type for UserDB
29+
T = TypeVar("T")
30+
31+
2732
@dataclass
28-
class UserDB(UserBase):
33+
class UserDB(UserBase, Generic[T]):
2934
user_type: UserTypes
3035
active: bool
36+
created_at: datetime.datetime
37+
last_used: T
38+
apikey_first_letters: str
3139

3240

3341
@dataclass

0 commit comments

Comments
 (0)