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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ stytch.egg-info/
.coverage
dist/
.idea/
.worktrees/
10 changes: 10 additions & 0 deletions stytch/core/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ def __init__(
self.async_client = AsyncClient(project_id, secret, session=async_session)
self.jwks_client = self.get_jwks_client(project_id)

async def close(self) -> None:
await self.async_client.close()

async def __aenter__(self) -> "ClientBase":
return self

async def __aexit__(self, *_: object) -> Optional[bool]:
await self.close()
return None

@abc.abstractmethod
def get_jwks_client(self, project_id: str) -> jwt.PyJWKClient:
pass
Expand Down
6 changes: 6 additions & 0 deletions stytch/core/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ def __del__(self) -> None:
except Exception:
pass

async def close(self) -> None:
if self._external_session or self.__session is None:
return
await self.__session.close()
self.__session = None

@classmethod
async def _response_from_request(
cls, r: aiohttp.ClientResponse
Expand Down
20 changes: 20 additions & 0 deletions stytch/core/http/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
import unittest

import aiohttp
import requests

from stytch.core.http.client import AsyncClient, SyncClient
Expand All @@ -31,3 +32,22 @@ def test_session_without_event_loop(self):
asyncio.set_event_loop_policy(NoEventLoopPolicy())
client = AsyncClient("project_id", "secret")
self.assertIsNotNone(client)


class TestAsyncClientClose(unittest.IsolatedAsyncioTestCase):
async def test_close_owned_session(self):
client = AsyncClient("project_id", "secret")
session = client._session # force session creation and capture reference
await client.close()
self.assertTrue(session.closed)

async def test_close_external_session_is_noop(self):
async with aiohttp.ClientSession() as external:
client = AsyncClient("project_id", "secret", session=external)
await client.close()
self.assertFalse(external.closed)

async def test_close_before_session_created_is_noop(self):
client = AsyncClient("project_id", "secret")
# no session accessed — should not raise
await client.close()
33 changes: 32 additions & 1 deletion stytch/core/test/test_client_base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env python3

import unittest
import warnings
from unittest import TestCase
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch

import pydantic

Expand Down Expand Up @@ -155,3 +156,33 @@ def test_resolve_api_url_requires_https(self):

# Verify the error message
self.assertIn("HTTPS scheme", str(context.exception))


class AsyncClientLifecycleTests(unittest.IsolatedAsyncioTestCase):
def _make_client(self):
return Client(
project_id="project-test-00000000-0000-0000-0000-000000000000",
secret="secret",
suppress_warnings=True,
)

async def test_close_delegates_to_async_client(self):
client = self._make_client()
client.async_client.close = AsyncMock()
await client.close()
client.async_client.close.assert_awaited_once()

async def test_context_manager_calls_close(self):
client = self._make_client()
client.async_client.close = AsyncMock()
async with client as c:
self.assertIs(c, client)
client.async_client.close.assert_awaited_once()

async def test_context_manager_calls_close_on_exception(self):
client = self._make_client()
client.async_client.close = AsyncMock()
with self.assertRaises(ValueError):
async with client:
raise ValueError("boom")
client.async_client.close.assert_awaited_once()
2 changes: 1 addition & 1 deletion stytch/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "15.1.0"
__version__ = "15.2.0"
Loading