paycascade-sdk 0.1.1__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.
- paycascade_sdk/__init__.py +40 -0
- paycascade_sdk/auth_intercept.py +387 -0
- paycascade_sdk-0.1.1.dist-info/METADATA +19 -0
- paycascade_sdk-0.1.1.dist-info/RECORD +7 -0
- paycascade_sdk-0.1.1.dist-info/WHEEL +5 -0
- paycascade_sdk-0.1.1.dist-info/licenses/LICENSE.md +26 -0
- paycascade_sdk-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright (c) 2026 BitBerry Labs. SPDX-License-Identifier: MIT
|
|
2
|
+
"""PayCascade customer-side SDK: cache and access middleware."""
|
|
3
|
+
|
|
4
|
+
from paycascade_sdk.auth_intercept import (
|
|
5
|
+
DEFAULT_EXEMPT_ROUTES,
|
|
6
|
+
DEFAULT_MAX_STALENESS_SECONDS,
|
|
7
|
+
DEFAULT_READ_METHODS,
|
|
8
|
+
AccessDecision,
|
|
9
|
+
CachedSubscriptionState,
|
|
10
|
+
EscalationLevel,
|
|
11
|
+
FailPolicy,
|
|
12
|
+
InMemoryStateCache,
|
|
13
|
+
PayCascadeClient,
|
|
14
|
+
StateCache,
|
|
15
|
+
StatePoller,
|
|
16
|
+
calculate_jittered_interval,
|
|
17
|
+
enforce_cached_state,
|
|
18
|
+
initialize_state_cache,
|
|
19
|
+
is_route_exempt,
|
|
20
|
+
refresh_subscription_state,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DEFAULT_EXEMPT_ROUTES",
|
|
25
|
+
"DEFAULT_MAX_STALENESS_SECONDS",
|
|
26
|
+
"DEFAULT_READ_METHODS",
|
|
27
|
+
"AccessDecision",
|
|
28
|
+
"CachedSubscriptionState",
|
|
29
|
+
"EscalationLevel",
|
|
30
|
+
"FailPolicy",
|
|
31
|
+
"InMemoryStateCache",
|
|
32
|
+
"PayCascadeClient",
|
|
33
|
+
"StateCache",
|
|
34
|
+
"StatePoller",
|
|
35
|
+
"calculate_jittered_interval",
|
|
36
|
+
"enforce_cached_state",
|
|
37
|
+
"initialize_state_cache",
|
|
38
|
+
"is_route_exempt",
|
|
39
|
+
"refresh_subscription_state",
|
|
40
|
+
]
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Copyright (c) 2026 BitBerry Labs. SPDX-License-Identifier: MIT
|
|
2
|
+
"""Customer-side SDK cache and access middleware for PayCascade.
|
|
3
|
+
|
|
4
|
+
Provides background polling of the PayCascade hosted GET /api/v1/state endpoint,
|
|
5
|
+
in-memory caching with jitter, and request-level access enforcement policies.
|
|
6
|
+
This module is part of the standalone `paycascade-sdk` package and must never
|
|
7
|
+
import from `app/` or require server-side dependencies.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from enum import Enum
|
|
15
|
+
import random
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Callable, Sequence
|
|
19
|
+
from uuid import UUID
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EscalationLevel(str, Enum):
|
|
24
|
+
HEALTHY = "HEALTHY"
|
|
25
|
+
SOFT_WARNING = "SOFT_WARNING"
|
|
26
|
+
SCOPE_RESTRICTED = "SCOPE_RESTRICTED"
|
|
27
|
+
HARD_LOCKED = "HARD_LOCKED"
|
|
28
|
+
GRACE_PERIOD_EXPIRED = "GRACE_PERIOD_EXPIRED"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FailPolicy(str, Enum):
|
|
32
|
+
FAIL_OPEN = "FAIL_OPEN"
|
|
33
|
+
FAIL_CLOSED = "FAIL_CLOSED"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class CachedSubscriptionState:
|
|
38
|
+
subscription_id: str
|
|
39
|
+
level: EscalationLevel
|
|
40
|
+
failed_at: datetime | None
|
|
41
|
+
retry_count: int
|
|
42
|
+
evaluated_at: datetime
|
|
43
|
+
cached_at: datetime
|
|
44
|
+
|
|
45
|
+
def is_stale(self, max_staleness_seconds: float, now: datetime | None = None) -> bool:
|
|
46
|
+
current_time = now or datetime.now(timezone.utc)
|
|
47
|
+
cached_time = self.cached_at
|
|
48
|
+
if cached_time.tzinfo is None:
|
|
49
|
+
cached_time = cached_time.replace(tzinfo=timezone.utc)
|
|
50
|
+
if current_time.tzinfo is None:
|
|
51
|
+
current_time = current_time.replace(tzinfo=timezone.utc)
|
|
52
|
+
age = (current_time - cached_time).total_seconds()
|
|
53
|
+
return age > max_staleness_seconds
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class AccessDecision:
|
|
58
|
+
allowed: bool
|
|
59
|
+
level: EscalationLevel | None
|
|
60
|
+
reason: str
|
|
61
|
+
warning: bool = False
|
|
62
|
+
is_stale: bool = False
|
|
63
|
+
is_exempt_route: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
DEFAULT_EXEMPT_ROUTES: tuple[str, ...] = (
|
|
67
|
+
"/billing",
|
|
68
|
+
"/stripe/portal-session",
|
|
69
|
+
"/logout",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
DEFAULT_READ_METHODS: tuple[str, ...] = ("GET", "HEAD", "OPTIONS")
|
|
73
|
+
DEFAULT_MAX_STALENESS_SECONDS: float = 86400.0 # 24 hours
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class StateCache:
|
|
77
|
+
"""Interface / base class for customer subscription state caching."""
|
|
78
|
+
|
|
79
|
+
def get(self, subscription_id: str) -> CachedSubscriptionState | None:
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
def set(self, state: CachedSubscriptionState) -> None:
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class InMemoryStateCache(StateCache):
|
|
87
|
+
"""Thread-safe in-memory cache for subscription states."""
|
|
88
|
+
|
|
89
|
+
def __init__(self) -> None:
|
|
90
|
+
self._lock = threading.RLock()
|
|
91
|
+
self._cache: dict[str, CachedSubscriptionState] = {}
|
|
92
|
+
|
|
93
|
+
def get(self, subscription_id: str) -> CachedSubscriptionState | None:
|
|
94
|
+
with self._lock:
|
|
95
|
+
return self._cache.get(str(subscription_id))
|
|
96
|
+
|
|
97
|
+
def set(self, state: CachedSubscriptionState) -> None:
|
|
98
|
+
with self._lock:
|
|
99
|
+
self._cache[str(state.subscription_id)] = state
|
|
100
|
+
|
|
101
|
+
def clear(self) -> None:
|
|
102
|
+
with self._lock:
|
|
103
|
+
self._cache.clear()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_iso_datetime(value: str | None) -> datetime | None:
|
|
107
|
+
if value is None:
|
|
108
|
+
return None
|
|
109
|
+
# Normalize Z to +00:00 for fromisoformat compatibility
|
|
110
|
+
clean_value = value.replace("Z", "+00:00")
|
|
111
|
+
dt = datetime.fromisoformat(clean_value)
|
|
112
|
+
if dt.tzinfo is None:
|
|
113
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
114
|
+
return dt.astimezone(timezone.utc)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PayCascadeClient:
|
|
118
|
+
"""Client for interacting with the PayCascade hosted API contract."""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
base_url: str,
|
|
123
|
+
api_key: str,
|
|
124
|
+
http_client: httpx.Client | None = None,
|
|
125
|
+
timeout: float = 10.0,
|
|
126
|
+
) -> None:
|
|
127
|
+
self.base_url = base_url.rstrip("/")
|
|
128
|
+
self.api_key = api_key
|
|
129
|
+
self.timeout = timeout
|
|
130
|
+
self._client = http_client or httpx.Client(timeout=timeout)
|
|
131
|
+
self._owns_client = http_client is None
|
|
132
|
+
|
|
133
|
+
def close(self) -> None:
|
|
134
|
+
if self._owns_client:
|
|
135
|
+
self._client.close()
|
|
136
|
+
|
|
137
|
+
def fetch_state(self, subscription_id: str | UUID) -> CachedSubscriptionState:
|
|
138
|
+
url = f"{self.base_url}/api/v1/state"
|
|
139
|
+
headers = {
|
|
140
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
141
|
+
"Accept": "application/json",
|
|
142
|
+
}
|
|
143
|
+
params = {"subscription_id": str(subscription_id)}
|
|
144
|
+
|
|
145
|
+
response = self._client.get(url, headers=headers, params=params)
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
data = response.json()
|
|
148
|
+
|
|
149
|
+
now = datetime.now(timezone.utc)
|
|
150
|
+
return CachedSubscriptionState(
|
|
151
|
+
subscription_id=str(data["subscription_id"]),
|
|
152
|
+
level=EscalationLevel(data["level"]),
|
|
153
|
+
failed_at=parse_iso_datetime(data.get("failed_at")),
|
|
154
|
+
retry_count=int(data["retry_count"]),
|
|
155
|
+
evaluated_at=parse_iso_datetime(data["evaluated_at"]) or now,
|
|
156
|
+
cached_at=now,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def calculate_jittered_interval(interval_seconds: float, jitter_ratio: float = 0.2) -> float:
|
|
161
|
+
"""Compute a randomized interval with +/- jitter_ratio spread."""
|
|
162
|
+
if jitter_ratio <= 0.0:
|
|
163
|
+
return interval_seconds
|
|
164
|
+
jitter_bound = interval_seconds * jitter_ratio
|
|
165
|
+
return random.uniform(interval_seconds - jitter_bound, interval_seconds + jitter_bound)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class StatePoller:
|
|
169
|
+
"""Background poller that periodically refreshes subscription state into a cache."""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
client: PayCascadeClient,
|
|
174
|
+
cache: StateCache,
|
|
175
|
+
subscription_ids: Sequence[str | UUID] | Callable[[], Sequence[str | UUID]],
|
|
176
|
+
interval_seconds: float = 300.0,
|
|
177
|
+
jitter_ratio: float = 0.2,
|
|
178
|
+
on_error: Callable[[str, Exception], None] | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
self.client = client
|
|
181
|
+
self.cache = cache
|
|
182
|
+
self.subscription_ids = subscription_ids
|
|
183
|
+
self.interval_seconds = interval_seconds
|
|
184
|
+
self.jitter_ratio = jitter_ratio
|
|
185
|
+
self.on_error = on_error
|
|
186
|
+
self._stop_event = threading.Event()
|
|
187
|
+
self._thread: threading.Thread | None = None
|
|
188
|
+
|
|
189
|
+
def _get_subscription_ids(self) -> Sequence[str | UUID]:
|
|
190
|
+
if callable(self.subscription_ids):
|
|
191
|
+
return self.subscription_ids()
|
|
192
|
+
return self.subscription_ids
|
|
193
|
+
|
|
194
|
+
def refresh_all(self) -> dict[str, CachedSubscriptionState]:
|
|
195
|
+
refreshed: dict[str, CachedSubscriptionState] = {}
|
|
196
|
+
for sub_id in self._get_subscription_ids():
|
|
197
|
+
sub_id_str = str(sub_id)
|
|
198
|
+
try:
|
|
199
|
+
state = self.client.fetch_state(sub_id_str)
|
|
200
|
+
self.cache.set(state)
|
|
201
|
+
refreshed[sub_id_str] = state
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
if self.on_error:
|
|
204
|
+
self.on_error(sub_id_str, exc)
|
|
205
|
+
return refreshed
|
|
206
|
+
|
|
207
|
+
def run_loop(self) -> None:
|
|
208
|
+
while not self._stop_event.is_set():
|
|
209
|
+
self.refresh_all()
|
|
210
|
+
delay = calculate_jittered_interval(self.interval_seconds, self.jitter_ratio)
|
|
211
|
+
if self._stop_event.wait(timeout=max(0.01, delay)):
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
def start(self) -> None:
|
|
215
|
+
if self._thread is not None and self._thread.is_alive():
|
|
216
|
+
return
|
|
217
|
+
self._stop_event.clear()
|
|
218
|
+
self._thread = threading.Thread(target=self.run_loop, daemon=True, name="PayCascadeStatePoller")
|
|
219
|
+
self._thread.start()
|
|
220
|
+
|
|
221
|
+
def stop(self, timeout: float = 5.0) -> None:
|
|
222
|
+
self._stop_event.set()
|
|
223
|
+
if self._thread is not None and self._thread.is_alive():
|
|
224
|
+
self._thread.join(timeout=timeout)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def is_route_exempt(path: str, exempt_routes: Sequence[str] = DEFAULT_EXEMPT_ROUTES) -> bool:
|
|
228
|
+
"""Check if request path matches or is a subpath of an exempt recovery route."""
|
|
229
|
+
normalized_path = "/" + path.strip("/") if not path.startswith("/") else path
|
|
230
|
+
for exempt in exempt_routes:
|
|
231
|
+
normalized_exempt = "/" + exempt.strip("/") if not exempt.startswith("/") else exempt
|
|
232
|
+
if normalized_path == normalized_exempt or normalized_path.startswith(f"{normalized_exempt}/"):
|
|
233
|
+
return True
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def enforce_cached_state(
|
|
238
|
+
subscription_id: str | UUID,
|
|
239
|
+
cache: StateCache,
|
|
240
|
+
path: str,
|
|
241
|
+
method: str = "GET",
|
|
242
|
+
exempt_routes: Sequence[str] = DEFAULT_EXEMPT_ROUTES,
|
|
243
|
+
fail_policy: FailPolicy = FailPolicy.FAIL_OPEN,
|
|
244
|
+
max_staleness_seconds: float = DEFAULT_MAX_STALENESS_SECONDS,
|
|
245
|
+
read_methods: Sequence[str] = DEFAULT_READ_METHODS,
|
|
246
|
+
now: datetime | None = None,
|
|
247
|
+
) -> AccessDecision:
|
|
248
|
+
"""Enforce access control policies based strictly on cached state."""
|
|
249
|
+
sub_id_str = str(subscription_id)
|
|
250
|
+
|
|
251
|
+
# 1. Exempt recovery routes always bypass enforcement
|
|
252
|
+
if is_route_exempt(path, exempt_routes):
|
|
253
|
+
cached = cache.get(sub_id_str)
|
|
254
|
+
level = cached.level if cached else None
|
|
255
|
+
return AccessDecision(
|
|
256
|
+
allowed=True,
|
|
257
|
+
level=level,
|
|
258
|
+
reason="Route is exempt from payment access controls",
|
|
259
|
+
warning=False,
|
|
260
|
+
is_stale=False,
|
|
261
|
+
is_exempt_route=True,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
cached = cache.get(sub_id_str)
|
|
265
|
+
|
|
266
|
+
# 2. Cache miss handling
|
|
267
|
+
if cached is None:
|
|
268
|
+
if fail_policy == FailPolicy.FAIL_OPEN:
|
|
269
|
+
return AccessDecision(
|
|
270
|
+
allowed=True,
|
|
271
|
+
level=None,
|
|
272
|
+
reason="Cache miss: fail-open policy allowed request",
|
|
273
|
+
warning=False,
|
|
274
|
+
is_stale=False,
|
|
275
|
+
is_exempt_route=False,
|
|
276
|
+
)
|
|
277
|
+
return AccessDecision(
|
|
278
|
+
allowed=False,
|
|
279
|
+
level=None,
|
|
280
|
+
reason="Cache miss: fail-closed policy blocked request",
|
|
281
|
+
warning=False,
|
|
282
|
+
is_stale=False,
|
|
283
|
+
is_exempt_route=False,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# 3. Staleness evaluation
|
|
287
|
+
is_stale = cached.is_stale(max_staleness_seconds=max_staleness_seconds, now=now)
|
|
288
|
+
if is_stale:
|
|
289
|
+
if fail_policy == FailPolicy.FAIL_OPEN:
|
|
290
|
+
return AccessDecision(
|
|
291
|
+
allowed=True,
|
|
292
|
+
level=cached.level,
|
|
293
|
+
reason=f"Cached state is stale (> {max_staleness_seconds}s): fail-open policy allowed request",
|
|
294
|
+
warning=True,
|
|
295
|
+
is_stale=True,
|
|
296
|
+
is_exempt_route=False,
|
|
297
|
+
)
|
|
298
|
+
return AccessDecision(
|
|
299
|
+
allowed=False,
|
|
300
|
+
level=cached.level,
|
|
301
|
+
reason=f"Cached state is stale (> {max_staleness_seconds}s): fail-closed policy blocked request",
|
|
302
|
+
warning=False,
|
|
303
|
+
is_stale=True,
|
|
304
|
+
is_exempt_route=False,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# 4. Active escalation level enforcement
|
|
308
|
+
normalized_method = method.upper()
|
|
309
|
+
level = cached.level
|
|
310
|
+
|
|
311
|
+
if level == EscalationLevel.HEALTHY:
|
|
312
|
+
return AccessDecision(
|
|
313
|
+
allowed=True,
|
|
314
|
+
level=level,
|
|
315
|
+
reason="Subscription is healthy",
|
|
316
|
+
warning=False,
|
|
317
|
+
is_stale=False,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if level == EscalationLevel.SOFT_WARNING:
|
|
321
|
+
return AccessDecision(
|
|
322
|
+
allowed=True,
|
|
323
|
+
level=level,
|
|
324
|
+
reason="Payment delinquent (soft warning): access allowed with warning indicator",
|
|
325
|
+
warning=True,
|
|
326
|
+
is_stale=False,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if level == EscalationLevel.SCOPE_RESTRICTED:
|
|
330
|
+
if normalized_method in (m.upper() for m in read_methods):
|
|
331
|
+
return AccessDecision(
|
|
332
|
+
allowed=True,
|
|
333
|
+
level=level,
|
|
334
|
+
reason="Payment delinquent (scope restricted): read access allowed",
|
|
335
|
+
warning=True,
|
|
336
|
+
is_stale=False,
|
|
337
|
+
)
|
|
338
|
+
return AccessDecision(
|
|
339
|
+
allowed=False,
|
|
340
|
+
level=level,
|
|
341
|
+
reason="Payment delinquent (scope restricted): mutating write operations are blocked",
|
|
342
|
+
warning=True,
|
|
343
|
+
is_stale=False,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if level == EscalationLevel.HARD_LOCKED:
|
|
347
|
+
return AccessDecision(
|
|
348
|
+
allowed=False,
|
|
349
|
+
level=level,
|
|
350
|
+
reason="Payment delinquent (hard locked): protected application access is blocked",
|
|
351
|
+
warning=True,
|
|
352
|
+
is_stale=False,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if level == EscalationLevel.GRACE_PERIOD_EXPIRED:
|
|
356
|
+
return AccessDecision(
|
|
357
|
+
allowed=False,
|
|
358
|
+
level=level,
|
|
359
|
+
reason="Grace period expired: protected application access is blocked",
|
|
360
|
+
warning=False,
|
|
361
|
+
is_stale=False,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Fallback safety guard
|
|
365
|
+
return AccessDecision(
|
|
366
|
+
allowed=False,
|
|
367
|
+
level=level,
|
|
368
|
+
reason=f"Unknown escalation level: {level}",
|
|
369
|
+
warning=False,
|
|
370
|
+
is_stale=False,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def initialize_state_cache() -> StateCache:
|
|
375
|
+
"""Helper factory to create a default in-memory state cache."""
|
|
376
|
+
return InMemoryStateCache()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def refresh_subscription_state(
|
|
380
|
+
client: PayCascadeClient,
|
|
381
|
+
cache: StateCache,
|
|
382
|
+
subscription_id: str | UUID,
|
|
383
|
+
) -> CachedSubscriptionState:
|
|
384
|
+
"""Synchronously refresh and cache subscription state for a single subscription."""
|
|
385
|
+
state = client.fetch_state(subscription_id)
|
|
386
|
+
cache.set(state)
|
|
387
|
+
return state
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paycascade-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Customer-side cache and access middleware for the PayCascade hosted billing-state API.
|
|
5
|
+
Author: BitBerry Labs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
License-File: LICENSE.md
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
paycascade_sdk/__init__.py,sha256=CAnqlEGsY_SnthyKWljXV-bhFNR-Wm0xTGacy0Q91CY,914
|
|
2
|
+
paycascade_sdk/auth_intercept.py,sha256=Rkp0z9ut8OECBuw3pkh9PWJhlmE4EXf7DVUZPf-d380,11204
|
|
3
|
+
paycascade_sdk-0.1.1.dist-info/licenses/LICENSE.md,sha256=PO_mgqJjeQn1eDM0cRNP-uIqxZDrZhM1y8G_ORR6BNI,1372
|
|
4
|
+
paycascade_sdk-0.1.1.dist-info/METADATA,sha256=XA41YZ7AqULkXHLCCzNMDaIop6Oz86-VVAJ5hOsXFvc,759
|
|
5
|
+
paycascade_sdk-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
paycascade_sdk-0.1.1.dist-info/top_level.txt,sha256=XWU_tQqPJogAFHCyJ-Bjfjyr7WpVJ1sp_qrT4IkwQKg,15
|
|
7
|
+
paycascade_sdk-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BitBerry Labs
|
|
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
|
+
|
|
23
|
+
SCOPE: This license applies only to the PayCascade customer SDK files in this
|
|
24
|
+
directory and the corresponding SDK package. It does not license the PayCascade
|
|
25
|
+
server or any other files in the repository, and it grants no rights to
|
|
26
|
+
BitBerry Labs trademarks. The root LICENSE.md governs all other files.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
paycascade_sdk
|