Skip to content
Open
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
20 changes: 8 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,25 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v5
- uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python }}
cache: "pip"
cache-dependency-path: setup.py
enable-cache: true

- name: Install dependencies
run: |
pip install -e .[dev]
run: uv sync --locked --dev

- name: Check formatting
run: black --check .
run: uv run ruff format --check .

- name: Lint
run: |
flake8 . --extend-exclude .devbox --count --select=E9,F7,F82 --show-source --statistics
flake8 . --extend-exclude .devbox --count --exit-zero --max-complexity=10 --statistics
run: uv run ruff check --extend-exclude .devbox

- name: Type Check
run: mypy
run: uv run mypy

- name: Test
run: python -m pytest
run: uv run pytest
38 changes: 17 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,26 @@ defaults:
shell: bash

jobs:
test:
pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
# Environment and permissions trusted publishing.
environment:
# Create this environment in the GitHub repository under Settings -> Environments
name: pypi
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: "pip"
cache-dependency-path: setup.py

- name: Checkout
uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: |
pip install -e .[dev]

run: uv sync --locked
- name: Test
run: |
python -m pytest

run: uv run pytest
- name: Build
run: uv build
- name: Publish
env:
TWINE_NON_INTERACTIVE: true
TWINE_USERNAME: "__token__"
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/* --skip-existing
run: uv publish
36 changes: 25 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,87 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Development Commands

### Installation and Setup

```bash
pip install -e .[dev] # Install package in development mode with dev dependencies
uv sync --locked --dev # Install package in development mode with dev dependencies
```

### Code Quality

```bash
black . # Format code
black --check . # Check formatting without making changes
flake8 . # Lint code
mypy # Type checking
ur run ruff format . # Format code
uv run ruff format --check . # Check formatting without making changes
uv run ruff check . # Lint code
uv run mypy # Type checking
```

### Testing

```bash
python -m pytest # Run all tests
python -m pytest tests/test_sso.py # Run specific test file
python -m pytest -k "test_name" # Run tests matching pattern
python -m pytest --cov=workos # Run tests with coverage
uv run pytest # Run all tests
uv run pytest tests/test_sso.py # Run specific test file
uv run pytest -k "test_name" # Run tests matching pattern
uv run pytest --cov=workos # Run tests with coverage
```

### Build and Distribution

```bash
python setup.py sdist bdist_wheel # Build distribution packages
uv build --sdist --wheel # Build distribution packages
bash scripts/build_and_upload_dist.sh # Build and upload to PyPI
```

## Architecture Overview

### Client Architecture

The SDK provides both synchronous and asynchronous clients:

- `WorkOSClient` (sync) and `AsyncWorkOSClient` (async) are the main entry points
- Both inherit from `BaseClient` which handles configuration and module initialization
- Each feature area (SSO, Directory Sync, etc.) has dedicated module classes
- HTTP clients (`SyncHTTPClient`/`AsyncHTTPClient`) handle the actual API communication

### Module Structure

Each WorkOS feature has its own module following this pattern:

- **Module class** (e.g., `SSO`) - main API interface
- **Types directory** (e.g., `workos/types/sso/`) - Pydantic models for API objects
- **Tests** (e.g., `tests/test_sso.py`) - comprehensive test coverage

### Type System

- All models inherit from `WorkOSModel` (extends Pydantic `BaseModel`)
- Strict typing with mypy enforcement (`strict = True` in mypy.ini)
- Support for both sync and async operations via `SyncOrAsync` typing

### Testing Framework

- Uses pytest with custom fixtures for mocking HTTP clients
- `@pytest.mark.sync_and_async()` decorator runs tests for both sync/async variants
- Comprehensive fixtures in `conftest.py` for HTTP mocking and pagination testing
- Test utilities in `tests/utils/` for common patterns

### HTTP Client Abstraction

- Base HTTP client (`_BaseHTTPClient`) with sync/async implementations
- Request helper utilities for consistent API interaction patterns
- Built-in pagination support with `WorkOSListResource` type
- Automatic retry and error handling

### Key Patterns

- **Dual client support**: Every module supports both sync and async operations
- **Type safety**: Extensive use of Pydantic models and strict mypy checking
- **Pagination**: Consistent cursor-based pagination across list endpoints
- **Error handling**: Custom exception classes in `workos/exceptions.py`
- **Configuration**: Environment variable support (`WORKOS_API_KEY`, `WORKOS_CLIENT_ID`)

When adding new features:

1. Create module class with both sync/async HTTP client support
2. Add Pydantic models in appropriate `types/` subdirectory
3. Implement comprehensive tests using the sync_and_async marker
4. Follow existing patterns for pagination, error handling, and type annotations
4. Follow existing patterns for pagination, error handling, and type annotations

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pip install workos
To install from source, clone the repo and run the following:

```
python setup.py install
python -m pip install .
```

## Configuration
Expand Down
11 changes: 0 additions & 11 deletions mypy.ini

This file was deleted.

63 changes: 63 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[project]
name = "workos"
version = "5.32.0"
description = "WorkOS Python Client"
readme = "README.md"
license = "MIT"
authors = [{ name = "WorkOS", email = "team@workos.com" }]
requires-python = ">=3.8"

dependencies = [
"cryptography>=44.0.2",
"httpx~=0.28.1",
"pydantic>=2.10.4",
"pyjwt>=2.10.0 ; python_full_version >= '3.9'",
"pyjwt>=2.9.0,<2.10 ; python_full_version == '3.8.*'",
]

[project.urls]
Homepage = "https://workos.com/docs/sdks/python"
Documentation = "https://workos.com/docs/reference"
Changelog = "https://workos.com/docs/sdks/python"

[dependency-groups]
dev = [
{ include-group = "test" },
{ include-group = "lint" },
{ include-group = "type_check" },
]
test = [
"pytest==8.3.4",
"pytest-asyncio==0.23.8",
"pytest-cov==5.0.0",
"six==1.17.0",
]
lint = ["ruff==0.14.5"]
type_check = ["mypy==1.14.1"]


[tool.mypy]
packages = "workos"
warn_return_any = true
warn_unused_configs = true
warn_unreachable = true
warn_redundant_casts = true
warn_no_return = true
warn_unused_ignores = true
implicit_reexport = true
strict_equality = true
strict = true

[tool.ruff.lint.per-file-ignores]
"*/__init__.py" = ["F401", "F403"]

[tool.ruff.lint.mccabe]
max-complexity = 10

[tool.uv.build-backend]
source-include = ["py.typed"]
source-exclude = ["tests*"]

[build-system]
requires = ["uv_build>=0.8.15,<0.9.0"]
build-backend = "uv_build"
9 changes: 0 additions & 9 deletions requirements-dev.txt

This file was deleted.

5 changes: 0 additions & 5 deletions requirements.txt

This file was deleted.

9 changes: 5 additions & 4 deletions scripts/build_and_upload_dist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# Clean out the dist directory so only the current release gets uploaded
rm dist/*

# Build the distribution
python3 setup.py sdist bdist_wheel
# Build the package using uv
uv build --sdist --wheel

# Upload the distribution to PyPi via uv
uv publish

# Upload the distribution to PyPi via twine
twine upload dist/*
8 changes: 8 additions & 0 deletions scripts/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

if ! command -v uv &>/dev/null; then
echo "Can't find uv, please install uv or ensure it's available in your PATH before attempting setup."
exit 1
fi

uv sync --dev
53 changes: 0 additions & 53 deletions setup.py

This file was deleted.

File renamed without changes.
3 changes: 1 addition & 2 deletions workos/_base_client.py → src/workos/_base_client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from abc import abstractmethod
import os
from typing import Optional
from workos.__about__ import __version__

from workos._client_configuration import ClientConfiguration
from workos.fga import FGAModule
from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT
from workos.utils.http_client import HTTPClient
from workos.audit_logs import AuditLogsModule
from workos.directory_sync import DirectorySyncModule
from workos.events import EventsModule
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions workos/async_client.py → src/workos/async_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional
from workos.__about__ import __version__
from importlib.metadata import version
from workos._base_client import BaseClient
from workos.audit_logs import AuditLogsModule
from workos.directory_sync import AsyncDirectorySync
Expand Down Expand Up @@ -41,7 +41,7 @@ def __init__(
api_key=self._api_key,
base_url=self.base_url,
client_id=self._client_id,
version=__version__,
version=version("workos"),
timeout=self.request_timeout,
)

Expand Down
File renamed without changes.
Loading