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
33 changes: 14 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,14 @@ Python client for the Userverse HTTP server.

## Installation

Create and activate a virtual environment, then install the project in editable mode:

## linux configuration
Install from PyPI:

```bash
uv venv
source .venv\Scripts\activate
uv pip install -e .
python -m pip install userverse-python-client
```

## windows configuration

```bash
uv venv
.venv\Scripts\activate
uv pip install -e .
```
For editable installs from source, see the repository README:
https://github.com/SoftwareVerse/userverse-python-client#installation

## Usage

Expand All @@ -59,12 +50,17 @@ client = UverseUserClient(base_url="https://api.example.com")

## Demo

The runnable demo lives in `examples/user_demo.py`. See `examples/user_demo_README.md`
for flags and environment variables:
The runnable demo lives in:
https://github.com/SoftwareVerse/userverse-python-client/blob/main/examples/user_demo.py

```bash
uv run -m examples.user_demo --help
```
See the demo README for flags and environment variables:
https://github.com/SoftwareVerse/userverse-python-client/blob/main/examples/user_demo_README.md

## Developing Clients in Other Languages

See the guide for a summary of the Python client architecture and a plan for
implementing SDKs in other languages:
https://github.com/SoftwareVerse/userverse-python-client/blob/main/docs/other-language-clients.md

## Tests

Expand All @@ -73,4 +69,3 @@ Run the unit tests with:
```bash
pytest
```

118 changes: 118 additions & 0 deletions docs/other-language-clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Developing Userverse Clients in Other Languages

This document summarizes the Python client architecture and provides a plan for
building equivalent clients in other languages (e.g., Dart, JavaScript, Go).

## Summary of the Python Client

### Core HTTP design
- **Base URL normalization**: The Python client trims trailing slashes from the
base URL and requires all request paths to start with `/`.
- **Session headers**: Every request sets `Accept: application/json` and
`Content-Type: application/json`.
- **Timeouts**: The `BaseClient` accepts a configurable timeout (default: 30
seconds).
- **Authorization**:
- **Bearer** token for authenticated endpoints via
`Authorization: Bearer <token>`.
- **Basic Auth** for user login and account creation with email/password
encoded as `Basic <base64(email:password)>`.
- **Error handling**:
- HTTP errors are parsed into a structured error payload if possible.
- Network/timeouts are surfaced as client errors with a generic "invalid
request" message.
- Empty bodies (e.g., 204 responses) return `None`.
- Successful responses that are not JSON fall back to text.

### Client surface area
The Python package exposes dedicated client classes that map to API domains and
reuse the shared `BaseClient`:
- `UverseUserClient` (login, create user, get/update user, verification, password
reset).
- `UverseCompanyClient` (list companies, get company by ID/email, create/update
company).
- `UverseCompanyUserManagementClient` (add/list/delete users in a company).
- `UverseCompanyUserRolesManagement` (list/create/update/delete company roles).

### Models and response shapes
- Responses are wrapped in a generic response shape (`GenericResponseModel`)
with a `data` field that holds the typed payload.
- Request and response schemas are defined in the shared model packages
(`userverse_models`, `sverse_generic_models`), which the client references
directly.

### Usage patterns
Examples show a consistent flow:
1. Create a `UverseUserClient` with `base_url`.
2. Login with Basic Auth to obtain a JWT.
3. Set `Authorization: Bearer <token>` on other clients for protected endpoints.
4. Call methods that mirror API routes and validate responses.

## Plan to Build Clients in Other Languages

### 1. Establish a shared API contract
- Extract the request/response schemas (from `userverse_models` and
`sverse_generic_models`) into an OpenAPI or JSON schema source of truth.
- Ensure the response wrapper and error formats match the server responses.

### 2. Implement a base HTTP client
- Mirror the Python `BaseClient` behavior:
- Normalize `base_url` and enforce path formatting.
- Default headers: `Accept` + `Content-Type` set to JSON.
- Configurable timeout.
- Helper for setting Bearer tokens.
- Return `None` for empty bodies.
- Prefer explicit query parameter handling instead of manual string building.
- Provide a centralized request method that every endpoint uses.

### 3. Port authentication flows
- Implement Basic Auth encoding for login and create-user endpoints.
- Implement bearer token injection after login for all protected endpoints.
- Preserve the same endpoints/methods:
- `PATCH /user/login`
- `POST /user`
- `GET /user/get`
- `PATCH /user/update`
- `POST /user/resend-verification`
- `GET /user/verify?token=<token>`
- `PATCH /password-reset/request?email=<email>`
- `PATCH /password-reset/validate-otp`
- `GET /user/companies`
- `GET /company?company_id=<id>` or `GET /company?email=<email>`
- `PATCH /company/{id}`
- `POST /company`
- Company user/role management endpoints as in the Python client.

### 4. Generate or hand-write typed models
- If your language supports OpenAPI codegen (e.g., Dart `openapi-generator`,
TypeScript `openapi-typescript`), generate data models and response wrappers.
- Otherwise, hand-write models matching the Python client types and ensure
required/optional fields align with the API.

### 5. Error handling and response validation
- Match the Python client’s strategy:
- On non-2xx responses, parse the error body into a structured error type.
- If the server returns `detail.message` and `detail.error`, map them directly.
- If parsing fails, fall back to a generic error message and raw body text.
- Provide a top-level error type (similar to `ClientErrorModel`) that includes
HTTP status code and a detail payload.

### 6. Organize the client by domain
Mirror the Python structure for discoverability:
- `user` module/class for authentication + user profile.
- `company` module/class for company CRUD.
- `company_user_management` module/class for user membership in companies.
- `company_user_roles_management` module/class for role management.

### 7. Tests and example scripts
- Use the Python unit tests as reference for expected behaviors:
- Enforce path requirements.
- Validate error handling in HTTP failures.
- Verify response shape parsing and Basic/Bearer auth handling.
- Provide runnable examples similar to the Python `examples/` to document flows
for login, list companies, etc.

### 8. Packaging and documentation
- Provide a README that mirrors the Python client usage.
- Publish artifacts to the target ecosystem (npm, pub.dev, etc.).
- Version releases consistently with the API behavior.
7 changes: 4 additions & 3 deletions src/userverse_python_client/clients/company.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ def get_company_by_id_or_email(
email: Optional[EmailStr] = None,
) -> GenericResponseModel[CompanyReadModel]:
"""Fetches a company by its ID or email."""
params = None
if company_id is not None:
path = f"/company?company_id={company_id}"
params = {"company_id": company_id}
elif email is not None:
path = f"/company?email={email}"
params = {"email": email}
else:
raise ValueError("Either company_id or email must be provided")

response = self._request("GET", path)
response = self._request("GET", "/company", params=params)

if not response or "data" not in response:
raise ValueError("Invalid response from get company endpoint")
Expand Down
17 changes: 13 additions & 4 deletions src/userverse_python_client/clients/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ def create_user(
)
headers = {"Authorization": basic_auth}

response = self._request("POST", "/user", json=user_data, headers=headers)
response = self._request(
"POST",
"/user",
json=user_data.model_dump(exclude_none=True),
headers=headers,
)

if not response or "data" not in response:
raise ValueError("Invalid response from create user endpoint")
Expand All @@ -71,7 +76,9 @@ 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)
response = self._request(
"PATCH", "/user/update", json=user_update.model_dump(exclude_none=True)
)
if not response:
raise ValueError("No user data found in response")
if not isinstance(response, dict):
Expand All @@ -92,7 +99,7 @@ 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?token=" + token)
response = self._request("GET", "/user/verify", params={"token": token})
if not response:
raise ValueError("No data found in response")
if not isinstance(response, dict):
Expand All @@ -104,7 +111,9 @@ 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?email=" + email)
response = self._request(
"PATCH", "/password-reset/request", params={"email": email}
)
if not response:
raise ValueError("No data found in response")
if not isinstance(response, dict):
Expand Down
7 changes: 5 additions & 2 deletions src/userverse_python_client/http_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(
self.set_access_token(access_token)

def set_access_token(self, token: str) -> None:
self.session.headers["Authorization"] = f"bearer {token}"
self.session.headers["Authorization"] = f"Bearer {token}"

def _request(
self,
Expand Down Expand Up @@ -57,7 +57,10 @@ def _request(
if not resp.content:
return None

return resp.json()
try:
return resp.json()
except ValueError:
return resp.text

except requests.exceptions.HTTPError as http_err:
status = http_err.response.status_code if http_err.response else 500
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.