spaps 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: spaps
3
+ Version: 0.1.0
4
+ Summary: Sweet Potato Authentication & Payment Service Python client
5
+ Project-URL: Homepage, https://api.sweetpotato.dev
6
+ Project-URL: Repository, https://github.com/sweet-potato/spaps
7
+ Author-email: Sweet Potato Team <support@sweetpotato.dev>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 Sweet Potato Team
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ License-File: LICENSE
31
+ Keywords: authentication,payments,spaps,sweet-potato
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
41
+ Requires-Python: >=3.9
42
+ Requires-Dist: httpx<0.28.0,>=0.27.0
43
+ Requires-Dist: pydantic<3.0.0,>=2.7.0
44
+ Provides-Extra: dev
45
+ Requires-Dist: build<2.0.0,>=1.2.1; extra == 'dev'
46
+ Requires-Dist: httpx<0.28.0,>=0.27.0; extra == 'dev'
47
+ Requires-Dist: mypy<2.0.0,>=1.10.0; extra == 'dev'
48
+ Requires-Dist: pydantic<3.0.0,>=2.7.0; extra == 'dev'
49
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
50
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
51
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
52
+ Requires-Dist: respx<0.23.0,>=0.22.0; extra == 'dev'
53
+ Requires-Dist: ruff<1.0.0,>=0.5.5; extra == 'dev'
54
+ Description-Content-Type: text/markdown
55
+
56
+ ---
57
+ id: spaps-python-sdk
58
+ title: Sweet Potato Python Client
59
+ category: sdk
60
+ tags:
61
+ - sdk
62
+ - python
63
+ - client
64
+ ai_summary: |
65
+ Explains installation, configuration, and usage patterns for the spaps Python
66
+ SDK, including environment setup, async support, and integration guidance for
67
+ backend services.
68
+ last_updated: 2025-02-14
69
+ ---
70
+
71
+ # Sweet Potato Python Client
72
+
73
+ > Python SDK for the Sweet Potato Authentication & Payment Service (SPAPS).
74
+
75
+ This package is under active development. Follow the TDD plan in `TDD_PLAN.md`
76
+ to track progress and upcoming milestones.
77
+
78
+ ## Installation
79
+
80
+ Install from PyPI:
81
+
82
+ ```bash
83
+ pip install spaps
84
+ ```
85
+
86
+ For local development inside this repository:
87
+
88
+ ```bash
89
+ pip install -e .[dev]
90
+ ```
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ pytest
96
+ ```
97
+
98
+ ### Available clients
99
+
100
+ - `AuthClient` – wallet, email/password, and magic link flows
101
+ - `SessionsClient` – current session, validation, listing, revocation
102
+ - `PaymentsClient` – checkout sessions, wallet deposits, crypto invoices
103
+ - `UsageClient` – feature usage snapshots, recording, aggregated history
104
+ - `SecureMessagesClient` – encrypted message creation and retrieval
105
+ - `MetricsClient` – health and metrics convenience helpers
106
+
107
+ ### Quickstart
108
+
109
+ ```python
110
+ from spaps_client import SpapsClient
111
+
112
+ spaps = SpapsClient(base_url="http://localhost:3300", api_key="test_key_local_dev_only")
113
+
114
+ # Authenticate (tokens are persisted automatically)
115
+ spaps.auth.sign_in_with_password(email="user@example.com", password="Secret123!")
116
+
117
+ # Call downstream services using the stored access token
118
+ current = spaps.sessions.get_current_session()
119
+ print(current.session_id)
120
+
121
+ checkout = spaps.payments.create_checkout_session(
122
+ price_id="price_123",
123
+ mode="subscription",
124
+ success_url="https://example.com/success",
125
+ cancel_url="https://example.com/cancel",
126
+ )
127
+ print(checkout.checkout_url)
128
+
129
+ spaps.close()
130
+ ```
131
+
132
+ Configure retry/backoff and structured logging when constructing the client:
133
+
134
+ ```python
135
+ from spaps_client import SpapsClient, RetryConfig, default_logging_hooks
136
+
137
+ spaps = SpapsClient(
138
+ base_url="http://localhost:3300",
139
+ api_key="test_key_local_dev_only",
140
+ retry_config=RetryConfig(max_attempts=4, backoff_factor=0.2),
141
+ logging_hooks=default_logging_hooks(),
142
+ )
143
+ ```
144
+
145
+ ### Async Quickstart
146
+
147
+ ```python
148
+ import asyncio
149
+ from spaps_client import AsyncSpapsClient
150
+
151
+ async def main():
152
+ client = AsyncSpapsClient(base_url="http://localhost:3300", api_key="test_key_local_dev_only")
153
+ try:
154
+ await client.auth.sign_in_with_password(email="user@example.com", password="Secret123!")
155
+ current = await client.sessions.list_sessions()
156
+ print(len(current.sessions))
157
+ finally:
158
+ await client.aclose()
159
+
160
+ asyncio.run(main())
161
+ ```
162
+
163
+ ### Useful Scripts
164
+
165
+ ```bash
166
+ npm run test:python-client # run pytest from repo root
167
+ npm run lint:python-client # ruff linting
168
+ npm run typecheck:python-client # mypy type checking
169
+ npm run build:python-client # build wheel/sdist and run twine check
170
+ npm run publish:python-client # build and upload via twine (requires PYPI_TOKEN)
171
+ ```
172
+
173
+ Refer to `docs/RELEASE_CHECKLIST.md` for the full release process.
174
+
175
+ Refer to the repository root documentation for integration details.
176
+
177
+ ## Documentation
178
+
179
+ - [Quickstart (Python section)](../../docs/getting-started/quickstart.md#python-example---using-spaps)
180
+ - [Python Backend Integration Guide](../../docs/guides/python-backend.md)
181
+ - API references under `docs/api/` include Python usage snippets for sessions, payments, usage, whitelist, and secure messages.
@@ -0,0 +1,28 @@
1
+ spaps_client/__init__.py,sha256=W3NbWfDwSIpmZEQOggm407Vd60Q50V-x2WJpuao9L4c,4049
2
+ spaps_client/async_client.py,sha256=2IdoQnRmKet4cgOagNtGlIyYe3wkEZa3I4NkeNU6gf0,7489
3
+ spaps_client/auth.py,sha256=y2V0liV2oWKQ_Os5hqjzBaHtYVQy9TcFeZEgL7VAmTI,12950
4
+ spaps_client/auth_async.py,sha256=pSoDcqpnSQrcavY0qpFF9tt3q2Fr3-jj0wA2OzHegDE,9152
5
+ spaps_client/client.py,sha256=XTGnaHnrCVvZ7StTINthh3ZfhIJJTqV3vrhBnAvxBjI,7190
6
+ spaps_client/config.py,sha256=CzjtyZWnlvRuOirTxjvNipdky6-f5Hlz_lZ1VKry5lQ,2183
7
+ spaps_client/crypto.py,sha256=f6qmfynGstmwaj453RzmSwN2Z-CMzYbmWc2L4QBS4LY,8210
8
+ spaps_client/crypto_async.py,sha256=ZjrU2lr4d11mKavflWeByyQ3x3A0WfEaRnWdbV38xMw,4452
9
+ spaps_client/http.py,sha256=CzhS0lNWWDRncTRdB5mwZXB58r5nkc6ivfKq63g5OTo,5781
10
+ spaps_client/http_async.py,sha256=NfhT5S0HUnHMsS0RGC_2dsU6tbASJO9ZXFaNPcusCew,2819
11
+ spaps_client/metrics.py,sha256=hsP3cKu6Od3LqQQCxrX_F-gY8psIa3huIy_4EtF4ypo,1634
12
+ spaps_client/metrics_async.py,sha256=Ckl4182Q828BMxkQjJWh-ERwBvnidl00zsAPMrktBIY,1443
13
+ spaps_client/payments.py,sha256=NJJC9xgmk7T71bH_0rU4m6_1_F7JpVsnO5kVo2Uybng,11887
14
+ spaps_client/payments_async.py,sha256=Rj2ftZ1_W-hRneTCAG9EeA6UiWlYirs2FemfPLJGhM8,8037
15
+ spaps_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ spaps_client/secure_messages.py,sha256=Yq1EBoRdZ2CATQxoCFcOu_ZDNM8ieJts866dpdEdb2c,5140
17
+ spaps_client/secure_messages_async.py,sha256=CGMAFTbI-Na0VgA48o-JLEURDoxF4trxSuDscm1Nuhk,3705
18
+ spaps_client/sessions.py,sha256=74wyOIdCaKU8qJPFsXCPK8JLcuPeS1meCNo805yw-9U,8502
19
+ spaps_client/sessions_async.py,sha256=5L5CVqB5nh3jwTQfrUZo_narMCNbNtXBP-u-ySy5ylI,5605
20
+ spaps_client/storage.py,sha256=VrDBwj23CUpCAw_fH4faLNjzrwsJFgGV8Xth3vxoPTM,4503
21
+ spaps_client/usage.py,sha256=4QVpANBqDLt4xikTt8GBpvtuhTEGyopsveJY_AwNzIQ,7717
22
+ spaps_client/usage_async.py,sha256=HBv2rYxKXOl8rGVJhMRrzbfhltMuh_--O4m7zkZzT2U,4731
23
+ spaps_client/whitelist.py,sha256=EgscgzgTDkuFRvwxOVXsRSNFpOC3I_4ShuDdVNXj0Gs,8127
24
+ spaps_client/whitelist_async.py,sha256=D7le5zhOpU1Y7hOlU831t3TTCvXUjHHjDXkCjnrq-JQ,6716
25
+ spaps-0.1.0.dist-info/METADATA,sha256=prj-CeSPmvGzceLvLZ-g1qHyoo7ael9tjxXDwWDLxlc,6222
26
+ spaps-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ spaps-0.1.0.dist-info/licenses/LICENSE,sha256=62pDTGM_ffmWeJk1DglVsvYY7BMWZxbgvj0o0napi-I,1075
28
+ spaps-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sweet Potato Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,163 @@
1
+ """
2
+ Sweet Potato Authentication & Payment Service Python client.
3
+
4
+ This package exposes modules for authentication, session management,
5
+ payments, and supporting utilities as implementation progresses.
6
+ """
7
+
8
+ from .auth import AuthClient, AuthError, NonceResponse, TokenPair, TokenUser
9
+ from .sessions import (
10
+ SessionError,
11
+ SessionSummary,
12
+ SessionValidationResult,
13
+ SessionListResult,
14
+ SessionRecord,
15
+ SessionTouchResult,
16
+ SessionRevokeResult,
17
+ SessionsClient,
18
+ )
19
+ from .payments import (
20
+ PaymentsClient,
21
+ PaymentsError,
22
+ CheckoutSession,
23
+ PaymentIntent,
24
+ WalletDeposit,
25
+ WalletTransaction,
26
+ SubscriptionPlan,
27
+ SubscriptionDetail,
28
+ SubscriptionCancellation,
29
+ BalanceOverview,
30
+ BalanceAmounts,
31
+ UsageSummary,
32
+ PaymentMethodUpdateResult,
33
+ )
34
+ from .whitelist import (
35
+ WhitelistClient,
36
+ WhitelistError,
37
+ WhitelistEntry,
38
+ WhitelistCheckResult,
39
+ WhitelistListResult,
40
+ WhitelistMessage,
41
+ )
42
+ from .config import Settings, create_http_client
43
+ from .crypto import (
44
+ CryptoPaymentsClient,
45
+ CryptoPaymentsError,
46
+ CryptoInvoice,
47
+ CryptoInvoiceStatus,
48
+ CryptoReconcileJob,
49
+ verify_crypto_webhook_signature,
50
+ )
51
+ from .usage import (
52
+ UsageClient,
53
+ UsageError,
54
+ UsagePeriod,
55
+ UsageFeature,
56
+ UsageFeaturesResponse,
57
+ UsageRecordUsage,
58
+ UsageRecordResult,
59
+ UsageHistoryEntry,
60
+ UsageHistoryResponse,
61
+ )
62
+ from .secure_messages import (
63
+ SecureMessagesClient,
64
+ SecureMessagesError,
65
+ SecureMessage,
66
+ )
67
+ from .metrics import MetricsClient
68
+ from .auth_async import AsyncAuthClient
69
+ from .sessions_async import AsyncSessionsClient
70
+ from .payments_async import AsyncPaymentsClient
71
+ from .usage_async import AsyncUsageClient
72
+ from .whitelist_async import AsyncWhitelistClient
73
+ from .secure_messages_async import AsyncSecureMessagesClient
74
+ from .metrics_async import AsyncMetricsClient
75
+ from .crypto_async import AsyncCryptoPaymentsClient
76
+ from .async_client import AsyncSpapsClient
77
+ from .http_async import RetryAsyncClient
78
+ from .client import SpapsClient
79
+ from .storage import (
80
+ StoredTokens,
81
+ TokenStorage,
82
+ InMemoryTokenStorage,
83
+ FileTokenStorage,
84
+ )
85
+ from .http import RetryConfig, LoggingHooks, default_logging_hooks
86
+
87
+ __all__ = [
88
+ "__version__",
89
+ "AuthClient",
90
+ "AuthError",
91
+ "NonceResponse",
92
+ "TokenPair",
93
+ "TokenUser",
94
+ "SessionsClient",
95
+ "SessionListResult",
96
+ "SessionRecord",
97
+ "SessionTouchResult",
98
+ "SessionRevokeResult",
99
+ "PaymentsClient",
100
+ "PaymentsError",
101
+ "CheckoutSession",
102
+ "PaymentIntent",
103
+ "WalletDeposit",
104
+ "WalletTransaction",
105
+ "SubscriptionPlan",
106
+ "SubscriptionDetail",
107
+ "SubscriptionCancellation",
108
+ "BalanceOverview",
109
+ "BalanceAmounts",
110
+ "UsageSummary",
111
+ "PaymentMethodUpdateResult",
112
+ "WhitelistClient",
113
+ "WhitelistError",
114
+ "WhitelistEntry",
115
+ "WhitelistCheckResult",
116
+ "WhitelistListResult",
117
+ "WhitelistMessage",
118
+ "Settings",
119
+ "create_http_client",
120
+ "SessionError",
121
+ "SessionSummary",
122
+ "SessionValidationResult",
123
+ "CryptoPaymentsClient",
124
+ "CryptoPaymentsError",
125
+ "CryptoInvoice",
126
+ "CryptoInvoiceStatus",
127
+ "CryptoReconcileJob",
128
+ "verify_crypto_webhook_signature",
129
+ "UsageClient",
130
+ "UsageError",
131
+ "UsagePeriod",
132
+ "UsageFeature",
133
+ "UsageFeaturesResponse",
134
+ "UsageRecordUsage",
135
+ "UsageRecordResult",
136
+ "UsageHistoryEntry",
137
+ "UsageHistoryResponse",
138
+ "SecureMessagesClient",
139
+ "SecureMessagesError",
140
+ "SecureMessage",
141
+ "MetricsClient",
142
+ "SpapsClient",
143
+ "AsyncSpapsClient",
144
+ "AsyncAuthClient",
145
+ "AsyncSessionsClient",
146
+ "AsyncPaymentsClient",
147
+ "AsyncUsageClient",
148
+ "AsyncWhitelistClient",
149
+ "AsyncSecureMessagesClient",
150
+ "AsyncMetricsClient",
151
+ "AsyncCryptoPaymentsClient",
152
+ "RetryAsyncClient",
153
+ "StoredTokens",
154
+ "TokenStorage",
155
+ "InMemoryTokenStorage",
156
+ "FileTokenStorage",
157
+ "RetryConfig",
158
+ "LoggingHooks",
159
+ "default_logging_hooks",
160
+ ]
161
+
162
+ # Temporary development version; replaced during release automation.
163
+ __version__ = "0.1.0"
@@ -0,0 +1,214 @@
1
+ """Async equivalents for the SPAPS client suite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta, timezone
6
+ from typing import Optional, TYPE_CHECKING
7
+
8
+ import httpx
9
+
10
+ from .auth_async import AsyncAuthClient
11
+ from .sessions_async import AsyncSessionsClient
12
+ from .payments_async import AsyncPaymentsClient
13
+ from .usage_async import AsyncUsageClient
14
+ from .whitelist_async import AsyncWhitelistClient
15
+ from .secure_messages_async import AsyncSecureMessagesClient
16
+ from .metrics_async import AsyncMetricsClient
17
+ from .storage import InMemoryTokenStorage, StoredTokens, TokenStorage
18
+ from .config import Settings
19
+
20
+ if TYPE_CHECKING:
21
+ from .http import RetryConfig, LoggingHooks
22
+
23
+
24
+ class AsyncSpapsClient:
25
+ """Async client mirroring the ergonomics of the synchronous variant."""
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ base_url: Optional[str] = None,
31
+ api_key: Optional[str] = None,
32
+ request_timeout: Optional[float] = None,
33
+ token_storage: Optional[TokenStorage] = None,
34
+ http_client: Optional[httpx.AsyncClient] = None,
35
+ retry_config: Optional["RetryConfig"] = None,
36
+ logging_hooks: Optional["LoggingHooks"] = None,
37
+ ) -> None:
38
+ from .http_async import RetryAsyncClient
39
+
40
+ self.settings = Settings(base_url=base_url, api_key=api_key, request_timeout=request_timeout)
41
+ self._token_storage = token_storage or InMemoryTokenStorage()
42
+ from .http import RetryConfig as _RetryConfig, LoggingHooks as _LoggingHooks
43
+ if isinstance(retry_config, dict):
44
+ retry_config = _RetryConfig(**retry_config)
45
+ if isinstance(logging_hooks, dict):
46
+ logging_hooks = _LoggingHooks(**logging_hooks)
47
+ if http_client is not None:
48
+ self._client = http_client
49
+ self._owns_client = False
50
+ else:
51
+ self._client = RetryAsyncClient(
52
+ base_url=self.settings.base_url.rstrip("/"),
53
+ timeout=self.settings.request_timeout,
54
+ retry_config=retry_config,
55
+ logging_hooks=logging_hooks,
56
+ )
57
+ self._owns_client = True
58
+
59
+ self._auth = AsyncAuthClient(
60
+ base_url=self.settings.base_url,
61
+ api_key=self.settings.api_key,
62
+ client=self._client,
63
+ token_storage=self._token_storage,
64
+ )
65
+
66
+ self._sessions: Optional[AsyncSessionsClient] = None
67
+ self._payments: Optional[AsyncPaymentsClient] = None
68
+ self._usage: Optional[AsyncUsageClient] = None
69
+ self._whitelist: Optional[AsyncWhitelistClient] = None
70
+ self._secure_messages: Optional[AsyncSecureMessagesClient] = None
71
+ self._metrics: Optional[AsyncMetricsClient] = None
72
+
73
+ # Factories --------------------------------------------------------
74
+
75
+ @property
76
+ def auth(self) -> AsyncAuthClient:
77
+ return self._auth
78
+
79
+ @property
80
+ def sessions(self) -> AsyncSessionsClient:
81
+ access_token = self._require_access_token()
82
+ if self._sessions is None:
83
+ self._sessions = AsyncSessionsClient(
84
+ base_url=self.settings.base_url,
85
+ api_key=self.settings.api_key,
86
+ access_token=access_token,
87
+ client=self._client,
88
+ )
89
+ else:
90
+ self._sessions.access_token = access_token
91
+ return self._sessions
92
+
93
+ @property
94
+ def payments(self) -> AsyncPaymentsClient:
95
+ access_token = self._require_access_token()
96
+ if self._payments is None:
97
+ self._payments = AsyncPaymentsClient(
98
+ base_url=self.settings.base_url,
99
+ api_key=self.settings.api_key,
100
+ access_token=access_token,
101
+ client=self._client,
102
+ )
103
+ else:
104
+ self._payments.access_token = access_token
105
+ return self._payments
106
+
107
+ @property
108
+ def usage(self) -> AsyncUsageClient:
109
+ access_token = self._require_access_token()
110
+ if self._usage is None:
111
+ self._usage = AsyncUsageClient(
112
+ base_url=self.settings.base_url,
113
+ api_key=self.settings.api_key,
114
+ access_token=access_token,
115
+ client=self._client,
116
+ )
117
+ else:
118
+ self._usage.access_token = access_token
119
+ return self._usage
120
+
121
+ @property
122
+ def whitelist(self) -> AsyncWhitelistClient:
123
+ access_token = self._require_access_token()
124
+ if self._whitelist is None:
125
+ self._whitelist = AsyncWhitelistClient(
126
+ base_url=self.settings.base_url,
127
+ api_key=self.settings.api_key,
128
+ access_token=access_token,
129
+ client=self._client,
130
+ )
131
+ else:
132
+ self._whitelist.access_token = access_token
133
+ return self._whitelist
134
+
135
+ @property
136
+ def secure_messages(self) -> AsyncSecureMessagesClient:
137
+ access_token = self._require_access_token()
138
+ if self._secure_messages is None:
139
+ self._secure_messages = AsyncSecureMessagesClient(
140
+ base_url=self.settings.base_url,
141
+ api_key=self.settings.api_key,
142
+ access_token=access_token,
143
+ client=self._client,
144
+ )
145
+ else:
146
+ self._secure_messages.access_token = access_token
147
+ return self._secure_messages
148
+
149
+ @property
150
+ def metrics(self) -> AsyncMetricsClient:
151
+ if self._metrics is None:
152
+ self._metrics = AsyncMetricsClient(base_url=self.settings.base_url, client=self._client)
153
+ return self._metrics
154
+
155
+ # Token helpers ----------------------------------------------------
156
+
157
+ @property
158
+ def token_storage(self) -> TokenStorage:
159
+ return self._token_storage
160
+
161
+ def get_tokens(self) -> Optional[StoredTokens]:
162
+ return self._token_storage.load()
163
+
164
+ def set_tokens(
165
+ self,
166
+ *,
167
+ access_token: str,
168
+ refresh_token: Optional[str] = None,
169
+ token_type: Optional[str] = "Bearer",
170
+ expires_in: Optional[int] = None,
171
+ expires_at: Optional[datetime] = None,
172
+ ) -> None:
173
+ if expires_at is None and expires_in is not None:
174
+ expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in)
175
+ tokens = StoredTokens(
176
+ access_token=access_token,
177
+ refresh_token=refresh_token,
178
+ expires_at=expires_at,
179
+ token_type=token_type,
180
+ )
181
+ self._token_storage.save(tokens)
182
+
183
+ def clear_tokens(self) -> None:
184
+ self._token_storage.clear()
185
+
186
+ # Lifecycle --------------------------------------------------------
187
+
188
+ async def aclose(self) -> None:
189
+ await self._auth.aclose()
190
+ for candidate in (
191
+ self._sessions,
192
+ self._payments,
193
+ self._usage,
194
+ self._whitelist,
195
+ self._secure_messages,
196
+ self._metrics,
197
+ ):
198
+ if candidate is not None:
199
+ await candidate.aclose()
200
+ if self._owns_client:
201
+ await self._client.aclose()
202
+
203
+ # Internal helpers -------------------------------------------------
204
+
205
+ def _require_access_token(self) -> str:
206
+ tokens = self._token_storage.load()
207
+ if not tokens or not tokens.access_token:
208
+ raise ValueError(
209
+ "Access token not found. Authenticate via auth helpers or call set_tokens()."
210
+ )
211
+ return tokens.access_token
212
+
213
+
214
+ __all__ = ["AsyncSpapsClient"]