Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"
}
```

Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. include:: ../CHANGELOG.md
:parser: myst
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
user_guide
testing
api
changelog
34 changes: 16 additions & 18 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
pbrassel marked this conversation as resolved.
* 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -215,29 +213,29 @@ 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
Comment thread
pbrassel marked this conversation as resolved.

If you are ever unsure which fields can be requested this way on a specific resource, use :py:func:`.get_field_names`
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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 25 additions & 15 deletions flame_hub/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -90,15 +100,15 @@ 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
realm_id: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET
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
Expand All @@ -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)]

Expand All @@ -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)]

Expand All @@ -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)]

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,13 @@ 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()

assert len(roles_get) > 0
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})

Expand Down