Skip to content

Commit

Permalink
Merge version 5 (#339)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgd authored Aug 14, 2024
1 parent 5ed2044 commit 1afa8ee
Show file tree
Hide file tree
Showing 164 changed files with 10,581 additions and 6,276 deletions.
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

0 comments on commit 1afa8ee

Please sign in to comment.