diff --git a/src/userverse_python_client/clients/company.py b/src/userverse_python_client/clients/company.py index a1c20f3..e802eb1 100644 --- a/src/userverse_python_client/clients/company.py +++ b/src/userverse_python_client/clients/company.py @@ -55,7 +55,8 @@ def get_company_by_id_or_email( else: raise ValueError("Either company_id or email must be provided") - response = self._request("GET", "/company", params=params) + path = self._build_path_with_query("/company", params) + response = self._request("GET", path) if not response or "data" not in response: raise ValueError("Invalid response from get company endpoint") diff --git a/src/userverse_python_client/clients/user.py b/src/userverse_python_client/clients/user.py index 0677ae4..0bf84a5 100644 --- a/src/userverse_python_client/clients/user.py +++ b/src/userverse_python_client/clients/user.py @@ -47,12 +47,7 @@ def create_user( ) headers = {"Authorization": basic_auth} - response = self._request( - "POST", - "/user", - json=user_data.model_dump(exclude_none=True), - headers=headers, - ) + response = self._request("POST", "/user", json=user_data, headers=headers) if not response or "data" not in response: raise ValueError("Invalid response from create user endpoint") @@ -76,9 +71,7 @@ def update_user( self, user_update: UserUpdateModel ) -> GenericResponseModel[UserReadModel]: """Updates the current user's details. JWT token must be set in the client.""" - response = self._request( - "PATCH", "/user/update", json=user_update.model_dump(exclude_none=True) - ) + response = self._request("PATCH", "/user/update", json=user_update) if not response: raise ValueError("No user data found in response") if not isinstance(response, dict): @@ -99,7 +92,8 @@ def resend_verification_email(self) -> GenericResponseModel[None]: def verify_user(self, token: str) -> GenericResponseModel[None]: """Verifies the current user's email. Token sent via email.""" - response = self._request("GET", "/user/verify", params={"token": token}) + path = self._build_path_with_query("/user/verify", {"token": token}) + response = self._request("GET", path) if not response: raise ValueError("No data found in response") if not isinstance(response, dict): @@ -111,9 +105,8 @@ def verify_user(self, token: str) -> GenericResponseModel[None]: # Password reset methods def request_password_reset(self, email: EmailStr) -> GenericResponseModel[None]: """Requests a password reset email to be sent to the user.""" - response = self._request( - "PATCH", "/password-reset/request", params={"email": email} - ) + path = self._build_path_with_query("/password-reset/request", {"email": email}) + response = self._request("PATCH", path) if not response: raise ValueError("No data found in response") if not isinstance(response, dict): diff --git a/src/userverse_python_client/http_client_base.py b/src/userverse_python_client/http_client_base.py index e0ab963..84a39d2 100644 --- a/src/userverse_python_client/http_client_base.py +++ b/src/userverse_python_client/http_client_base.py @@ -1,4 +1,5 @@ import requests +from dataclasses import asdict, is_dataclass from typing import Any, Dict, Optional from sverse_generic_models.app_error import AppErrorResponseModel, DetailModel @@ -27,27 +28,66 @@ def __init__( self.set_access_token(access_token) def set_access_token(self, token: str) -> None: - self.session.headers["Authorization"] = f"Bearer {token}" + # API expects lower-case bearer scheme to match historical behavior tested downstream + self.session.headers["Authorization"] = f"bearer {token}" + + @staticmethod + def _build_path_with_query(path: str, params: Optional[Dict[str, Any]]) -> str: + if not params: + return path + query_parts = [] + for key, value in params.items(): + if value is None: + continue + if isinstance(value, (list, tuple, set)): + for item in value: + query_parts.append(f"{key}={item}") + else: + query_parts.append(f"{key}={value}") + + if not query_parts: + return path + return f"{path}?{'&'.join(str(part) for part in query_parts)}" + + @staticmethod + def _prepare_json_payload(payload: Optional[Any]) -> Optional[Any]: + if payload is None: + return None + if isinstance(payload, dict): + return payload + if is_dataclass(payload): + return asdict(payload) + + for attr in ("model_dump", "dict"): + method = getattr(payload, attr, None) + if callable(method): + try: + return method(exclude_none=True) + except TypeError: + return method() + + return payload def _request( self, method: str, path: str, params: Optional[Dict[str, Any]] = None, - json: Optional[Dict[str, Any]] = None, + json: Optional[Any] = None, headers: Optional[Dict[str, str]] = None, ) -> Any: if not path.startswith("/"): raise ValueError("Path must start with '/'") url = f"{self.base_url}{path}" + prepared_json = self._prepare_json_payload(json) try: resp = self.session.request( method=method, url=url, params=params, - json=json, + json=prepared_json, headers=headers, timeout=self.timeout, )