Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/userverse_python_client/clients/company.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
19 changes: 6 additions & 13 deletions src/userverse_python_client/clients/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down
46 changes: 43 additions & 3 deletions src/userverse_python_client/http_client_base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
)
Expand Down