Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7c079ed
Refactor and streamline organization and set base for SDK typing (#284)
jonatascastro12 Jul 22, 2024
52c7644
Add asyncio support for events API methods (#285)
mattgd Jul 23, 2024
d330930
List typing fixes (#287)
tribble Jul 23, 2024
bafe7b7
Add async support for Directory Sync (#288)
mattgd Jul 24, 2024
c10894a
Formatting and dependency upgrades (#290)
mattgd Jul 25, 2024
032981a
Add Event types (#289)
tribble Jul 25, 2024
67c262b
Add typing and async support for SSO (#291)
mattgd Jul 25, 2024
5003f6d
Type all dsync events (#292)
tribble Jul 25, 2024
60d4f65
List resource returns more specific type (#293)
tribble Jul 26, 2024
224d706
Add typing for User Management (#294)
mattgd Jul 29, 2024
ef78e2f
Add typing for MFA. (#295)
mattgd Jul 29, 2024
fb8281c
Add typing for Audit Logs APIs (#297)
mattgd Jul 29, 2024
0d17baa
Add typing for portal. (#299)
mattgd Jul 29, 2024
336ea6d
Add typing for Passwordless APIs. (#298)
mattgd Jul 29, 2024
061174a
Type the rest of the events (#301)
tribble Jul 30, 2024
06f4360
Clean up requests dependency (#302)
mattgd Jul 30, 2024
c3af80d
Remove domain param from directories query (#230)
ameesha Jul 30, 2024
1c1c6e9
Remove WorkOSBaseResource (#303)
mattgd Jul 30, 2024
7b46a5b
Fix methods missing _id for unique IDs. (#304)
mattgd Jul 30, 2024
5dd0f12
Bump to version 5.0.0beta1. (#306)
mattgd Jul 30, 2024
adc73ff
Incorporate CI changes.
mattgd Jul 30, 2024
37f36c6
Type webhook methods (#307)
tribble Jul 31, 2024
bb3f3c3
Return untyped webhook (#308)
tribble Aug 1, 2024
3899f51
Consistently use _id of IDs (#310)
tribble Aug 2, 2024
a34e514
Type quality of life type updates (#312)
tribble Aug 2, 2024
f518871
Input type QoL improvements (#313)
tribble Aug 5, 2024
d70843f
Fix sso profile resource (#314)
tribble Aug 5, 2024
1714cc0
Fix auth factor response types (#316)
tribble Aug 5, 2024
e8e5ca0
Use single WorkOsListResource for sync and async. (#319)
mattgd Aug 6, 2024
0e1ae78
Put mypy in strict mode (#318)
mattgd Aug 7, 2024
d93e7ff
Change unlinked directory state to inactive and convert in validator …
tribble Aug 7, 2024
b1b1a92
Add async support for UM. (#321)
mattgd Aug 7, 2024
ab62532
Separate params and json parameters for requests. (#322)
mattgd Aug 8, 2024
5a1fe91
Move all types to types module (#323)
tribble Aug 8, 2024
b307533
Fix type for authentication events (#326)
tribble Aug 8, 2024
e386e85
Require kwargs for multi param methods (#325)
tribble Aug 8, 2024
68ccae5
[FGA-73] Add FGA support (#324)
atainter Aug 9, 2024
114ff73
Allow multiple WorkOS clients to be instantiated with different keys …
mattgd Aug 12, 2024
2b0b085
[FGA-73] Add BatchCheck and Query methods to FGA module (#328)
atainter Aug 12, 2024
6d7ab8c
Update mypy config to be more strict and properly check the entire wo…
tribble Aug 12, 2024
ca35743
Name is optional when updating an organization (#331)
tribble Aug 13, 2024
056d30b
Small typing fixes (#332)
tribble Aug 13, 2024
b1b9564
A few fixes from docs audit. (#333)
mattgd Aug 13, 2024
cd8aac9
Update Docstrings across the SDK (#335)
tribble Aug 14, 2024
d4939fe
Fga type updates (#336)
tribble Aug 14, 2024
02740dc
Add async support for organizations (#334)
mattgd Aug 14, 2024
8d41712
Rename FGA types (#337)
tribble Aug 14, 2024
6b96f02
Fix list_organization_memberships statuses parameter when auto paging…
mattgd Aug 14, 2024
d403413
Fix merge conflicts.
mattgd Aug 14, 2024
74b85ea
Minor changes.
mattgd Aug 14, 2024
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
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: CI
on:
push:
branches:
- 'main'
- "main"
pull_request: {}

defaults:
Expand All @@ -17,13 +17,13 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['3.8', '3.9', '3.10', '3.11']
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: 'pip'
cache: "pip"
cache-dependency-path: setup.py

- name: Install dependencies
Expand All @@ -38,5 +38,8 @@ jobs:
flake8 . --extend-exclude .devbox --count --select=E9,F7,F82 --show-source --statistics
flake8 . --extend-exclude .devbox --count --exit-zero --max-complexity=10 --statistics

- name: Type Check
run: mypy

- name: Test
run: python -m pytest
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,9 @@ dmypy.json

# VSCode
.vscode/

# macOS
.DS_Store

# Intellij
.idea/
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,24 @@ python setup.py install

## Configuration

The package will need to be configured with your [api key](https://dashboard.workos.com/api-keys) at a minimum and [client id](https://dashboard.workos.com/sso/configuration) if you plan on using SSO:
The package will need to be configured with your [api key and client ID](https://dashboard.workos.com/api-keys).

```python
import workos
from workos import WorkOSClient

workos.api_key = "sk_1234"
workos.client_id = "client_1234"
workos_client = WorkOSClient(
api_key="sk_1234", client_id="client_1234"
)
```

The SDK also provides asyncio support for some SDK methods, via the async client:

```python
from workos import AsyncWorkOSClient

async_workos_client = AsyncWorkOSClient(
api_key="sk_1234", client_id="client_1234"
)
```

## SDK Versioning
Expand Down
11 changes: 11 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[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 = False
strict_equality = True
strict = True
27 changes: 14 additions & 13 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
import sys
from setuptools import setup, find_packages

base_dir = os.path.dirname(__file__)
Expand All @@ -20,24 +19,26 @@
description=about["__description__"],
long_description=long_description,
long_description_content_type="text/markdown",
package_data={"workos": ["py.typed"]},
packages=find_packages(
exclude=[
"tests*",
]
),
zip_safe=False,
license=about["__license__"],
install_requires=["requests>=2.22.0"],
install_requires=["httpx>=0.27.0", "pydantic==2.8.2"],
extras_require={
"dev": [
"flake8",
"pytest==8.1.1",
"pytest-cov==2.8.1",
"six==1.13.0",
"black==22.3.0",
"twine==4.0.2",
"requests==2.30.0",
"urllib3==2.0.2",
"pytest==8.3.2",
"pytest-asyncio==0.23.8",
"pytest-cov==5.0.0",
"six==1.16.0",
"black==24.4.2",
"twine==5.1.1",
"mypy==1.11.0",
"httpx>=0.27.0",
],
":python_version<'3.4'": ["enum34"],
},
Expand All @@ -48,10 +49,10 @@
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
)
180 changes: 129 additions & 51 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,158 @@
import pytest
import requests

import workos


class MockResponse(object):
def __init__(self, response_dict, status_code, headers=None):
self.response_dict = response_dict
self.status_code = status_code
self.headers = {} if headers is None else headers

if "content-type" not in self.headers:
self.headers["content-type"] = "application/json"

def json(self):
return self.response_dict
from typing import Any, Callable, Mapping, Optional
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest

class MockRawResponse(object):
def __init__(self, content, status_code, headers=None):
self.content = content
self.status_code = status_code
self.headers = {} if headers is None else headers
from tests.utils.client_configuration import ClientConfiguration
from tests.utils.list_resource import list_data_to_dicts, list_response_of
from workos.types.list_resource import WorkOSListResource
from workos.utils.http_client import AsyncHTTPClient, HTTPClient, SyncHTTPClient


@pytest.fixture
def set_api_key(monkeypatch):
monkeypatch.setattr(workos, "api_key", "sk_test")
def sync_http_client_for_test():
return SyncHTTPClient(
api_key="sk_test",
base_url="https://api.workos.test/",
client_id="client_b27needthisforssotemxo",
version="test",
)


@pytest.fixture
def set_client_id(monkeypatch):
monkeypatch.setattr(workos, "client_id", "client_b27needthisforssotemxo")
def async_http_client_for_test():
return AsyncHTTPClient(
api_key="sk_test",
base_url="https://api.workos.test/",
client_id="client_b27needthisforssotemxo",
version="test",
)


@pytest.fixture
def set_api_key_and_client_id(set_api_key, set_client_id):
pass
def mock_http_client_with_response(monkeypatch):
def inner(
http_client: HTTPClient,
response_dict: Optional[dict] = None,
status_code: int = 200,
headers: Optional[Mapping[str, str]] = None,
):
mock_class = (
AsyncMock if isinstance(http_client, AsyncHTTPClient) else MagicMock
)
mock = mock_class(
return_value=httpx.Response(
status_code=status_code, headers=headers, json=response_dict
),
)
monkeypatch.setattr(http_client._client, "request", mock)

return inner


@pytest.fixture
def mock_request_method(monkeypatch):
def inner(method, response_dict, status_code, headers=None):
def mock(*args, **kwargs):
return MockResponse(response_dict, status_code, headers=headers)
def capture_and_mock_http_client_request(monkeypatch):
def inner(
http_client: HTTPClient,
response_dict: Optional[dict] = None,
status_code: int = 200,
headers: Optional[Mapping[str, str]] = None,
):
request_kwargs = {}

monkeypatch.setattr(requests, method, mock)
def capture_and_mock(*args, **kwargs):
request_kwargs.update(kwargs)

return inner
return httpx.Response(
status_code=status_code,
headers=headers,
json=response_dict,
)

mock_class = (
AsyncMock if isinstance(http_client, AsyncHTTPClient) else MagicMock
)
mock = mock_class(side_effect=capture_and_mock)

@pytest.fixture
def mock_raw_request_method(monkeypatch):
def inner(method, content, status_code, headers=None):
def mock(*args, **kwargs):
return MockRawResponse(content, status_code, headers=headers)
monkeypatch.setattr(http_client._client, "request", mock)

monkeypatch.setattr(requests, method, mock)
return request_kwargs

return inner


@pytest.fixture
def capture_and_mock_request(monkeypatch):
def inner(method, response_dict, status_code, headers=None):
request_args = []
request_kwargs = {}
def mock_pagination_request_for_http_client(monkeypatch):
# Mocking pagination correctly requires us to index into a list of data
# and correctly set the before and after metadata in the response.
def inner(
http_client: HTTPClient,
data_list: list,
status_code: int = 200,
headers: Optional[Mapping[str, str]] = None,
):
# For convenient index lookup, store the list of object IDs.
data_ids = list(map(lambda x: x["id"], data_list))

def mock_function(*args, **kwargs):
params = kwargs.get("params") or {}
request_after = params.get("after", None)
limit = params.get("limit", 10)

if request_after is None:
# First page
start = 0
else:
# A subsequent page, return the first item _after_ the index we locate
start = data_ids.index(request_after) + 1
data = data_list[start : start + limit]
if len(data) < limit or len(data) == 0:
# No more data, set after to None
after = None
else:
# Set after to the last item in this page of results
after = data[-1]["id"]

return httpx.Response(
status_code=status_code,
headers=headers,
json=list_response_of(data=data, before=request_after, after=after),
)

mock_class = (
AsyncMock if isinstance(http_client, AsyncHTTPClient) else MagicMock
)
mock = mock_class(side_effect=mock_function)

monkeypatch.setattr(http_client._client, "request", mock)

def capture_and_mock(*args, **kwargs):
request_args.extend(args)
request_kwargs.update(kwargs)

return MockResponse(response_dict, status_code, headers=headers)
return inner

monkeypatch.setattr(requests, method, capture_and_mock)

return (request_args, request_kwargs)
@pytest.fixture
def test_sync_auto_pagination(
mock_pagination_request_for_http_client,
):
def inner(
http_client: SyncHTTPClient,
list_function: Callable[[], WorkOSListResource],
expected_all_page_data: dict,
list_function_params: Optional[Mapping[str, Any]] = None,
):
mock_pagination_request_for_http_client(
http_client=http_client,
data_list=expected_all_page_data,
status_code=200,
)

results = list_function(**list_function_params or {})
all_results = []

for result in results:
all_results.append(result)

assert len(list(all_results)) == len(expected_all_page_data)
assert (list_data_to_dicts(all_results)) == expected_all_page_data

return inner
Loading