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..c1e372c 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", + "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", + "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..26452a9 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,45 @@ 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 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: + if r.status_code != httpx.codes.OK.value: + r.read() + raise new_hub_api_error_from_response(r) - 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: 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 +137,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 +145,49 @@ 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, + *, + 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: + if r.status_code != httpx.codes.OK.value: + r.read() + raise new_hub_api_error_from_response(r) - 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: 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..227ae9b 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,185 @@ 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 + + +@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))