Skip to content

Commit b812c14

Browse files
authored
Merge pull request #100 from PrivateAIM/hub-0.8.25
Update to Hub v0.8.25
2 parents a9eca87 + 921eded commit b812c14

17 files changed

Lines changed: 585 additions & 181 deletions

.env.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ PYTEST_ADMIN_PASSWORD=start123
77
PYTEST_DEFAULT_MASTER_IMAGE=python/base
88
PYTEST_ASYNC_MAX_RETRIES=5
99
PYTEST_ASYNC_RETRY_DELAY_MILLIS=500
10-
PYTEST_HUB_VERSION=0.8.21
10+
PYTEST_HUB_VERSION=0.8.25

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ print(my_node.model_dump_json(indent=2))
8080
"registry": null,
8181
"registry_project_id": null,
8282
"registry_project": null,
83-
"robot_id": "200aab68-a686-407c-a6c1-2dd367ff6031",
83+
"robot_id": null,
84+
"client_id": "2d3e19b4-6708-4279-b2a7-34ad42638e4b",
8485
"created_at": "2025-05-19T15:43:57.859000Z",
8586
"updated_at": "2025-05-19T15:43:57.859000Z"
8687
}

docs/testing.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Tests for the FLAME Hub Client are implemented with `pytest <https://docs.pytest
99

1010
This assumes that an virtual environment has been setup and activated with `poetry <https://python-poetry.org/>`_.
1111

12-
Furthermore, tests require access to a FLAME Hub instance. There are two way of accomplishing this - either by using
12+
Furthermore, tests require access to a FLAME Hub instance. There are two ways of accomplishing this - either by using
1313
`testcontainers <https://testcontainers-python.readthedocs.io/en/latest/>`_ or by deploying your own instance.
1414

1515

@@ -25,13 +25,13 @@ development, it is highly recommended to set up you own Hub instance instead.
2525
Deploying your own Hub instance
2626
===============================
2727

28-
Grab the `Docker compose file <https://raw.githubusercontent.com/PrivateAIM/hub/refs/heads/master/docker-compose.yml>`_
29-
from the Hub repository and store it somewhere warm and comfy. For ``core``, ``messenger``, ``analysis-manager``,
30-
``storage`` and ``ui`` services, remove the ``build`` property and replace it with
31-
``image: ghcr.io/privateaim/hub:HUB_VERSION``. The latest version of the FLAME Hub Client that is tested with the Hub is
32-
|hub_version|. Now you can run :console:`docker compose up -d` and, after a few minutes, you will be able to access the
33-
UI at http://localhost:3000.
28+
Clone the Hub deployment repository :console:`git clone https://github.com/PrivateAIM/hub-deployment.git` and navigate
29+
to the ``docker-compose`` directory :console:`cd hub-deployment/docker-compose`. Copy the ``.env.example`` file with
30+
:console:`cp .env.example .env`. Edit the new ``.env`` file and change the ``HUB_IMAGE_TAG`` variable if you need a
31+
specific version of the Hub. The latest version of the FLAME Hub Client is tested with the Hub version |hub_version|.
32+
Now you can run :console:`docker compose up -d` and, after a few minutes, you will be able to access the UI at
33+
http://localhost:3000.
3434

35-
In order for ``pytest`` to pick up on the locally deployed instance, run :console:`cp .env.test .env` and modify the
36-
:file:`.env` file such that ``PYTEST_USE_TESTCONTAINERS=0``. This will skip the creation of all test containers and make
37-
test setup much faster.
35+
In order for ``pytest`` to pick up on the locally deployed instance, run :console:`cp .env.test .env` inside the
36+
``hub-python-client`` directory and modify the :file:`.env` file such that ``PYTEST_USE_TESTCONTAINERS=0``. This will
37+
skip the creation of all test containers and make test setup much faster.

flame_hub/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"__version_info__",
1313
]
1414

15+
import warnings
16+
1517
from . import auth, types, models
1618

1719
from ._auth_client import AuthClient
@@ -20,3 +22,7 @@
2022
from ._core_client import CoreClient
2123
from ._storage_client import StorageClient
2224
from ._version import __version__, __version_info__
25+
26+
27+
# Show deprecation warnings per default.
28+
warnings.simplefilter("default", DeprecationWarning)

flame_hub/_auth_client.py

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,15 @@ class Realm(CreateRealm):
4343
class CreateUser(BaseModel):
4444
name: str
4545
display_name: str | None
46-
email: t.Annotated[str | None, IsOptionalField]
46+
email: t.Annotated[EmailStr, IsOptionalField] = None
4747
active: bool
4848
name_locked: bool
4949
first_name: str | None
5050
last_name: str | None
5151

5252

53-
class User(BaseModel):
53+
class User(CreateUser):
5454
id: uuid.UUID
55-
name: str
56-
active: bool
57-
name_locked: bool
58-
email: t.Annotated[EmailStr, IsOptionalField] = None
59-
display_name: str | None
60-
first_name: str | None
61-
last_name: str | None
6255
avatar: str | None
6356
cover: str | None
6457
realm_id: uuid.UUID
@@ -237,6 +230,43 @@ class RobotRole(CreateRobotRole):
237230
role_realm: t.Annotated[Realm | None, IsIncludable] = None
238231

239232

233+
class CreateClient(BaseModel):
234+
name: str
235+
secret: t.Annotated[str | None, IsOptionalField] = None
236+
display_name: str | None
237+
description: str | None
238+
redirect_uri: str | None
239+
active: bool
240+
is_confidential: bool
241+
secret_hashed: bool
242+
grant_types: str | None
243+
realm_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)]
244+
245+
246+
class Client(CreateClient):
247+
id: uuid.UUID
248+
built_in: bool
249+
secret_encrypted: bool
250+
scope: str | None
251+
base_url: str | None
252+
root_url: str | None
253+
created_at: datetime
254+
updated_at: datetime
255+
realm: t.Annotated[Realm, IsIncludable] = None
256+
257+
258+
class UpdateClient(BaseModel):
259+
name: str | UNSET_T = UNSET
260+
secret: str | None | UNSET_T = UNSET
261+
display_name: str | None | UNSET_T = UNSET
262+
description: str | None | UNSET_T = UNSET
263+
redirect_uri: str | None | UNSET_T = UNSET
264+
active: bool | UNSET_T = UNSET
265+
is_confidential: bool | UNSET_T = UNSET
266+
secret_hashed: bool | UNSET_T = UNSET
267+
grant_types: str | None | UNSET_T = UNSET
268+
269+
240270
class AuthClient(BaseClient):
241271
"""The client which implements all auth endpoints.
242272
@@ -642,3 +672,75 @@ def get_robot_roles(self, **params: te.Unpack[GetKwargs]) -> list[RobotRole]:
642672

643673
def find_robot_roles(self, **params: te.Unpack[FindAllKwargs]) -> list[RobotRole]:
644674
return self._find_all_resources(RobotRole, "robot-roles", include=get_includable_names(RobotRole), **params)
675+
676+
def create_client(
677+
self,
678+
name: str,
679+
realm_id: Realm | str | uuid.UUID,
680+
secret: str = None,
681+
display_name: str = None,
682+
description: str = None,
683+
redirect_uri: str = None,
684+
active: bool = True,
685+
is_confidential: bool = True,
686+
secret_hashed: bool = False,
687+
grant_types: str = None,
688+
) -> Client:
689+
return self._create_resource(
690+
Client,
691+
CreateClient(
692+
name=name,
693+
realm_id=realm_id,
694+
secret=secret,
695+
display_name=display_name,
696+
description=description,
697+
redirect_uri=redirect_uri,
698+
active=active,
699+
is_confidential=is_confidential,
700+
secret_hashed=secret_hashed,
701+
grant_types=grant_types,
702+
),
703+
"clients",
704+
)
705+
706+
def delete_client(self, client_id: Client | uuid.UUID | str):
707+
self._delete_resource("clients", client_id)
708+
709+
def get_client(self, client_id: Client | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Client | None:
710+
return self._get_single_resource(Client, "clients", client_id, include=get_includable_names(Client), **params)
711+
712+
def get_clients(self, **params: te.Unpack[GetKwargs]) -> list[Client]:
713+
return self._get_all_resources(Client, "clients", include=get_includable_names(Client), **params)
714+
715+
def find_clients(self, **params: te.Unpack[FindAllKwargs]) -> list[Client]:
716+
return self._find_all_resources(Client, "clients", include=get_includable_names(Client), **params)
717+
718+
def update_client(
719+
self,
720+
client_id: Client | uuid.UUID | str,
721+
name: str | UNSET_T = UNSET,
722+
secret: str | None | UNSET_T = UNSET,
723+
display_name: str | None | UNSET_T = UNSET,
724+
description: str | None | UNSET_T = UNSET,
725+
redirect_uri: str | None | UNSET_T = UNSET,
726+
active: bool | UNSET_T = UNSET,
727+
is_confidential: bool | UNSET_T = UNSET,
728+
secret_hashed: bool | UNSET_T = UNSET,
729+
grant_types: str | None | UNSET_T = UNSET,
730+
) -> Client:
731+
return self._update_resource(
732+
Client,
733+
UpdateClient(
734+
name=name,
735+
secret=secret,
736+
display_name=display_name,
737+
description=description,
738+
redirect_uri=redirect_uri,
739+
active=active,
740+
is_confidential=is_confidential,
741+
secret_hashed=secret_hashed,
742+
grant_types=grant_types,
743+
),
744+
"clients",
745+
client_id,
746+
)

flame_hub/_auth_flows.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import time
22
import typing as t
3+
import warnings
34

45
import httpx
56
from pydantic import BaseModel
@@ -62,6 +63,11 @@ def __init__(
6263
self._current_token_expires_at_nanos = 0
6364
self._client = client or httpx.Client(base_url=base_url)
6465

66+
warnings.warn(
67+
"'RobotAuth' is deprecated and will be removed in a future version. Please use 'ClientAuth' instead.",
68+
category=DeprecationWarning,
69+
)
70+
6571
def auth_flow(self, request) -> t.Iterator[httpx.Request]:
6672
"""Executes the robot authentication flow.
6773
@@ -100,6 +106,83 @@ def auth_flow(self, request) -> t.Iterator[httpx.Request]:
100106
yield request
101107

102108

109+
class ClientAuth(httpx.Auth):
110+
"""Client authentication for the FLAME Hub.
111+
112+
This class implements a client authentication flow which is one possible flow that is recognized by the FLAME Hub.
113+
It is derived from the ``httpx`` base class for all authentication flows ``httpx.Auth``. For more information about
114+
this base class, click
115+
`here <https://www.python-httpx.org/advanced/authentication/#custom-authentication-schemes>`_. Note that
116+
``base_url`` is ignored if you pass your own client via the ``client`` keyword argument. An instance of this class
117+
could be used for authentication to access the Hub endpoints via the clients.
118+
119+
Parameters
120+
----------
121+
client_id : :py:class:`str`
122+
The ID of the client which is used to execute the authentication flow.
123+
client_secret : :py:class:`str`
124+
The secret which corresponds to the client with ID ``client_id``.
125+
base_url : :py:class:`str`, default=\\ :py:const:`~flame_hub._defaults.DEFAULT_AUTH_BASE_URL`
126+
The base URL for the authentication flow.
127+
client : :py:class:`httpx.Client`
128+
Pass your own client to avoid the instantiation of a client while initializing an instance of this class.
129+
130+
See Also
131+
--------
132+
:py:class:`.AuthClient`, :py:class:`.CoreClient`, :py:class:`.StorageClient`
133+
"""
134+
135+
def __init__(
136+
self,
137+
client_id: str,
138+
client_secret: str,
139+
base_url: str = DEFAULT_AUTH_BASE_URL,
140+
client: httpx.Client = None,
141+
):
142+
self._client_id = client_id
143+
self._client_secret = client_secret
144+
self._current_token = None
145+
self._current_token_expires_at_nanos = 0
146+
self._client = client or httpx.Client(base_url=base_url)
147+
148+
def auth_flow(self, request) -> t.Iterator[httpx.Request]:
149+
"""Executes the client authentication flow.
150+
151+
This method checks if the current access token is not set or expired and, if so, requests a new one from the Hub
152+
instance. It then yields the authentication request. Click
153+
`here <https://www.python-httpx.org/advanced/authentication/#custom-authentication-schemes>`_ for further
154+
information on this method.
155+
156+
See Also
157+
--------
158+
:py:class:`.AccessToken`
159+
"""
160+
161+
# Check if token is not set or current token is expired.
162+
if self._current_token is None or time.monotonic_ns() > self._current_token_expires_at_nanos:
163+
request_nanos = time.monotonic_ns()
164+
165+
r = self._client.post(
166+
"token",
167+
json={
168+
"grant_type": "client_credentials",
169+
"client_id": self._client_id,
170+
"client_secret": self._client_secret,
171+
},
172+
)
173+
174+
if r.status_code != httpx.codes.OK.value:
175+
raise new_hub_api_error_from_response(r)
176+
177+
at = AccessToken(**r.json())
178+
179+
self._current_token = at
180+
self._current_token_expires_at_nanos = request_nanos + secs_to_nanos(at.expires_in)
181+
182+
request.headers["Authorization"] = f"Bearer {self._current_token.access_token}"
183+
yield request
184+
185+
103186
class PasswordAuth(httpx.Auth):
104187
"""Password authentication for the FLAME Hub.
105188

0 commit comments

Comments
 (0)