Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e78b07b
fix(storage): make `actor_id` and `actor_type` for buckets optional
pbrassel Feb 3, 2026
e7eb50f
fix(core): make `robot_id` on nodes optional since it is deprecated
pbrassel Feb 3, 2026
75715b5
fix(test): use `virtual_path` to filter for `master_image` fixture
pbrassel Feb 3, 2026
c081963
feat(core): `build_status` and `build_progress` attributes for master…
pbrassel Feb 3, 2026
5cc296e
fix(core): update analysis model
pbrassel Feb 4, 2026
8536383
fix(core): validate `build_progress` in master images as an integer
pbrassel Feb 4, 2026
9130c25
fix: `AnalysisNodeApprovalStatus` and `AnalysisNodeRunStatus` are no …
pbrassel Feb 4, 2026
77e6568
fix(core): update analysis node model
pbrassel Feb 4, 2026
0549187
test(core): update analysis node models
pbrassel Feb 4, 2026
fc876ac
feat: make `AnalysisBucketType` an enum to remove hard-coded bucket t…
pbrassel Feb 4, 2026
d52088a
fix: set higher read timeout since creating registry projects takes a…
pbrassel Feb 4, 2026
b6b9b99
feat(auth): client resource
pbrassel Feb 4, 2026
82bcf07
refactor(auth): let `User` inherit from `CreateUser`
pbrassel Feb 4, 2026
698f21a
feat(auth): client authentication
pbrassel Feb 4, 2026
0ac79e0
feat(auth): show a deprecation warning for `RobotAuth`
pbrassel Feb 4, 2026
b8145b3
feat: show deprecation warnings per default
pbrassel Feb 4, 2026
7018a3c
fix(core): update `Log` model
pbrassel Feb 4, 2026
0a96613
fix(core): add `client_id` attribute for nodes
pbrassel Feb 5, 2026
7799cd5
feat: make expected code for Hub responses variable in `BaseClient` m…
pbrassel Feb 5, 2026
a4cbc22
refactor(core): creating analysis nodes logs now uses the intended `B…
pbrassel Feb 5, 2026
25021f9
test(refactor): improve test for analysis node logs
pbrassel Feb 5, 2026
18e4909
fix(test): use `nginx.conf` from `hub-deployment` repository
pbrassel Feb 9, 2026
5cb307f
docs: update readme
pbrassel Feb 9, 2026
b6b085c
chore: update hub version
pbrassel Feb 9, 2026
a5672f9
test: test for deprecation warnings
pbrassel Feb 9, 2026
6f69e27
docs: reason for read timeout
pbrassel Feb 10, 2026
9c8586a
feat(storage): add `realm_id` attribute for `BucketFile`
pbrassel Feb 10, 2026
0b19281
fix(core): adapt analysis bucket and analysis bucket file models
pbrassel Feb 10, 2026
e2b6ee0
fix: correct return type for `delete_analysis_bucket_file()` method
pbrassel Feb 10, 2026
8ea3258
fix(core): update analysis commands
pbrassel Feb 10, 2026
13b459b
docs: update section about deploying a hub instance
pbrassel Feb 10, 2026
def7378
fix(test): check for integers in `next_random_number()` helper function
pbrassel Feb 10, 2026
921eded
fix(test): pin `nginx.conf` to specific commit
pbrassel Feb 10, 2026
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.8.21
PYTEST_HUB_VERSION=0.8.25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix dotenv-linter key order warning.

Line 10 is flagged by dotenv-linter for key ordering. Move PYTEST_HUB_VERSION above PYTEST_STORAGE_BASE_URL.

🔧 Suggested reorder
 PYTEST_CORE_BASE_URL=http://localhost:3000/core/
 PYTEST_AUTH_BASE_URL=http://localhost:3000/auth/
-PYTEST_STORAGE_BASE_URL=http://localhost:3000/storage/
+PYTEST_HUB_VERSION=0.8.25
+PYTEST_STORAGE_BASE_URL=http://localhost:3000/storage/
 PYTEST_ADMIN_USERNAME=admin
 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.8.25
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PYTEST_HUB_VERSION=0.8.25
PYTEST_CORE_BASE_URL=http://localhost:3000/core/
PYTEST_AUTH_BASE_URL=http://localhost:3000/auth/
PYTEST_HUB_VERSION=0.8.25
PYTEST_STORAGE_BASE_URL=http://localhost:3000/storage/
PYTEST_ADMIN_USERNAME=admin
PYTEST_ADMIN_PASSWORD=start123
PYTEST_DEFAULT_MASTER_IMAGE=python/base
PYTEST_ASYNC_MAX_RETRIES=5
PYTEST_ASYNC_RETRY_DELAY_MILLIS=500
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 10-10: [UnorderedKey] The PYTEST_HUB_VERSION key should go before the PYTEST_STORAGE_BASE_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
In @.env.test at line 10, Dotenv-linter complains about key ordering for
PYTEST_HUB_VERSION; fix it by moving the PYTEST_HUB_VERSION entry so it appears
above the PYTEST_STORAGE_BASE_URL entry in the env file, preserving the exact
key/value and ensuring the rest of the key order remains unchanged.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ print(my_node.model_dump_json(indent=2))
"registry": null,
"registry_project_id": null,
"registry_project": null,
"robot_id": "200aab68-a686-407c-a6c1-2dd367ff6031",
"robot_id": null,
"client_id": "2d3e19b4-6708-4279-b2a7-34ad42638e4b",
"created_at": "2025-05-19T15:43:57.859000Z",
"updated_at": "2025-05-19T15:43:57.859000Z"
}
Expand Down
20 changes: 10 additions & 10 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Tests for the FLAME Hub Client are implemented with `pytest <https://docs.pytest

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

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


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

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

In order for ``pytest`` to pick up on the locally deployed instance, run :console:`cp .env.test .env` and modify the
:file:`.env` file such that ``PYTEST_USE_TESTCONTAINERS=0``. This will skip the creation of all test containers and make
test setup much faster.
In order for ``pytest`` to pick up on the locally deployed instance, run :console:`cp .env.test .env` inside the
``hub-python-client`` directory and modify the :file:`.env` file such that ``PYTEST_USE_TESTCONTAINERS=0``. This will
skip the creation of all test containers and make test setup much faster.
6 changes: 6 additions & 0 deletions flame_hub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"__version_info__",
]

import warnings

from . import auth, types, models

from ._auth_client import AuthClient
Expand All @@ -20,3 +22,7 @@
from ._core_client import CoreClient
from ._storage_client import StorageClient
from ._version import __version__, __version_info__


# Show deprecation warnings per default.
warnings.simplefilter("default", DeprecationWarning)
120 changes: 111 additions & 9 deletions flame_hub/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,15 @@ class Realm(CreateRealm):
class CreateUser(BaseModel):
name: str
display_name: str | None
email: t.Annotated[str | None, IsOptionalField]
email: t.Annotated[EmailStr, IsOptionalField] = None
active: bool
name_locked: bool
first_name: str | None
last_name: str | None


class User(BaseModel):
class User(CreateUser):
id: uuid.UUID
name: str
active: bool
name_locked: bool
email: t.Annotated[EmailStr, IsOptionalField] = None
display_name: str | None
first_name: str | None
last_name: str | None
avatar: str | None
cover: str | None
realm_id: uuid.UUID
Expand Down Expand Up @@ -237,6 +230,43 @@ class RobotRole(CreateRobotRole):
role_realm: t.Annotated[Realm | None, IsIncludable] = None


class CreateClient(BaseModel):
name: str
secret: t.Annotated[str | None, IsOptionalField] = None
display_name: str | None
description: str | None
redirect_uri: str | None
active: bool
is_confidential: bool
secret_hashed: bool
grant_types: str | None
realm_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)]


class Client(CreateClient):
id: uuid.UUID
built_in: bool
secret_encrypted: bool
scope: str | None
base_url: str | None
root_url: str | None
created_at: datetime
updated_at: datetime
realm: t.Annotated[Realm, IsIncludable] = None


class UpdateClient(BaseModel):
name: str | UNSET_T = UNSET
secret: str | None | UNSET_T = UNSET
display_name: str | None | UNSET_T = UNSET
description: str | None | UNSET_T = UNSET
redirect_uri: str | None | UNSET_T = UNSET
active: bool | UNSET_T = UNSET
is_confidential: bool | UNSET_T = UNSET
secret_hashed: bool | UNSET_T = UNSET
grant_types: str | None | UNSET_T = UNSET


class AuthClient(BaseClient):
"""The client which implements all auth endpoints.

Expand Down Expand Up @@ -642,3 +672,75 @@ def get_robot_roles(self, **params: te.Unpack[GetKwargs]) -> list[RobotRole]:

def find_robot_roles(self, **params: te.Unpack[FindAllKwargs]) -> list[RobotRole]:
return self._find_all_resources(RobotRole, "robot-roles", include=get_includable_names(RobotRole), **params)

def create_client(
self,
name: str,
realm_id: Realm | str | uuid.UUID,
secret: str = None,
display_name: str = None,
description: str = None,
redirect_uri: str = None,
active: bool = True,
is_confidential: bool = True,
secret_hashed: bool = False,
grant_types: str = None,
) -> Client:
return self._create_resource(
Client,
CreateClient(
name=name,
realm_id=realm_id,
secret=secret,
display_name=display_name,
description=description,
redirect_uri=redirect_uri,
active=active,
is_confidential=is_confidential,
secret_hashed=secret_hashed,
grant_types=grant_types,
),
"clients",
)

def delete_client(self, client_id: Client | uuid.UUID | str):
self._delete_resource("clients", client_id)

def get_client(self, client_id: Client | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Client | None:
return self._get_single_resource(Client, "clients", client_id, include=get_includable_names(Client), **params)

def get_clients(self, **params: te.Unpack[GetKwargs]) -> list[Client]:
return self._get_all_resources(Client, "clients", include=get_includable_names(Client), **params)

def find_clients(self, **params: te.Unpack[FindAllKwargs]) -> list[Client]:
return self._find_all_resources(Client, "clients", include=get_includable_names(Client), **params)

def update_client(
self,
client_id: Client | uuid.UUID | str,
name: str | UNSET_T = UNSET,
secret: str | None | UNSET_T = UNSET,
display_name: str | None | UNSET_T = UNSET,
description: str | None | UNSET_T = UNSET,
redirect_uri: str | None | UNSET_T = UNSET,
active: bool | UNSET_T = UNSET,
is_confidential: bool | UNSET_T = UNSET,
secret_hashed: bool | UNSET_T = UNSET,
grant_types: str | None | UNSET_T = UNSET,
) -> Client:
return self._update_resource(
Client,
UpdateClient(
name=name,
secret=secret,
display_name=display_name,
description=description,
redirect_uri=redirect_uri,
active=active,
is_confidential=is_confidential,
secret_hashed=secret_hashed,
grant_types=grant_types,
),
"clients",
client_id,
)
83 changes: 83 additions & 0 deletions flame_hub/_auth_flows.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
import typing as t
import warnings

import httpx
from pydantic import BaseModel
Expand Down Expand Up @@ -62,6 +63,11 @@ def __init__(
self._current_token_expires_at_nanos = 0
self._client = client or httpx.Client(base_url=base_url)

warnings.warn(
"'RobotAuth' is deprecated and will be removed in a future version. Please use 'ClientAuth' instead.",
category=DeprecationWarning,
)

def auth_flow(self, request) -> t.Iterator[httpx.Request]:
"""Executes the robot authentication flow.

Expand Down Expand Up @@ -100,6 +106,83 @@ def auth_flow(self, request) -> t.Iterator[httpx.Request]:
yield request


class ClientAuth(httpx.Auth):
"""Client authentication for the FLAME Hub.

This class implements a client authentication flow which is one possible flow that is recognized by the FLAME Hub.
It is derived from the ``httpx`` base class for all authentication flows ``httpx.Auth``. For more information about
this base class, click
`here <https://www.python-httpx.org/advanced/authentication/#custom-authentication-schemes>`_. Note that
``base_url`` is ignored if you pass your own client via the ``client`` keyword argument. An instance of this class
could be used for authentication to access the Hub endpoints via the clients.

Parameters
----------
client_id : :py:class:`str`
The ID of the client which is used to execute the authentication flow.
client_secret : :py:class:`str`
The secret which corresponds to the client with ID ``client_id``.
base_url : :py:class:`str`, default=\\ :py:const:`~flame_hub._defaults.DEFAULT_AUTH_BASE_URL`
The base URL for the authentication flow.
client : :py:class:`httpx.Client`
Pass your own client to avoid the instantiation of a client while initializing an instance of this class.

See Also
--------
:py:class:`.AuthClient`, :py:class:`.CoreClient`, :py:class:`.StorageClient`
"""

def __init__(
self,
client_id: str,
client_secret: str,
base_url: str = DEFAULT_AUTH_BASE_URL,
client: httpx.Client = None,
):
self._client_id = client_id
self._client_secret = client_secret
self._current_token = None
self._current_token_expires_at_nanos = 0
self._client = client or httpx.Client(base_url=base_url)

def auth_flow(self, request) -> t.Iterator[httpx.Request]:
"""Executes the client authentication flow.

This method checks if the current access token is not set or expired and, if so, requests a new one from the Hub
instance. It then yields the authentication request. Click
`here <https://www.python-httpx.org/advanced/authentication/#custom-authentication-schemes>`_ for further
information on this method.

See Also
--------
:py:class:`.AccessToken`
"""

# Check if token is not set or current token is expired.
if self._current_token is None or time.monotonic_ns() > self._current_token_expires_at_nanos:
request_nanos = time.monotonic_ns()

r = self._client.post(
"token",
json={
"grant_type": "client_credentials",
"client_id": self._client_id,
"client_secret": self._client_secret,
},
)

if r.status_code != httpx.codes.OK.value:
raise new_hub_api_error_from_response(r)

at = AccessToken(**r.json())

self._current_token = at
self._current_token_expires_at_nanos = request_nanos + secs_to_nanos(at.expires_in)

request.headers["Authorization"] = f"Bearer {self._current_token.access_token}"
yield request


class PasswordAuth(httpx.Auth):
"""Password authentication for the FLAME Hub.

Expand Down
Loading