From c2f252b6b2b7f851aa2c26fec813bde67e6e2e54 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 23 Jun 2026 10:29:17 +0200 Subject: [PATCH 1/2] feat: support overriding authentication per request Add an optional keyword-only `auth` parameter to every public client method (AuthClient, CoreClient, StorageClient) to override authentication for a single request, leaving the client's bound authentication in place for all others. `auth` accepts an httpx.Auth instance, a (username, password) tuple, or a string sent verbatim as the Authorization header. Following httpx conventions, omitting it keeps the client's bound auth and `auth=None` disables authentication for that request. --- README.md | 21 ++ flame_hub/_auth_client.py | 368 ++++++++++++++++++++++------- flame_hub/_base_client.py | 115 +++++++++- flame_hub/_core_client.py | 434 +++++++++++++++++++++++++++-------- flame_hub/_storage_client.py | 88 +++++-- flame_hub/types.py | 4 + tests/test_base_client.py | 167 ++++++++++++++ 7 files changed, 986 insertions(+), 211 deletions(-) diff --git a/README.md b/README.md index 5cdcc2d..749e2ef 100644 --- a/README.md +++ b/README.md @@ -106,5 +106,26 @@ core_client.delete_node(my_node) assert core_client.get_node(my_node.id) is None ``` +## Overriding authentication per request + +By default every request uses the authentication you passed to the client. Every client method also accepts a +keyword-only `auth` argument to override authentication for that single request. It accepts either an `httpx.Auth` +instance (such as another `PasswordAuth`/`ClientAuth`), a `(username, password)` tuple for HTTP basic authentication, +or a string that is sent verbatim as the `Authorization` header. + +```python +# use a raw header value for this request only +core_client.get_nodes(auth="Bearer ") + +# or use a dedicated authenticator for this request only +core_client.create_node(name="my-node", realm_id=master_realm, auth=other_auth) +``` + +The `auth` argument follows `httpx` conventions: + +- omit it (the default) to use the authentication bound to the client, +- pass an override (`httpx.Auth`, `(username, password)` tuple or `Authorization` header string) to replace it, or +- pass `auth=None` to send the request without any authentication. + Note that not all method types are implemented for each resource. Check out the [documentation](https://privateaim.github.io/hub-python-client/) to see which methods are available. diff --git a/flame_hub/_auth_client.py b/flame_hub/_auth_client.py index 229b00f..90c339d 100644 --- a/flame_hub/_auth_client.py +++ b/flame_hub/_auth_client.py @@ -16,6 +16,8 @@ get_includable_names, UNSET, UNSET_T, + RequestAuthArg, + USE_CLIENT_DEFAULT, ) from flame_hub._defaults import DEFAULT_AUTH_BASE_URL from flame_hub._auth_flows import ClientAuth, PasswordAuth @@ -298,13 +300,17 @@ def __init__( ): super().__init__(base_url, auth, **kwargs) - def get_realms(self, **params: te.Unpack[GetKwargs]) -> list[Realm]: - return self._get_all_resources(Realm, "realms", **params) + def get_realms(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs]) -> list[Realm]: + return self._get_all_resources(Realm, "realms", auth=auth, **params) - def find_realms(self, **params: te.Unpack[FindAllKwargs]) -> list[Realm]: - return self._find_all_resources(Realm, "realms", **params) + def find_realms( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Realm]: + return self._find_all_resources(Realm, "realms", auth=auth, **params) - def create_realm(self, name: str, display_name: str = None, description: str = None) -> Realm: + def create_realm( + self, name: str, display_name: str = None, description: str = None, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ) -> Realm: return self._create_resource( Realm, CreateRealm( @@ -313,13 +319,20 @@ def create_realm(self, name: str, display_name: str = None, description: str = N description=description, ), "realms", + auth=auth, ) - def delete_realm(self, realm_id: Realm | uuid.UUID | str): - self._delete_resource("realms", realm_id) + def delete_realm(self, realm_id: Realm | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("realms", realm_id, auth=auth) - def get_realm(self, realm_id: Realm | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Realm | None: - return self._get_single_resource(Realm, "realms", realm_id, **params) + def get_realm( + self, + realm_id: Realm | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Realm | None: + return self._get_single_resource(Realm, "realms", realm_id, auth=auth, **params) def update_realm( self, @@ -327,6 +340,8 @@ def update_realm( name: str | UNSET_T = UNSET, display_name: str | None | UNSET_T = UNSET, description: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Realm: return self._update_resource( Realm, @@ -337,22 +352,38 @@ def update_realm( ), "realms", realm_id, + auth=auth, ) def create_robot( - self, name: str, realm_id: Realm | str | uuid.UUID, secret: str, display_name: str = None + self, + name: str, + realm_id: Realm | str | uuid.UUID, + secret: str, + display_name: str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Robot: return self._create_resource( Robot, CreateRobot(name=name, display_name=display_name, realm_id=realm_id, secret=secret), "robots", + auth=auth, ) - def delete_robot(self, robot_id: Robot | str | uuid.UUID): - self._delete_resource("robots", robot_id) + def delete_robot(self, robot_id: Robot | str | uuid.UUID, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("robots", robot_id, auth=auth) - def get_robot(self, robot_id: Robot | str | uuid.UUID, **params: te.Unpack[GetKwargs]) -> Robot | None: - return self._get_single_resource(Robot, "robots", robot_id, include=get_includable_names(Robot), **params) + def get_robot( + self, + robot_id: Robot | str | uuid.UUID, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Robot | None: + return self._get_single_resource( + Robot, "robots", robot_id, include=get_includable_names(Robot), auth=auth, **params + ) def update_robot( self, @@ -361,19 +392,24 @@ def update_robot( display_name: str | None | UNSET_T = UNSET, realm_id: Realm | str | uuid.UUID | UNSET_T = UNSET, secret: str | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Robot: return self._update_resource( Robot, UpdateRobot(name=name, display_name=display_name, realm_id=realm_id, secret=secret), "robots", robot_id, + auth=auth, ) - def get_robots(self, **params: te.Unpack[GetKwargs]) -> list[Robot]: - return self._get_all_resources(Robot, "robots", include=get_includable_names(Robot), **params) + def get_robots(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs]) -> list[Robot]: + return self._get_all_resources(Robot, "robots", include=get_includable_names(Robot), auth=auth, **params) - def find_robots(self, **params: te.Unpack[FindAllKwargs]) -> list[Robot]: - return self._find_all_resources(Robot, "robots", include=get_includable_names(Robot), **params) + def find_robots( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Robot]: + return self._find_all_resources(Robot, "robots", include=get_includable_names(Robot), auth=auth, **params) def create_permission( self, @@ -381,6 +417,8 @@ def create_permission( display_name: str = None, description: str = None, realm_id: Realm | uuid.UUID | str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Permission: return self._create_resource( Permission, @@ -392,17 +430,24 @@ def create_permission( policy_id=None, # TODO: add policies when hub implements them ), "permissions", + auth=auth, ) def get_permission( - self, permission_id: Permission | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + permission_id: Permission | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> Permission | None: return self._get_single_resource( - Permission, "permissions", permission_id, include=get_includable_names(Permission), **params + Permission, "permissions", permission_id, include=get_includable_names(Permission), auth=auth, **params ) - def delete_permission(self, permission_id: Permission | uuid.UUID | str): - self._delete_resource("permissions", permission_id) + def delete_permission( + self, permission_id: Permission | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("permissions", permission_id, auth=auth) def update_permission( self, @@ -411,32 +456,54 @@ def update_permission( display_name: str | None | UNSET_T = UNSET, description: str | None | UNSET_T = UNSET, realm_id: Realm | uuid.UUID | str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Permission: return self._update_resource( Permission, UpdatePermission(name=name, display_name=display_name, description=description, realm_id=realm_id), "permissions", permission_id, + auth=auth, ) - def get_permissions(self, **params: te.Unpack[GetKwargs]) -> list[Permission]: - return self._get_all_resources(Permission, "permissions", include=get_includable_names(Permission), **params) + def get_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[Permission]: + return self._get_all_resources( + Permission, "permissions", include=get_includable_names(Permission), auth=auth, **params + ) - def find_permissions(self, **params: te.Unpack[FindAllKwargs]) -> list[Permission]: - return self._find_all_resources(Permission, "permissions", include=get_includable_names(Permission), **params) + def find_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Permission]: + return self._find_all_resources( + Permission, "permissions", include=get_includable_names(Permission), auth=auth, **params + ) - def create_role(self, name: str, display_name: str = None, description: str = None) -> Role: + def create_role( + self, name: str, display_name: str = None, description: str = None, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ) -> Role: return self._create_resource( Role, CreateRole(name=name, display_name=display_name, description=description), "roles", + auth=auth, ) - def get_role(self, role_id: Role | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Role | None: - return self._get_single_resource(Role, "roles", role_id, include=get_includable_names(Role), **params) + def get_role( + self, + role_id: Role | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Role | None: + return self._get_single_resource( + Role, "roles", role_id, include=get_includable_names(Role), auth=auth, **params + ) - def delete_role(self, role_id: Role | uuid.UUID | str): - self._delete_resource("roles", role_id) + def delete_role(self, role_id: Role | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("roles", role_id, auth=auth) def update_role( self, @@ -444,56 +511,79 @@ def update_role( name: str | UNSET_T = UNSET, display_name: str | None | UNSET_T = UNSET, description: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Role: return self._update_resource( Role, UpdateRole(name=name, display_name=display_name, description=description), "roles", role_id, + auth=auth, ) - def get_roles(self, **params: te.Unpack[GetKwargs]) -> list[Role]: - return self._get_all_resources(Role, "roles", include=get_includable_names(Role), **params) + def get_roles(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs]) -> list[Role]: + return self._get_all_resources(Role, "roles", include=get_includable_names(Role), auth=auth, **params) - def find_roles(self, **params: te.Unpack[FindAllKwargs]) -> list[Role]: - return self._find_all_resources(Role, "roles", include=get_includable_names(Role), **params) + def find_roles( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Role]: + return self._find_all_resources(Role, "roles", include=get_includable_names(Role), auth=auth, **params) def create_role_permission( - self, role_id: Role | uuid.UUID | str, permission_id: Permission | uuid.UUID | str + self, + role_id: Role | uuid.UUID | str, + permission_id: Permission | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> RolePermission: return self._create_resource( RolePermission, CreateRolePermission(role_id=role_id, permission_id=permission_id), "role-permissions", + auth=auth, ) def get_role_permission( - self, role_permission_id: RolePermission | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + role_permission_id: RolePermission | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> RolePermission | None: return self._get_single_resource( RolePermission, "role-permissions", role_permission_id, include=get_includable_names(RolePermission), + auth=auth, **params, ) - def delete_role_permission(self, role_permission_id: RolePermission | uuid.UUID | str): - self._delete_resource("role-permissions", role_permission_id) + def delete_role_permission( + self, role_permission_id: RolePermission | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("role-permissions", role_permission_id, auth=auth) - def get_role_permissions(self, **params: te.Unpack[GetKwargs]) -> list[RolePermission]: + def get_role_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[RolePermission]: return self._get_all_resources( RolePermission, "role-permissions", include=get_includable_names(RolePermission), + auth=auth, **params, ) - def find_role_permissions(self, **params: te.Unpack[FindAllKwargs]) -> list[RolePermission]: + def find_role_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[RolePermission]: return self._find_all_resources( RolePermission, "role-permissions", include=get_includable_names(RolePermission), + auth=auth, **params, ) @@ -506,6 +596,8 @@ def create_user( name_locked: bool = False, first_name: str = None, last_name: str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> User: return self._create_resource( User, @@ -519,13 +611,22 @@ def create_user( last_name=last_name, ), "users", + auth=auth, ) - def get_user(self, user_id: User | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> User | None: - return self._get_single_resource(User, "users", user_id, include=get_includable_names(User), **params) + def get_user( + self, + user_id: User | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> User | None: + return self._get_single_resource( + User, "users", user_id, include=get_includable_names(User), auth=auth, **params + ) - def delete_user(self, user_id: User | uuid.UUID | str): - self._delete_resource("users", user_id) + def delete_user(self, user_id: User | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("users", user_id, auth=auth) def update_user( self, @@ -537,6 +638,8 @@ def update_user( name_locked: bool | UNSET_T = UNSET, first_name: str | None | UNSET_T = UNSET, last_name: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> User: return self._update_resource( User, @@ -551,139 +654,216 @@ def update_user( ), "users", user_id, + auth=auth, ) - def get_users(self, **params: te.Unpack[GetKwargs]) -> list[User]: - return self._get_all_resources(User, "users", include=get_includable_names(User), **params) + def get_users(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs]) -> list[User]: + return self._get_all_resources(User, "users", include=get_includable_names(User), auth=auth, **params) - def find_users(self, **params: te.Unpack[FindAllKwargs]) -> list[User]: - return self._find_all_resources(User, "users", include=get_includable_names(User), **params) + def find_users( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[User]: + return self._find_all_resources(User, "users", include=get_includable_names(User), auth=auth, **params) def create_user_permission( self, user_id: User | uuid.UUID | str, permission_id: Permission | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> UserPermission: return self._create_resource( UserPermission, CreateUserPermission(user_id=user_id, permission_id=permission_id), "user-permissions", + auth=auth, ) def get_user_permission( - self, user_permission_id: UserPermission | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + user_permission_id: UserPermission | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> UserPermission | None: return self._get_single_resource( UserPermission, "user-permissions", user_permission_id, include=get_includable_names(UserPermission), + auth=auth, **params, ) - def delete_user_permission(self, user_permission_id: UserPermission | uuid.UUID | str): - self._delete_resource("user-permissions", user_permission_id) + def delete_user_permission( + self, user_permission_id: UserPermission | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("user-permissions", user_permission_id, auth=auth) - def get_user_permissions(self, **params: te.Unpack[GetKwargs]) -> list[UserPermission]: + def get_user_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[UserPermission]: return self._get_all_resources( UserPermission, "user-permissions", include=get_includable_names(UserPermission), + auth=auth, **params, ) - def find_user_permissions(self, **params: te.Unpack[FindAllKwargs]) -> list[UserPermission]: + def find_user_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[UserPermission]: return self._find_all_resources( UserPermission, "user-permissions", include=get_includable_names(UserPermission), + auth=auth, **params, ) - def create_user_role(self, user_id: User | uuid.UUID | str, role_id: Role | uuid.UUID | str) -> UserRole: + def create_user_role( + self, + user_id: User | uuid.UUID | str, + role_id: Role | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ) -> UserRole: return self._create_resource( UserRole, CreateUserRole(user_id=user_id, role_id=role_id), "user-roles", + auth=auth, ) def get_user_role( - self, user_role_id: UserRole | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + user_role_id: UserRole | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> UserRole | None: return self._get_single_resource( - UserRole, "user-roles", user_role_id, include=get_includable_names(UserRole), **params + UserRole, "user-roles", user_role_id, include=get_includable_names(UserRole), auth=auth, **params ) - def delete_user_role(self, user_role_id: UserRole | uuid.UUID | str): - self._delete_resource("user-roles", user_role_id) + def delete_user_role(self, user_role_id: UserRole | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("user-roles", user_role_id, auth=auth) - def get_user_roles(self, **params: te.Unpack[GetKwargs]) -> list[UserRole]: - return self._get_all_resources(UserRole, "user-roles", include=get_includable_names(UserRole), **params) + def get_user_roles( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[UserRole]: + return self._get_all_resources( + UserRole, "user-roles", include=get_includable_names(UserRole), auth=auth, **params + ) - def find_user_roles(self, **params: te.Unpack[FindAllKwargs]) -> list[UserRole]: - return self._find_all_resources(UserRole, "user-roles", include=get_includable_names(UserRole), **params) + def find_user_roles( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[UserRole]: + return self._find_all_resources( + UserRole, "user-roles", include=get_includable_names(UserRole), auth=auth, **params + ) def create_robot_permission( - self, robot_id: Robot | uuid.UUID | str, permission_id: Permission | uuid.UUID | str + self, + robot_id: Robot | uuid.UUID | str, + permission_id: Permission | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> RobotPermission: return self._create_resource( RobotPermission, CreateRobotPermission(robot_id=robot_id, permission_id=permission_id), "robot-permissions", + auth=auth, ) def get_robot_permission( - self, robot_permission_id: RobotPermission | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + robot_permission_id: RobotPermission | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> RobotPermission | None: return self._get_single_resource( RobotPermission, "robot-permissions", robot_permission_id, include=get_includable_names(RobotPermission), + auth=auth, **params, ) - def delete_robot_permission(self, robot_permission_id: RobotPermission | uuid.UUID | str): - self._delete_resource("robot-permissions", robot_permission_id) + def delete_robot_permission( + self, robot_permission_id: RobotPermission | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("robot-permissions", robot_permission_id, auth=auth) - def get_robot_permissions(self, **params: te.Unpack[GetKwargs]) -> list[RobotPermission]: + def get_robot_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[RobotPermission]: return self._get_all_resources( RobotPermission, "robot-permissions", include=get_includable_names(RobotPermission), + auth=auth, **params, ) - def find_robot_permissions(self, **params: te.Unpack[FindAllKwargs]) -> list[RobotPermission]: + def find_robot_permissions( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[RobotPermission]: return self._find_all_resources( RobotPermission, "robot-permissions", include=get_includable_names(RobotPermission), + auth=auth, **params, ) - def create_robot_role(self, robot_id: Robot | uuid.UUID | str, role_id: Role | uuid.UUID | str) -> RobotRole: + def create_robot_role( + self, + robot_id: Robot | uuid.UUID | str, + role_id: Role | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ) -> RobotRole: return self._create_resource( RobotRole, CreateRobotRole(robot_id=robot_id, role_id=role_id), "robot-roles", + auth=auth, ) def get_robot_role( - self, robot_role_id: RobotRole | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + robot_role_id: RobotRole | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> RobotRole | None: return self._get_single_resource( - RobotRole, "robot-roles", robot_role_id, include=get_includable_names(RobotRole), **params + RobotRole, "robot-roles", robot_role_id, include=get_includable_names(RobotRole), auth=auth, **params ) - def delete_robot_role(self, robot_role_id: RobotRole | uuid.UUID | str): - self._delete_resource("robot-roles", robot_role_id) + def delete_robot_role( + self, robot_role_id: RobotRole | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("robot-roles", robot_role_id, auth=auth) - def get_robot_roles(self, **params: te.Unpack[GetKwargs]) -> list[RobotRole]: - return self._get_all_resources(RobotRole, "robot-roles", include=get_includable_names(RobotRole), **params) + def get_robot_roles( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[RobotRole]: + return self._get_all_resources( + RobotRole, "robot-roles", include=get_includable_names(RobotRole), auth=auth, **params + ) - 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 find_robot_roles( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[RobotRole]: + return self._find_all_resources( + RobotRole, "robot-roles", include=get_includable_names(RobotRole), auth=auth, **params + ) def create_client( self, @@ -697,6 +877,8 @@ def create_client( is_confidential: bool = True, secret_hashed: bool = False, grant_types: str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Client: return self._create_resource( Client, @@ -713,19 +895,30 @@ def create_client( grant_types=grant_types, ), "clients", + auth=auth, ) - def delete_client(self, client_id: Client | uuid.UUID | str): - self._delete_resource("clients", client_id) + def delete_client(self, client_id: Client | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("clients", client_id, auth=auth) - 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_client( + self, + client_id: Client | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Client | None: + return self._get_single_resource( + Client, "clients", client_id, include=get_includable_names(Client), auth=auth, **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 get_clients(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs]) -> list[Client]: + return self._get_all_resources(Client, "clients", include=get_includable_names(Client), auth=auth, **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 find_clients( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Client]: + return self._find_all_resources(Client, "clients", include=get_includable_names(Client), auth=auth, **params) def update_client( self, @@ -739,6 +932,8 @@ def update_client( is_confidential: bool | UNSET_T = UNSET, secret_hashed: bool | UNSET_T = UNSET, grant_types: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Client: return self._update_resource( Client, @@ -755,4 +950,5 @@ def update_client( ), "clients", client_id, + auth=auth, ) diff --git a/flame_hub/_base_client.py b/flame_hub/_base_client.py index f009148..c726e45 100644 --- a/flame_hub/_base_client.py +++ b/flame_hub/_base_client.py @@ -6,12 +6,88 @@ import httpx import typing_extensions as te +from httpx._client import USE_CLIENT_DEFAULT, UseClientDefault from pydantic import BaseModel, ValidatorFunctionWrapHandler, ValidationError, ConfigDict from flame_hub._exceptions import new_hub_api_error_from_response from flame_hub._auth_flows import PasswordAuth, ClientAuth +RequestAuth = httpx.Auth | tuple[str | bytes, str | bytes] | str +"""Authentication override accepted for a single request. + +One of: + +* an :py:class:`httpx.Auth` instance (e.g. :py:class:`.PasswordAuth`, :py:class:`.ClientAuth`), +* a ``(username, password)`` tuple for HTTP basic authentication, or +* a string which is sent verbatim as the value of the ``Authorization`` header. + +See Also +-------- +:py:data:`.RequestAuthArg`, :py:func:`.resolve_request_auth` +""" + +RequestAuthArg = RequestAuth | None | UseClientDefault +"""Type of the ``auth`` parameter accepted by client methods. + +In addition to the override forms described by :py:type:`.RequestAuth`, two sentinels are accepted: + +* :py:data:`httpx.USE_CLIENT_DEFAULT` (the default) keeps the authentication bound to the client, and +* :any:`None` disables authentication for the request, i.e. no ``Authorization`` header is sent. + +See Also +-------- +:py:type:`.RequestAuth`, :py:func:`.resolve_request_auth` +""" + + +class _StaticAuthorization(httpx.Auth): + """Authentication flow which sets a fixed ``Authorization`` header on every request. + + This is used to support passing a raw header value (e.g. ``"Bearer "``) as a per-request authentication + override. + + See Also + -------- + :py:func:`.resolve_request_auth`, :py:type:`.RequestAuth` + """ + + def __init__(self, authorization: str): + self._authorization = authorization + + def auth_flow(self, request): + request.headers["Authorization"] = self._authorization + yield request + + +def resolve_request_auth(auth: RequestAuthArg) -> RequestAuthArg: + """Translate a per-request ``auth`` argument into a value understood by ``httpx``. + + A string is wrapped in :py:class:`._StaticAuthorization` so that it is sent verbatim as the ``Authorization`` + header. Every other value is passed through unchanged, relying on ``httpx`` semantics: + :py:data:`httpx.USE_CLIENT_DEFAULT` keeps the client's bound authentication, :any:`None` disables authentication for + the request, and an :py:class:`httpx.Auth` (or ``(username, password)`` tuple) overrides it. + + Parameters + ---------- + auth : :py:data:`.RequestAuthArg` + The per-request authentication argument. + + Returns + ------- + :py:data:`.RequestAuthArg` + A value suitable to pass as the ``auth`` argument of an ``httpx`` request. + + See Also + -------- + :py:type:`.RequestAuth`, :py:data:`.RequestAuthArg`, :py:class:`._StaticAuthorization` + """ + if isinstance(auth, str): + return _StaticAuthorization(auth) + + return auth + + class UNSET(BaseModel): """Sentinel to mark parameters as unset as opposed to using :any:`None`.""" @@ -416,6 +492,7 @@ def _get_all_resources( *path: str, include: IncludeParams | None = None, expected_code: int = httpx.codes.OK.value, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs], ) -> list[ResourceT] | tuple[list[ResourceT], ResourceListMeta]: """Retrieve all resources of a certain type at the specified path from the FLAME Hub. @@ -432,7 +509,9 @@ def _get_all_resources( Default pagination parameters are applied as explained in the return section of :py:meth:`_find_all_resources`. """ - return self._find_all_resources(resource_type, *path, include=include, expected_code=expected_code, **params) + return self._find_all_resources( + resource_type, *path, include=include, expected_code=expected_code, auth=auth, **params + ) def _find_all_resources( self, @@ -440,6 +519,7 @@ def _find_all_resources( *path: str, include: IncludeParams | None = None, expected_code: int = httpx.codes.OK.value, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs], ) -> list[ResourceT] | tuple[list[ResourceT], ResourceListMeta]: """Find all resources at the specified path on the FLAME Hub that match certain criteria. @@ -463,6 +543,9 @@ def _find_all_resources( :doc:`model specifications ` which resources can be included in other resources. expected_code : :py:class:`int`, optional The expected status code of the response from the ``GET`` request. This defaults to ``200``. + auth : :py:data:`.RequestAuthArg`, optional + Override the authentication for this request. Defaults to :py:data:`httpx.USE_CLIENT_DEFAULT` so the + client's bound authentication is used. **params : :py:obj:`~typing.Unpack` [:py:class:`.FindAllKwargs`] Further keyword arguments to define filtering, sorting and pagination conditions, adding optional fields to a response and returning meta information. @@ -502,7 +585,7 @@ def _find_all_resources( | build_field_params(field_params) ) - r = self._client.get("/".join(path), params=request_params) + r = self._client.get("/".join(path), params=request_params, auth=resolve_request_auth(auth)) if r.status_code != expected_code: raise new_hub_api_error_from_response(r) @@ -520,6 +603,7 @@ def _create_resource( resource: BaseModel, *path: str, expected_code: int = httpx.codes.CREATED.value, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> ResourceT: """Create a resource of a certain type at the specified path. @@ -540,6 +624,9 @@ def _create_resource( Path to the endpoint where the resource should be created. expected_code : :py:class:`int`, optional The expected status code of the response from the ``POST`` request. This defaults to ``201``. + auth : :py:data:`.RequestAuthArg`, optional + Override the authentication for this request. Defaults to :py:data:`httpx.USE_CLIENT_DEFAULT` so the + client's bound authentication is used. Returns ------- @@ -556,6 +643,7 @@ def _create_resource( r = self._client.post( "/".join(path), json=resource.model_dump(mode="json"), + auth=resolve_request_auth(auth), ) if r.status_code != expected_code: @@ -569,6 +657,7 @@ def _get_single_resource( *path: str | UuidIdentifiable, include: IncludeParams | None = None, expected_code: int = httpx.codes.OK.value, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs], ) -> ResourceT | None: """Get a single resource of a certain type at the specified path. @@ -593,6 +682,9 @@ def _get_single_resource( :doc:`model specifications ` which resources can be included in other resources. expected_code : :py:class:`int`, optional The expected status code of the response from the ``GET`` request. This defaults to ``200``. + auth : :py:data:`.RequestAuthArg`, optional + Override the authentication for this request. Defaults to :py:data:`httpx.USE_CLIENT_DEFAULT` so the + client's bound authentication is used. **params : :py:obj:`~typing.Unpack` [:py:class:`.GetKwargs`] Further keyword arguments for adding optional fields to a response and returning meta information. @@ -621,7 +713,7 @@ def _get_single_resource( request_params = build_field_params(field_params) | build_include_params(include) - r = self._client.get("/".join(convert_path(path)), params=request_params) + r = self._client.get("/".join(convert_path(path)), params=request_params, auth=resolve_request_auth(auth)) if r.status_code == httpx.codes.NOT_FOUND.value: return None @@ -637,6 +729,7 @@ def _update_resource( resource: BaseModel, *path: str | UuidIdentifiable, expected_code: int = httpx.codes.ACCEPTED.value, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> ResourceT: """Update a resource of a certain type at the specified path. @@ -659,6 +752,9 @@ def _update_resource( ``id`` attribute. expected_code : :py:class:`int`, optional The expected status code of the response from the ``POST`` request. This defaults to ``202``. + auth : :py:data:`.RequestAuthArg`, optional + Override the authentication for this request. Defaults to :py:data:`httpx.USE_CLIENT_DEFAULT` so the + client's bound authentication is used. Returns ------- @@ -676,6 +772,7 @@ def _update_resource( "/".join(convert_path(path)), # Exclude defaults so that properties that are set to UNSET are excluded from update models. json=resource.model_dump(mode="json", exclude_defaults=True), + auth=resolve_request_auth(auth), ) if r.status_code != expected_code: @@ -683,7 +780,12 @@ def _update_resource( return resource_type(**r.json()) - def _delete_resource(self, *path: str | UuidIdentifiable, expected_code: int = httpx.codes.ACCEPTED.value) -> None: + def _delete_resource( + self, + *path: str | UuidIdentifiable, + expected_code: int = httpx.codes.ACCEPTED.value, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ) -> None: """Delete a resource of a certain type at the specified path. Parameters @@ -694,13 +796,16 @@ def _delete_resource(self, *path: str | UuidIdentifiable, expected_code: int = h ``id`` attribute. expected_code : :py:class:`int`, optional The expected status code of the response from the ``DELETE`` request. This defaults to ``202``. + auth : :py:data:`.RequestAuthArg`, optional + Override the authentication for this request. Defaults to :py:data:`httpx.USE_CLIENT_DEFAULT` so the + client's bound authentication is used. Raises ------ :py:exc:`.HubAPIError` If the status code of the response does not match ``expected_code``. """ - r = self._client.delete("/".join(convert_path(path))) + r = self._client.delete("/".join(convert_path(path)), auth=resolve_request_auth(auth)) if r.status_code != expected_code: raise new_hub_api_error_from_response(r) diff --git a/flame_hub/_core_client.py b/flame_hub/_core_client.py index 6b0fb07..2ee6b1f 100644 --- a/flame_hub/_core_client.py +++ b/flame_hub/_core_client.py @@ -21,6 +21,9 @@ IsIncludable, get_includable_names, build_filter_params, + RequestAuthArg, + USE_CLIENT_DEFAULT, + resolve_request_auth, ) from flame_hub._exceptions import new_hub_api_error_from_response from flame_hub._defaults import DEFAULT_CORE_BASE_URL @@ -401,11 +404,13 @@ def __init__( ): super().__init__(base_url, auth, **kwargs) - def get_nodes(self, **params: te.Unpack[GetKwargs]) -> list[Node]: - return self._get_all_resources(Node, "nodes", include=get_includable_names(Node), **params) + def get_nodes(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs]) -> list[Node]: + return self._get_all_resources(Node, "nodes", include=get_includable_names(Node), auth=auth, **params) - def find_nodes(self, **params: te.Unpack[FindAllKwargs]) -> list[Node]: - return self._find_all_resources(Node, "nodes", include=get_includable_names(Node), **params) + def find_nodes( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Node]: + return self._find_all_resources(Node, "nodes", include=get_includable_names(Node), auth=auth, **params) def create_node( self, @@ -415,6 +420,8 @@ def create_node( external_name: str | None = None, node_type: NodeType = "default", hidden: bool = False, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Node: return self._create_resource( Node, @@ -427,13 +434,22 @@ def create_node( type=node_type, ), "nodes", + auth=auth, ) - def get_node(self, node_id: Node | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Node | None: - return self._get_single_resource(Node, "nodes", node_id, include=get_includable_names(Node), **params) + def get_node( + self, + node_id: Node | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Node | None: + return self._get_single_resource( + Node, "nodes", node_id, include=get_includable_names(Node), auth=auth, **params + ) - def delete_node(self, node_id: Node | uuid.UUID | str): - self._delete_resource("nodes", node_id) + def delete_node(self, node_id: Node | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("nodes", node_id, auth=auth) def update_node( self, @@ -444,6 +460,8 @@ def update_node( realm_id: Realm | str | uuid.UUID | UNSET_T = UNSET, registry_id: Registry | str | uuid.UUID | None | UNSET_T = UNSET, public_key: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Node: return self._update_resource( Node, @@ -457,52 +475,78 @@ def update_node( ), "nodes", node_id, + auth=auth, ) - def get_master_image_groups(self, **params: te.Unpack[GetKwargs]) -> list[MasterImageGroup]: - return self._get_all_resources(MasterImageGroup, "master-image-groups", **params) + def get_master_image_groups( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[MasterImageGroup]: + return self._get_all_resources(MasterImageGroup, "master-image-groups", auth=auth, **params) def get_master_image_group( - self, master_image_group_id: MasterImageGroup | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + master_image_group_id: MasterImageGroup | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> MasterImageGroup | None: - return self._get_single_resource(MasterImageGroup, "master-image-groups", master_image_group_id, **params) + return self._get_single_resource( + MasterImageGroup, "master-image-groups", master_image_group_id, auth=auth, **params + ) - def find_master_image_groups(self, **params: te.Unpack[FindAllKwargs]) -> list[MasterImageGroup]: - return self._find_all_resources(MasterImageGroup, "master-image-groups", **params) + def find_master_image_groups( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[MasterImageGroup]: + return self._find_all_resources(MasterImageGroup, "master-image-groups", auth=auth, **params) - def get_master_images(self, **params: te.Unpack[GetKwargs]) -> list[MasterImage]: - return self._get_all_resources(MasterImage, "master-images", **params) + def get_master_images( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[MasterImage]: + return self._get_all_resources(MasterImage, "master-images", auth=auth, **params) def get_master_image( - self, master_image_id: MasterImage | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + master_image_id: MasterImage | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> MasterImage | None: - return self._get_single_resource(MasterImage, "master-images", master_image_id, **params) + return self._get_single_resource(MasterImage, "master-images", master_image_id, auth=auth, **params) - def find_master_images(self, **params: te.Unpack[FindAllKwargs]) -> list[MasterImage]: - return self._find_all_resources(MasterImage, "master-images", **params) + def find_master_images( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[MasterImage]: + return self._find_all_resources(MasterImage, "master-images", auth=auth, **params) - def get_projects(self, **params: te.Unpack[GetKwargs]) -> list[Project]: - return self._get_all_resources(Project, "projects", include=get_includable_names(Project), **params) + def get_projects( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[Project]: + return self._get_all_resources(Project, "projects", include=get_includable_names(Project), auth=auth, **params) - def find_projects(self, **params: te.Unpack[FindAllKwargs]) -> list[Project]: - return self._find_all_resources(Project, "projects", include=get_includable_names(Project), **params) + def find_projects( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Project]: + return self._find_all_resources(Project, "projects", include=get_includable_names(Project), auth=auth, **params) - def sync_master_images(self): + def sync_master_images(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): """This method will start to synchronize the master images. Note that an error is raised if you request a synchronization while the Hub instance is still synchronizing master images. """ - r = self._client.post("master-images/command", json={"command": "sync"}) + r = self._client.post("master-images/command", json={"command": "sync"}, auth=resolve_request_auth(auth)) if r.status_code != httpx.codes.ACCEPTED.value: raise new_hub_api_error_from_response(r) - def build_master_image(self, master_image_id: MasterImage | uuid.UUID | str): + def build_master_image( + self, master_image_id: MasterImage | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): """This method will command the Hub to start building a master image. Note that building a master image could take some time. """ r = self._client.post( "master-images/command", json={"command": "build", "id": str(obtain_uuid_from(master_image_id))}, + auth=resolve_request_auth(auth), ) if r.status_code != httpx.codes.ACCEPTED.value: @@ -514,6 +558,8 @@ def create_project( display_name: str = None, master_image_id: MasterImage | uuid.UUID | str = None, description: str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Project: return self._create_resource( Project, @@ -524,14 +570,21 @@ def create_project( display_name=display_name, ), "projects", + auth=auth, ) - def delete_project(self, project_id: Project | uuid.UUID | str): - self._delete_resource("projects", project_id) + def delete_project(self, project_id: Project | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("projects", project_id, auth=auth) - def get_project(self, project_id: Project | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Project | None: + def get_project( + self, + project_id: Project | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Project | None: return self._get_single_resource( - Project, "projects", project_id, include=get_includable_names(Project), **params + Project, "projects", project_id, include=get_includable_names(Project), auth=auth, **params ) def update_project( @@ -541,6 +594,8 @@ def update_project( master_image_id: MasterImage | str | uuid.UUID | None | UNSET_T = UNSET, name: str | UNSET_T = UNSET, display_name: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Project: return self._update_resource( Project, @@ -549,35 +604,56 @@ def update_project( ), "projects", project_id, + auth=auth, ) def create_project_node( - self, project_id: Project | uuid.UUID | str, node_id: Node | uuid.UUID | str + self, + project_id: Project | uuid.UUID | str, + node_id: Node | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> ProjectNode: return self._create_resource( ProjectNode, CreateProjectNode(project_id=project_id, node_id=node_id), "project-nodes", + auth=auth, ) - def delete_project_node(self, project_node_id: ProjectNode | uuid.UUID | str): - self._delete_resource("project-nodes", project_node_id) + def delete_project_node( + self, project_node_id: ProjectNode | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("project-nodes", project_node_id, auth=auth) - def get_project_nodes(self, **params: te.Unpack[GetKwargs]) -> list[ProjectNode]: + def get_project_nodes( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[ProjectNode]: return self._get_all_resources( - ProjectNode, "project-nodes", include=get_includable_names(ProjectNode), **params + ProjectNode, "project-nodes", include=get_includable_names(ProjectNode), auth=auth, **params ) - def find_project_nodes(self, **params: te.Unpack[FindAllKwargs]) -> list[ProjectNode]: + def find_project_nodes( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[ProjectNode]: return self._find_all_resources( - ProjectNode, "project-nodes", include=get_includable_names(ProjectNode), **params + ProjectNode, "project-nodes", include=get_includable_names(ProjectNode), auth=auth, **params ) def get_project_node( - self, project_node_id: ProjectNode | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + project_node_id: ProjectNode | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> ProjectNode | None: return self._get_single_resource( - ProjectNode, "project-nodes", project_node_id, include=get_includable_names(ProjectNode), **params + ProjectNode, + "project-nodes", + project_node_id, + include=get_includable_names(ProjectNode), + auth=auth, + **params, ) def update_project_node( @@ -585,12 +661,15 @@ def update_project_node( project_node_id: ProjectNode | uuid.UUID | str, comment: str | None | UNSET_T = UNSET, approval_status: ProjectNodeApprovalStatus | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ): return self._update_resource( ProjectNode, UpdateProjectNode(comment=comment, approval_status=approval_status), "project-nodes", project_node_id, + auth=auth, ) def create_analysis( @@ -602,6 +681,8 @@ def create_analysis( master_image_id: MasterImage | uuid.UUID | str = None, registry_id: Registry | uuid.UUID | str = None, image_command_arguments: list[MasterImageCommandArgument] = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Analysis: return self._create_resource( Analysis, @@ -615,20 +696,35 @@ def create_analysis( image_command_arguments=image_command_arguments, ), "analyses", + auth=auth, ) - def delete_analysis(self, analysis_id: Analysis | uuid.UUID | str): - self._delete_resource("analyses", analysis_id) + def delete_analysis(self, analysis_id: Analysis | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("analyses", analysis_id, auth=auth) - def get_analyses(self, **params: te.Unpack[GetKwargs]) -> list[Analysis]: - return self._get_all_resources(Analysis, "analyses", include=get_includable_names(Analysis), **params) + def get_analyses( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[Analysis]: + return self._get_all_resources( + Analysis, "analyses", include=get_includable_names(Analysis), auth=auth, **params + ) - def find_analyses(self, **params: te.Unpack[FindAllKwargs]) -> list[Analysis]: - return self._find_all_resources(Analysis, "analyses", include=get_includable_names(Analysis), **params) + def find_analyses( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Analysis]: + return self._find_all_resources( + Analysis, "analyses", include=get_includable_names(Analysis), auth=auth, **params + ) - def get_analysis(self, analysis_id: Analysis | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Analysis | None: + def get_analysis( + self, + analysis_id: Analysis | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Analysis | None: return self._get_single_resource( - Analysis, "analyses", analysis_id, include=get_includable_names(Analysis), **params + Analysis, "analyses", analysis_id, include=get_includable_names(Analysis), auth=auth, **params ) def update_analysis( @@ -639,6 +735,8 @@ def update_analysis( description: str | None | UNSET_T = UNSET, master_image_id: MasterImage | uuid.UUID | str | None | UNSET_T = UNSET, image_command_arguments: list[MasterImageCommandArgument] | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Analysis: return self._update_resource( Analysis, @@ -651,10 +749,21 @@ def update_analysis( ), "analyses", analysis_id, + auth=auth, ) - def send_analysis_command(self, analysis_id: Analysis | uuid.UUID | str, command: AnalysisCommand) -> Analysis: - r = self._client.post(f"analyses/{obtain_uuid_from(analysis_id)}/command", json={"command": command}) + def send_analysis_command( + self, + analysis_id: Analysis | uuid.UUID | str, + command: AnalysisCommand, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ) -> Analysis: + r = self._client.post( + f"analyses/{obtain_uuid_from(analysis_id)}/command", + json={"command": command}, + auth=resolve_request_auth(auth), + ) if r.status_code != httpx.codes.ACCEPTED.value: raise new_hub_api_error_from_response(r) @@ -662,16 +771,23 @@ def send_analysis_command(self, analysis_id: Analysis | uuid.UUID | str, command return Analysis(**r.json()) def create_analysis_node( - self, analysis_id: Analysis | uuid.UUID | str, node_id: Node | uuid.UUID | str + self, + analysis_id: Analysis | uuid.UUID | str, + node_id: Node | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> AnalysisNode: return self._create_resource( AnalysisNode, CreateAnalysisNode(analysis_id=analysis_id, node_id=node_id), "analysis-nodes", + auth=auth, ) - def delete_analysis_node(self, analysis_node_id: AnalysisNode | uuid.UUID | str): - self._delete_resource("analysis-nodes", analysis_node_id) + def delete_analysis_node( + self, analysis_node_id: AnalysisNode | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("analysis-nodes", analysis_node_id, auth=auth) def update_analysis_node( self, @@ -680,6 +796,8 @@ def update_analysis_node( approval_status: AnalysisNodeApprovalStatus | None | UNSET_T = UNSET, execution_status: ProcessStatus | None | UNSET_T = UNSET, execution_progress: int | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> AnalysisNode: return self._update_resource( AnalysisNode, @@ -691,23 +809,37 @@ def update_analysis_node( ), "analysis-nodes", analysis_node_id, + auth=auth, ) def get_analysis_node( - self, analysis_node_id: AnalysisNode | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + analysis_node_id: AnalysisNode | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> AnalysisNode | None: return self._get_single_resource( - AnalysisNode, "analysis-nodes", analysis_node_id, include=get_includable_names(AnalysisNode), **params + AnalysisNode, + "analysis-nodes", + analysis_node_id, + include=get_includable_names(AnalysisNode), + auth=auth, + **params, ) - def get_analysis_nodes(self, **params: te.Unpack[GetKwargs]) -> list[AnalysisNode]: + def get_analysis_nodes( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[AnalysisNode]: return self._get_all_resources( - AnalysisNode, "analysis-nodes", include=get_includable_names(AnalysisNode), **params + AnalysisNode, "analysis-nodes", include=get_includable_names(AnalysisNode), auth=auth, **params ) - def find_analysis_nodes(self, **params: te.Unpack[FindAllKwargs]) -> list[AnalysisNode]: + def find_analysis_nodes( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[AnalysisNode]: return self._find_all_resources( - AnalysisNode, "analysis-nodes", include=get_includable_names(AnalysisNode), **params + AnalysisNode, "analysis-nodes", include=get_includable_names(AnalysisNode), auth=auth, **params ) def create_analysis_node_log( @@ -718,6 +850,8 @@ def create_analysis_node_log( message: str, status: str = None, code: str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Log: return self._create_resource( Log, @@ -731,27 +865,39 @@ def create_analysis_node_log( ), "analysis-node-logs", expected_code=httpx.codes.ACCEPTED.value, + auth=auth, ) - def delete_analysis_node_logs(self, analysis_id: Analysis | uuid.UUID | str, node_id: Node | uuid.UUID | str): + def delete_analysis_node_logs( + self, + analysis_id: Analysis | uuid.UUID | str, + node_id: Node | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ): r = self._client.delete( "/analysis-node-logs", params=build_filter_params( {"analysis_id": str(obtain_uuid_from(analysis_id)), "node_id": str(obtain_uuid_from(node_id))} ), + auth=resolve_request_auth(auth), ) if r.status_code != httpx.codes.ACCEPTED.value: raise new_hub_api_error_from_response(r) - def find_analysis_node_logs(self, **params: te.Unpack[FindAllKwargs]) -> list[Log]: - return self._find_all_resources(Log, "analysis-node-logs", **params) + def find_analysis_node_logs( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Log]: + return self._find_all_resources(Log, "analysis-node-logs", auth=auth, **params) def create_analysis_bucket( self, bucket_type: AnalysisBucketType, bucket_id: Bucket | uuid.UUID | str, analysis_id: Analysis | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> AnalysisBucket: return self._create_resource( AnalysisBucket, @@ -761,55 +907,89 @@ def create_analysis_bucket( analysis_id=analysis_id, ), "analysis-buckets", + auth=auth, ) - def delete_analysis_bucket(self, analysis_bucket_id: AnalysisBucket | uuid.UUID | str): - self._delete_resource("analysis-buckets", analysis_bucket_id) + def delete_analysis_bucket( + self, analysis_bucket_id: AnalysisBucket | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("analysis-buckets", analysis_bucket_id, auth=auth) - def get_analysis_buckets(self, **params: te.Unpack[GetKwargs]) -> list[AnalysisBucket]: + def get_analysis_buckets( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[AnalysisBucket]: return self._get_all_resources( - AnalysisBucket, "analysis-buckets", include=get_includable_names(AnalysisBucket), **params + AnalysisBucket, "analysis-buckets", include=get_includable_names(AnalysisBucket), auth=auth, **params ) - def find_analysis_buckets(self, **params: te.Unpack[FindAllKwargs]) -> list[AnalysisBucket]: + def find_analysis_buckets( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[AnalysisBucket]: return self._find_all_resources( - AnalysisBucket, "analysis-buckets", include=get_includable_names(AnalysisBucket), **params + AnalysisBucket, "analysis-buckets", include=get_includable_names(AnalysisBucket), auth=auth, **params ) def get_analysis_bucket( - self, analysis_bucket_id: AnalysisBucket | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + analysis_bucket_id: AnalysisBucket | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> AnalysisBucket | None: return self._get_single_resource( AnalysisBucket, "analysis-buckets", analysis_bucket_id, include=get_includable_names(AnalysisBucket), + auth=auth, **params, ) - def get_analysis_bucket_files(self, **params: te.Unpack[GetKwargs]) -> list[AnalysisBucketFile]: + def get_analysis_bucket_files( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[AnalysisBucketFile]: return self._get_all_resources( - AnalysisBucketFile, "analysis-bucket-files", include=get_includable_names(AnalysisBucketFile), **params + AnalysisBucketFile, + "analysis-bucket-files", + include=get_includable_names(AnalysisBucketFile), + auth=auth, + **params, ) - def find_analysis_bucket_files(self, **params: te.Unpack[FindAllKwargs]) -> list[AnalysisBucketFile]: + def find_analysis_bucket_files( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[AnalysisBucketFile]: return self._find_all_resources( - AnalysisBucketFile, "analysis-bucket-files", include=get_includable_names(AnalysisBucketFile), **params + AnalysisBucketFile, + "analysis-bucket-files", + include=get_includable_names(AnalysisBucketFile), + auth=auth, + **params, ) def get_analysis_bucket_file( - self, analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> AnalysisBucketFile | None: return self._get_single_resource( AnalysisBucketFile, "analysis-bucket-files", analysis_bucket_file_id, include=get_includable_names(AnalysisBucketFile), + auth=auth, **params, ) - def delete_analysis_bucket_file(self, analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str): - self._delete_resource("analysis-bucket-files", analysis_bucket_file_id) + def delete_analysis_bucket_file( + self, + analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ): + self._delete_resource("analysis-bucket-files", analysis_bucket_file_id, auth=auth) def create_analysis_bucket_file( self, @@ -818,6 +998,8 @@ def create_analysis_bucket_file( bucket_id: Bucket | uuid.UUID | str, analysis_bucket_id: AnalysisBucket | uuid.UUID | str, is_entrypoint: bool = False, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> AnalysisBucketFile: return self._create_resource( AnalysisBucketFile, @@ -829,30 +1011,51 @@ def create_analysis_bucket_file( root=is_entrypoint, ), "analysis-bucket-files", + auth=auth, ) def update_analysis_bucket_file( - self, analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, is_entrypoint: bool | UNSET_T = UNSET + self, + analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, + is_entrypoint: bool | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> AnalysisBucketFile: return self._update_resource( AnalysisBucketFile, UpdateAnalysisBucketFile(root=is_entrypoint), "analysis-bucket-files", analysis_bucket_file_id, + auth=auth, ) - def create_registry(self, name: str, host: str, account_name: str = None, account_secret: str = None) -> Registry: + def create_registry( + self, + name: str, + host: str, + account_name: str = None, + account_secret: str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ) -> Registry: return self._create_resource( Registry, CreateRegistry(name=name, host=host, account_name=account_name, account_secret=account_secret), "registries", + auth=auth, ) - def get_registry(self, registry_id: Registry | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Registry | None: - return self._get_single_resource(Registry, "registries", registry_id, **params) + def get_registry( + self, + registry_id: Registry | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Registry | None: + return self._get_single_resource(Registry, "registries", registry_id, auth=auth, **params) - def delete_registry(self, registry_id: Registry | uuid.UUID | str): - self._delete_resource("registries", registry_id) + def delete_registry(self, registry_id: Registry | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("registries", registry_id, auth=auth) def update_registry( self, @@ -861,23 +1064,38 @@ def update_registry( host: str | UNSET_T = UNSET, account_name: str | None | UNSET_T = UNSET, account_secret: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> Registry: return self._update_resource( Registry, UpdateRegistry(name=name, host=host, account_name=account_name, account_secret=account_secret), "registries", registry_id, + auth=auth, ) - def get_registries(self, **params: te.Unpack[GetKwargs]) -> list[Registry]: - return self._get_all_resources(Registry, "registries", **params) + def get_registries( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[Registry]: + return self._get_all_resources(Registry, "registries", auth=auth, **params) - def find_registries(self, **params: te.Unpack[FindAllKwargs]) -> list[Registry]: - return self._find_all_resources(Registry, "registries", **params) + def find_registries( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Registry]: + return self._find_all_resources(Registry, "registries", auth=auth, **params) - def send_registry_command(self, registry_id: Registry | uuid.UUID | str, command: RegistryCommand): + def send_registry_command( + self, + registry_id: Registry | uuid.UUID | str, + command: RegistryCommand, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ): r = self._client.post( - "services/registry/command", json={"command": command, "id": str(obtain_uuid_from(registry_id))} + "services/registry/command", + json={"command": command, "id": str(obtain_uuid_from(registry_id))}, + auth=resolve_request_auth(auth), ) if r.status_code != httpx.codes.ACCEPTED.value: @@ -891,6 +1109,8 @@ def create_registry_project( external_name: str, account_name: str = None, account_secret: str = None, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> RegistryProject: return self._create_resource( RegistryProject, @@ -903,21 +1123,29 @@ def create_registry_project( account_secret=account_secret, ), "registry-projects", + auth=auth, ) def get_registry_project( - self, registry_project_id: RegistryProject | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, + registry_project_id: RegistryProject | uuid.UUID | str, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> RegistryProject | None: return self._get_single_resource( RegistryProject, "registry-projects", registry_project_id, include=get_includable_names(RegistryProject), + auth=auth, **params, ) - def delete_registry_project(self, registry_project_id: RegistryProject | uuid.UUID | str): - self._delete_resource("registry-projects", registry_project_id) + def delete_registry_project( + self, registry_project_id: RegistryProject | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("registry-projects", registry_project_id, auth=auth) def update_registry_project( self, @@ -928,6 +1156,8 @@ def update_registry_project( external_name: str | UNSET_T = UNSET, account_name: str | None | UNSET_T = UNSET, account_secret: str | None | UNSET_T = UNSET, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, ) -> RegistryProject: return self._update_resource( RegistryProject, @@ -941,32 +1171,44 @@ def update_registry_project( ), "registry-projects", registry_project_id, + auth=auth, ) - def get_registry_projects(self, **params: te.Unpack[GetKwargs]) -> list[RegistryProject]: + def get_registry_projects( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[RegistryProject]: return self._get_all_resources( RegistryProject, "registry-projects", include=get_includable_names(RegistryProject), + auth=auth, **params, ) - def find_registry_projects(self, **params: te.Unpack[FindAllKwargs]) -> list[RegistryProject]: + def find_registry_projects( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[RegistryProject]: return self._find_all_resources( RegistryProject, "registry-projects", include=get_includable_names(RegistryProject), + auth=auth, **params, ) - def delete_analysis_logs(self, analysis_id: Analysis | uuid.UUID | str): + def delete_analysis_logs( + self, analysis_id: Analysis | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): r = self._client.delete( "/analysis-logs", params=build_filter_params({"analysis_id": str(obtain_uuid_from(analysis_id))}), + auth=resolve_request_auth(auth), ) if r.status_code != httpx.codes.ACCEPTED.value: raise new_hub_api_error_from_response(r) - def find_analysis_logs(self, **params: te.Unpack[FindAllKwargs]) -> list[Log]: - return self._find_all_resources(Log, "analysis-logs", **params) + def find_analysis_logs( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Log]: + return self._find_all_resources(Log, "analysis-logs", auth=auth, **params) diff --git a/flame_hub/_storage_client.py b/flame_hub/_storage_client.py index 9bbce94..807892c 100644 --- a/flame_hub/_storage_client.py +++ b/flame_hub/_storage_client.py @@ -16,6 +16,9 @@ ClientKwargs, IsIncludable, get_includable_names, + RequestAuthArg, + resolve_request_auth, + USE_CLIENT_DEFAULT, ) from flame_hub._defaults import DEFAULT_STORAGE_BASE_URL from flame_hub._exceptions import new_hub_api_error_from_response @@ -87,27 +90,41 @@ def __init__( ): super().__init__(base_url, auth, **kwargs) - def create_bucket(self, name: str, region: str = None) -> Bucket: - return self._create_resource(Bucket, CreateBucket(name=name, region=region), "buckets") + def create_bucket(self, name: str, region: str = None, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT) -> Bucket: + return self._create_resource(Bucket, CreateBucket(name=name, region=region), "buckets", auth=auth) - def delete_bucket(self, bucket_id: Bucket | str | uuid.UUID): - self._delete_resource("buckets", bucket_id) + def delete_bucket(self, bucket_id: Bucket | str | uuid.UUID, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT): + self._delete_resource("buckets", bucket_id, auth=auth) - def get_buckets(self, **params: te.Unpack[GetKwargs]) -> list[Bucket]: - return self._get_all_resources(Bucket, "buckets", **params) + def get_buckets(self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs]) -> list[Bucket]: + return self._get_all_resources(Bucket, "buckets", auth=auth, **params) - def find_buckets(self, **params: te.Unpack[FindAllKwargs]) -> list[Bucket]: - return self._find_all_resources(Bucket, "buckets", **params) + def find_buckets( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[Bucket]: + return self._find_all_resources(Bucket, "buckets", auth=auth, **params) - def get_bucket(self, bucket_id: Bucket | str | uuid.UUID, **params: te.Unpack[GetKwargs]) -> Bucket | None: - return self._get_single_resource(Bucket, "buckets", bucket_id, **params) - - def stream_bucket_tarball(self, bucket_id: Bucket | str | uuid.UUID, chunk_size=1024) -> t.Iterator[bytes]: - with self._client.stream("GET", f"buckets/{obtain_uuid_from(bucket_id)}/stream") as r: + def get_bucket( + self, + bucket_id: Bucket | str | uuid.UUID, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], + ) -> Bucket | None: + return self._get_single_resource(Bucket, "buckets", bucket_id, auth=auth, **params) + + def stream_bucket_tarball( + self, bucket_id: Bucket | str | uuid.UUID, chunk_size=1024, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ) -> t.Iterator[bytes]: + with self._client.stream( + "GET", f"buckets/{obtain_uuid_from(bucket_id)}/stream", auth=resolve_request_auth(auth) + ) as r: for b in r.iter_bytes(chunk_size=chunk_size): yield b - def upload_to_bucket(self, bucket_id: Bucket | str | uuid.UUID, *upload_file: UploadFile) -> list[BucketFile]: + def upload_to_bucket( + self, bucket_id: Bucket | str | uuid.UUID, *upload_file: UploadFile, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ) -> list[BucketFile]: upload_file_tpl = tuple(apply_upload_file_defaults(uf) for uf in upload_file) upload_file_dict = { str(uuid.uuid4()): (uf["file_name"], uf["content"], uf["content_type"]) for uf in upload_file_tpl @@ -116,6 +133,7 @@ def upload_to_bucket(self, bucket_id: Bucket | str | uuid.UUID, *upload_file: Up r = self._client.post( f"buckets/{obtain_uuid_from(bucket_id)}/upload", files=upload_file_dict, + auth=resolve_request_auth(auth), ) if r.status_code != httpx.codes.CREATED.value: @@ -123,23 +141,45 @@ def upload_to_bucket(self, bucket_id: Bucket | str | uuid.UUID, *upload_file: Up return ResourceList[BucketFile](**r.json()).data - def delete_bucket_file(self, bucket_file_id: BucketFile | str | uuid.UUID): - self._delete_resource("bucket-files", bucket_file_id) + def delete_bucket_file( + self, bucket_file_id: BucketFile | str | uuid.UUID, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT + ): + self._delete_resource("bucket-files", bucket_file_id, auth=auth) def get_bucket_file( - self, bucket_file_id: BucketFile | str | uuid.UUID, **params: te.Unpack[GetKwargs] + self, + bucket_file_id: BucketFile | str | uuid.UUID, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + **params: te.Unpack[GetKwargs], ) -> BucketFile | None: return self._get_single_resource( - BucketFile, "bucket-files", bucket_file_id, include=get_includable_names(BucketFile), **params + BucketFile, "bucket-files", bucket_file_id, include=get_includable_names(BucketFile), auth=auth, **params ) - def get_bucket_files(self, **params: te.Unpack[GetKwargs]) -> list[BucketFile]: - return self._get_all_resources(BucketFile, "bucket-files", include=get_includable_names(BucketFile), **params) + def get_bucket_files( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[GetKwargs] + ) -> list[BucketFile]: + return self._get_all_resources( + BucketFile, "bucket-files", include=get_includable_names(BucketFile), auth=auth, **params + ) - def find_bucket_files(self, **params: te.Unpack[FindAllKwargs]) -> list[BucketFile]: - return self._find_all_resources(BucketFile, "bucket-files", include=get_includable_names(BucketFile), **params) + def find_bucket_files( + self, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT, **params: te.Unpack[FindAllKwargs] + ) -> list[BucketFile]: + return self._find_all_resources( + BucketFile, "bucket-files", include=get_includable_names(BucketFile), auth=auth, **params + ) - def stream_bucket_file(self, bucket_file_id: BucketFile | str | uuid.UUID, chunk_size=1024) -> t.Iterator[bytes]: - with self._client.stream("GET", f"bucket-files/{obtain_uuid_from(bucket_file_id)}/stream") as r: + def stream_bucket_file( + self, + bucket_file_id: BucketFile | str | uuid.UUID, + chunk_size=1024, + *, + auth: RequestAuthArg = USE_CLIENT_DEFAULT, + ) -> t.Iterator[bytes]: + with self._client.stream( + "GET", f"bucket-files/{obtain_uuid_from(bucket_file_id)}/stream", auth=resolve_request_auth(auth) + ) as r: for b in r.iter_bytes(chunk_size=chunk_size): yield b diff --git a/flame_hub/types.py b/flame_hub/types.py index f6fdbc9..9e0feab 100644 --- a/flame_hub/types.py +++ b/flame_hub/types.py @@ -5,6 +5,8 @@ "FilterParams", "FindAllKwargs", "GetKwargs", + "RequestAuth", + "RequestAuthArg", "NodeType", "RegistryCommand", "IncludeParams", @@ -35,6 +37,8 @@ FindAllKwargs, UuidIdentifiable, GetKwargs, + RequestAuth, + RequestAuthArg, ResourceT, UNSET_T, ) diff --git a/tests/test_base_client.py b/tests/test_base_client.py index ff5bd03..ed4a7f8 100644 --- a/tests/test_base_client.py +++ b/tests/test_base_client.py @@ -1,6 +1,7 @@ import typing as t import uuid +import httpx from pydantic import BaseModel, WrapValidator, Field, ValidationError import pytest @@ -19,6 +20,8 @@ BaseClient, UNSET, UNSET_T, + resolve_request_auth, + _StaticAuthorization, ) from flame_hub.types import FilterOperator from flame_hub.models import Node, User, Bucket @@ -218,3 +221,167 @@ def test_resource_list_meta_data(request, password_auth, resource_type, base_url assert meta.total >= 0 assert meta.limit == DEFAULT_PAGE_PARAMS["limit"] assert meta.offset == DEFAULT_PAGE_PARAMS["offset"] + + +def test_resolve_request_auth_use_client_default_passes_through(): + # The default sentinel is passed straight to httpx, which keeps the client's bound auth. + assert resolve_request_auth(httpx.USE_CLIENT_DEFAULT) is httpx.USE_CLIENT_DEFAULT + + +def test_resolve_request_auth_none_disables_auth(): + # None is passed through to httpx, which disables auth (no Authorization header) for the request. + assert resolve_request_auth(None) is None + + +def test_resolve_request_auth_string_wraps_header(): + resolved = resolve_request_auth("Bearer token123") + assert isinstance(resolved, _StaticAuthorization) + + # the string is sent verbatim as the Authorization header + request = next(resolved.auth_flow(httpx.Request("GET", "http://testserver"))) + assert request.headers["Authorization"] == "Bearer token123" + + +@pytest.mark.parametrize("auth", [httpx.BasicAuth("user", "pass"), ("user", "pass")]) +def test_resolve_request_auth_passes_through_non_string_overrides(auth): + assert resolve_request_auth(auth) is auth + + +class _AuthProbe(BaseModel): + id: uuid.UUID + + +def _make_probe_client(recorder: dict) -> httpx.Client: + """Build a client whose bound auth sets ``Bearer DEFAULT`` and which records the ``Authorization`` header of every + request it handles. + """ + + def handler(request: httpx.Request) -> httpx.Response: + recorder["authorization"] = request.headers.get("Authorization") + + if request.method == "DELETE": + return httpx.Response(httpx.codes.ACCEPTED.value) + if request.method == "POST": + return httpx.Response(httpx.codes.CREATED.value, json={"id": str(uuid.uuid4())}) + + # GET for a single resource + return httpx.Response(httpx.codes.OK.value, json={"id": str(uuid.uuid4())}) + + return httpx.Client( + auth=_StaticAuthorization("Bearer DEFAULT"), + base_url="http://testserver", + transport=httpx.MockTransport(handler), + ) + + +# (auth argument, expected Authorization header) covering every supported form +_AUTH_OVERRIDE_CASES = [ + (httpx.USE_CLIENT_DEFAULT, "Bearer DEFAULT"), # default sentinel keeps the client's bound auth + ("Bearer OVERRIDE", "Bearer OVERRIDE"), # raw header value + (_StaticAuthorization("Bearer VIA_AUTH"), "Bearer VIA_AUTH"), # httpx.Auth instance + (None, None), # disable auth for this request (no Authorization header) +] + + +def test_auth_defaults_to_client_bound_auth_when_omitted(): + recorder = {} + client = BaseClient(base_url="http://testserver", client=_make_probe_client(recorder)) + + # omitting auth must behave like the default sentinel and keep the bound auth + client._get_single_resource(_AuthProbe, "things", uuid.uuid4()) + + assert recorder["authorization"] == "Bearer DEFAULT" + + +@pytest.mark.parametrize("auth,expected", _AUTH_OVERRIDE_CASES) +def test_get_single_resource_auth_override(auth, expected): + recorder = {} + client = BaseClient(base_url="http://testserver", client=_make_probe_client(recorder)) + + client._get_single_resource(_AuthProbe, "things", uuid.uuid4(), auth=auth) + + assert recorder["authorization"] == expected + + +@pytest.mark.parametrize("auth,expected", _AUTH_OVERRIDE_CASES) +def test_create_resource_auth_override(auth, expected): + recorder = {} + client = BaseClient(base_url="http://testserver", client=_make_probe_client(recorder)) + + client._create_resource(_AuthProbe, _AuthProbe(id=uuid.uuid4()), "things", auth=auth) + + assert recorder["authorization"] == expected + + +@pytest.mark.parametrize("auth,expected", _AUTH_OVERRIDE_CASES) +def test_delete_resource_auth_override(auth, expected): + recorder = {} + client = BaseClient(base_url="http://testserver", client=_make_probe_client(recorder)) + + client._delete_resource("things", uuid.uuid4(), auth=auth) + + assert recorder["authorization"] == expected + + +@pytest.mark.parametrize("auth,expected", _AUTH_OVERRIDE_CASES) +def test_find_all_resources_auth_override(auth, expected): + recorder = {} + + def handler(request: httpx.Request) -> httpx.Response: + recorder["authorization"] = request.headers.get("Authorization") + return httpx.Response(httpx.codes.OK.value, json={"data": [], "meta": {"total": 0}}) + + probe_client = httpx.Client( + auth=_StaticAuthorization("Bearer DEFAULT"), + base_url="http://testserver", + transport=httpx.MockTransport(handler), + ) + client = BaseClient(base_url="http://testserver", client=probe_client) + + client._find_all_resources(_AuthProbe, "things", auth=auth) + + assert recorder["authorization"] == expected + + +@pytest.mark.parametrize("auth,expected", _AUTH_OVERRIDE_CASES) +def test_public_read_method_forwards_auth(auth, expected): + # exercises the public client surface (get_*/find_* forward auth through to the request) + from flame_hub import AuthClient + + recorder = {} + + def handler(request: httpx.Request) -> httpx.Response: + recorder["authorization"] = request.headers.get("Authorization") + return httpx.Response(httpx.codes.OK.value, json={"data": [], "meta": {"total": 0}}) + + probe_client = httpx.Client( + auth=_StaticAuthorization("Bearer DEFAULT"), + base_url="http://testserver", + transport=httpx.MockTransport(handler), + ) + auth_client = AuthClient(client=probe_client) + + auth_client.find_realms(auth=auth) + assert recorder["authorization"] == expected + + +@pytest.mark.parametrize("auth,expected", _AUTH_OVERRIDE_CASES) +def test_public_write_method_forwards_auth(auth, expected): + # exercises the public client surface (create_*/delete_* forward auth through to the request) + from flame_hub import AuthClient + + recorder = {} + + def handler(request: httpx.Request) -> httpx.Response: + recorder["authorization"] = request.headers.get("Authorization") + return httpx.Response(httpx.codes.ACCEPTED.value) + + probe_client = httpx.Client( + auth=_StaticAuthorization("Bearer DEFAULT"), + base_url="http://testserver", + transport=httpx.MockTransport(handler), + ) + auth_client = AuthClient(client=probe_client) + + auth_client.delete_realm(uuid.uuid4(), auth=auth) + assert recorder["authorization"] == expected From 2a4bb6894b03b92a478a4e76c069a628f8118442 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 23 Jun 2026 11:09:04 +0200 Subject: [PATCH 2/2] fix: raise on failed bucket stream responses and unify delete paths Stream methods now check the response status and raise HubAPIError before yielding bytes, so an error response (e.g. from auth=None or an invalid per-request override) is no longer returned as file content. Also drop the leading slash from the two direct delete request paths for consistency with the relative paths used elsewhere in the core client. Addresses review feedback on PR #115. --- flame_hub/_core_client.py | 4 ++-- flame_hub/_storage_client.py | 8 ++++++++ tests/test_base_client.py | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/flame_hub/_core_client.py b/flame_hub/_core_client.py index 2ee6b1f..c1e372c 100644 --- a/flame_hub/_core_client.py +++ b/flame_hub/_core_client.py @@ -876,7 +876,7 @@ def delete_analysis_node_logs( auth: RequestAuthArg = USE_CLIENT_DEFAULT, ): r = self._client.delete( - "/analysis-node-logs", + "analysis-node-logs", params=build_filter_params( {"analysis_id": str(obtain_uuid_from(analysis_id)), "node_id": str(obtain_uuid_from(node_id))} ), @@ -1200,7 +1200,7 @@ def delete_analysis_logs( self, analysis_id: Analysis | uuid.UUID | str, *, auth: RequestAuthArg = USE_CLIENT_DEFAULT ): r = self._client.delete( - "/analysis-logs", + "analysis-logs", params=build_filter_params({"analysis_id": str(obtain_uuid_from(analysis_id))}), auth=resolve_request_auth(auth), ) diff --git a/flame_hub/_storage_client.py b/flame_hub/_storage_client.py index 807892c..26452a9 100644 --- a/flame_hub/_storage_client.py +++ b/flame_hub/_storage_client.py @@ -119,6 +119,10 @@ def stream_bucket_tarball( with self._client.stream( "GET", f"buckets/{obtain_uuid_from(bucket_id)}/stream", auth=resolve_request_auth(auth) ) as r: + if r.status_code != httpx.codes.OK.value: + r.read() + raise new_hub_api_error_from_response(r) + for b in r.iter_bytes(chunk_size=chunk_size): yield b @@ -181,5 +185,9 @@ def stream_bucket_file( with self._client.stream( "GET", f"bucket-files/{obtain_uuid_from(bucket_file_id)}/stream", auth=resolve_request_auth(auth) ) as r: + if r.status_code != httpx.codes.OK.value: + r.read() + raise new_hub_api_error_from_response(r) + for b in r.iter_bytes(chunk_size=chunk_size): yield b diff --git a/tests/test_base_client.py b/tests/test_base_client.py index ed4a7f8..227ae9b 100644 --- a/tests/test_base_client.py +++ b/tests/test_base_client.py @@ -385,3 +385,21 @@ def handler(request: httpx.Request) -> httpx.Response: auth_client.delete_realm(uuid.uuid4(), auth=auth) assert recorder["authorization"] == expected + + +@pytest.mark.parametrize("stream_method", ["stream_bucket_tarball", "stream_bucket_file"]) +def test_stream_raises_on_error_response(stream_method): + # An error response (e.g. caused by auth=None or an invalid override) must raise instead of being + # yielded as file content. + from flame_hub import StorageClient + from flame_hub import HubAPIError + + def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(httpx.codes.FORBIDDEN.value, json={}) + + probe_client = httpx.Client(base_url="http://testserver", transport=httpx.MockTransport(handler)) + storage_client = StorageClient(client=probe_client) + + with pytest.raises(HubAPIError): + # consume the generator so the request is actually performed + list(getattr(storage_client, stream_method)(uuid.uuid4(), auth=None))