diff --git a/.env.test b/.env.test index 7cdd9ce..7679fe7 100644 --- a/.env.test +++ b/.env.test @@ -7,4 +7,4 @@ PYTEST_ADMIN_PASSWORD=start123 PYTEST_DEFAULT_MASTER_IMAGE=python/base PYTEST_ASYNC_MAX_RETRIES=5 PYTEST_ASYNC_RETRY_DELAY_MILLIS=500 -PYTEST_HUB_VERSION=0.12.2 +PYTEST_HUB_VERSION=0.12.5 diff --git a/README.md b/README.md index f5f5267..db3f53e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ +[![PyPI](https://img.shields.io/pypi/v/flame-hub-client?label=PyPI&cacheSeconds=0)](https://pypi.org/project/flame-hub-client/) ![Code Coverage](https://img.shields.io/badge/Coverage-97%25-brightgreen.svg) +![Python versions](https://img.shields.io/badge/Python->=3.10-blue) +[![License](https://img.shields.io/pypi/l/flame-hub-client?label=License&cacheSeconds=0)](https://pypi.org/project/flame-hub-client/) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) # FLAME Hub Python Client @@ -49,12 +53,12 @@ print(master_realm.model_dump_json(indent=2)) ```console { "name": "master", - "display_name": null, + "displayName": null, "description": null, "id": "794f2375-f043-4789-bd0c-e5534e8deeaa", - "built_in": true, - "created_at": "2025-05-12T09:44:08.284000Z", - "updated_at": "2025-05-12T09:44:08.284000Z" + "builtIn": true, + "createdAt": "2025-05-12T09:44:08.284000Z", + "updatedAt": "2025-05-12T09:44:08.284000Z" } ``` diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..abf03c2 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,2 @@ +.. include:: ../CHANGELOG.md + :parser: myst diff --git a/docs/index.rst b/docs/index.rst index 70567ec..274709a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,3 +9,4 @@ user_guide testing api + changelog diff --git a/docs/user_guide.rst b/docs/user_guide.rst index a13d067..6d2dfc6 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -14,8 +14,9 @@ respectively. The signature of the clients is always the same since they inherit When initializing a client, there are some things to keep in mind. -* You *should* provide an instance of either :py:class:`.PasswordAuth` or :py:class:`.ClientAuth` to the ``auth`` - argument as these are the two main authentication schemes supported by the FLAME Hub. +* You *should* provide an instance of either :py:class:`.PasswordAuth`, :py:class:`.ClientAuth` or + :py:class:`.StaticAuth` to the ``auth`` argument as these are the three main authentication schemes supported by the + FLAME Hub. * You *can* provide a custom ``base_url`` if you're hosting your own instance of the FLAME Hub, otherwise the client will use the default publicly available Hub instance https://privateaim.dev to connect to. * You *should'nt* set ``client`` explicitly unless you know what you're doing. When providing any of the previous two @@ -97,14 +98,11 @@ Overview of implemented resources * :py:class:`.AuthClient` * realms * users - * robots * permissions * roles * role permissions * user permissions * user roles - * robot permissions - * robot roles * :py:class:`.CoreClient` * registries * registry projects @@ -203,7 +201,7 @@ See :py:class:`.FindAllKwargs` for the API documentation of all possible paramet Optional fields =============== -Some fields are not provided by default, such as the secret tied to a robot. You can explicitly request these fields +Some fields are not provided by default, such as the email tied to a user. You can explicitly request these fields with the `fields` keyword argument. .. code-block:: python @@ -215,19 +213,19 @@ with the `fields` keyword argument. ) auth_client = flame_hub.AuthClient(base_url="http://localhost:3000/auth/", auth=auth) - system_robot = auth_client.find_robots(filter={"name": "system"}).pop() - assert system_robot.secret is None + admin = auth_client.find_users(filter={"name": "admin"}).pop() + assert admin.email is None -You have to request ``secret`` explicitly in order to get it. +You have to request ``email`` explicitly in order to get it. .. code-block:: python - system_robot = auth_client.find_robots(filter={"name": "system"}, fields="secret").pop() - print(system_robot.secret) + admin = auth_client.find_users(filter={"name": "admin"}, fields="email").pop() + print(admin.email) .. code-block:: - $2y$10$KUOKEwbbnaUDo41e7XBKGek4hggD6z6R95I69Cv3mTeBcx0hifBAC + admin@example.com If you are ever unsure which fields can be requested this way on a specific resource, use :py:func:`.get_field_names` function. @@ -235,9 +233,9 @@ function. .. code-block:: python from flame_hub import get_field_names - from flame_hub.models import Robot + from flame_hub.models import User - assert get_field_names(Robot) == ("secret",) + assert get_field_names(User) == ("email",) Meta information @@ -296,12 +294,12 @@ the resource you are requesting. 794f2375-f043-4789-bd0c-e5534e8deeaa { "name": "master", - "display_name": null, + "displayName": null, "description": null, "id": "794f2375-f043-4789-bd0c-e5534e8deeaa", - "built_in": true, - "created_at": "2025-05-12T09:44:08.284000Z", - "updated_at": "2025-05-12T09:44:08.284000Z" + "builtIn": true, + "createdAt": "2025-05-12T09:44:08.284000Z", + "updatedAt": "2025-05-12T09:44:08.284000Z" } Since the realm ID is present, we can use the ``realm`` property too. And just to be extremely sure, we verify that the diff --git a/flame_hub/_auth_client.py b/flame_hub/_auth_client.py index 8f7a093..2bfbc43 100644 --- a/flame_hub/_auth_client.py +++ b/flame_hub/_auth_client.py @@ -3,7 +3,8 @@ import typing as t import typing_extensions as te -from pydantic import BaseModel, Field, WrapValidator, EmailStr +from pydantic import BaseModel, Field, WrapValidator, EmailStr, ConfigDict +from pydantic.alias_generators import to_camel from flame_hub._base_client import ( BaseClient, @@ -23,13 +24,22 @@ from flame_hub._defaults import DEFAULT_AUTH_BASE_URL -class CreateRealm(BaseModel): +class AuthBaseModel(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + validate_by_alias=True, + validate_by_name=True, + serialize_by_alias=True, + ) + + +class CreateRealm(AuthBaseModel): name: str display_name: str | None description: str | None -class UpdateRealm(BaseModel): +class UpdateRealm(AuthBaseModel): name: str | UNSET_T = UNSET display_name: str | None | UNSET_T = UNSET description: str | None | UNSET_T = UNSET @@ -42,7 +52,7 @@ class Realm(CreateRealm): updated_at: datetime -class CreateUser(BaseModel): +class CreateUser(AuthBaseModel): name: str display_name: str | None email: t.Annotated[EmailStr, IsOptionalField] = None @@ -62,7 +72,7 @@ class User(CreateUser): updated_at: datetime -class UpdateUser(BaseModel): +class UpdateUser(AuthBaseModel): name: str | UNSET_T = UNSET display_name: str | UNSET_T = UNSET email: str | None | UNSET_T = UNSET @@ -73,7 +83,7 @@ class UpdateUser(BaseModel): password: str | None | UNSET_T = UNSET -class CreatePermission(BaseModel): +class CreatePermission(AuthBaseModel): name: str display_name: str | None description: str | None @@ -90,7 +100,7 @@ class Permission(CreatePermission): realm: t.Annotated[Realm | None, IsIncludable] = None -class UpdatePermission(BaseModel): +class UpdatePermission(AuthBaseModel): name: str | UNSET_T = UNSET display_name: str | None | UNSET_T = UNSET description: str | None | UNSET_T = UNSET @@ -98,7 +108,7 @@ class UpdatePermission(BaseModel): policy_id: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET -class CreateRole(BaseModel): +class CreateRole(AuthBaseModel): name: str display_name: str | None description: str | None @@ -113,13 +123,13 @@ class Role(CreateRole): realm: t.Annotated[Realm | None, IsIncludable] = None -class UpdateRole(BaseModel): +class UpdateRole(AuthBaseModel): name: str | UNSET_T = UNSET display_name: str | None | UNSET_T = UNSET description: str | None | UNSET_T = UNSET -class CreateRolePermission(BaseModel): +class CreateRolePermission(AuthBaseModel): role_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] permission_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] @@ -137,7 +147,7 @@ class RolePermission(CreateRolePermission): permission_realm: t.Annotated[Realm | None, IsIncludable] = None -class CreateUserPermission(BaseModel): +class CreateUserPermission(AuthBaseModel): user_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] permission_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] @@ -155,7 +165,7 @@ class UserPermission(CreateUserPermission): user_realm: t.Annotated[Realm | None, IsIncludable] = None -class CreateUserRole(BaseModel): +class CreateUserRole(AuthBaseModel): user_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] role_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] @@ -172,7 +182,7 @@ class UserRole(CreateUserRole): role_realm: t.Annotated[Realm | None, IsIncludable] = None -class CreateClient(BaseModel): +class CreateClient(AuthBaseModel): name: str secret: str | None display_name: str | None @@ -185,7 +195,7 @@ class CreateClient(BaseModel): realm_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] -class Client(BaseModel): +class Client(AuthBaseModel): id: uuid.UUID name: str built_in: bool @@ -205,7 +215,7 @@ class Client(BaseModel): realm: t.Annotated[Realm, IsIncludable] = None -class UpdateClient(BaseModel): +class UpdateClient(AuthBaseModel): name: str | UNSET_T = UNSET secret: str | None | UNSET_T = UNSET display_name: str | None | UNSET_T = UNSET diff --git a/tests/test_auth.py b/tests/test_auth.py index 01092c7..a266b82 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -184,7 +184,6 @@ def test_get_role_not_found(auth_client): assert auth_client.get_role(next_uuid()) is None -@pytest.mark.xfail(reason="bug in authup, see https://github.com/authup/authup/issues/2661") def test_get_roles(auth_client, role, role_includables): roles_get = auth_client.get_roles() @@ -192,7 +191,6 @@ def test_get_roles(auth_client, role, role_includables): assert all(includable in r.model_fields_set for r in roles_get for includable in role_includables) -@pytest.mark.xfail(reason="bug in authup, see https://github.com/authup/authup/issues/2661") def test_find_roles(auth_client, role, role_includables): roles_find = auth_client.find_roles(filter={"id": role.id})