subscription-sdk 1.0.0__tar.gz
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.
- subscription_sdk-1.0.0/PKG-INFO +139 -0
- subscription_sdk-1.0.0/README.md +119 -0
- subscription_sdk-1.0.0/pyproject.toml +30 -0
- subscription_sdk-1.0.0/setup.cfg +4 -0
- subscription_sdk-1.0.0/subscription_sdk/__init__.py +24 -0
- subscription_sdk-1.0.0/subscription_sdk/adapters/fastapi_adapter.py +105 -0
- subscription_sdk-1.0.0/subscription_sdk/adapters/flask_adapter.py +84 -0
- subscription_sdk-1.0.0/subscription_sdk/client.py +198 -0
- subscription_sdk-1.0.0/subscription_sdk/config_manager.py +16 -0
- subscription_sdk-1.0.0/subscription_sdk/poller.py +74 -0
- subscription_sdk-1.0.0/subscription_sdk/route_matcher.py +63 -0
- subscription_sdk-1.0.0/subscription_sdk/types.py +58 -0
- subscription_sdk-1.0.0/subscription_sdk.egg-info/PKG-INFO +139 -0
- subscription_sdk-1.0.0/subscription_sdk.egg-info/SOURCES.txt +21 -0
- subscription_sdk-1.0.0/subscription_sdk.egg-info/dependency_links.txt +1 -0
- subscription_sdk-1.0.0/subscription_sdk.egg-info/requires.txt +10 -0
- subscription_sdk-1.0.0/subscription_sdk.egg-info/top_level.txt +1 -0
- subscription_sdk-1.0.0/tests/test_client.py +203 -0
- subscription_sdk-1.0.0/tests/test_config_manager.py +42 -0
- subscription_sdk-1.0.0/tests/test_fastapi_middleware.py +147 -0
- subscription_sdk-1.0.0/tests/test_flask_middleware.py +132 -0
- subscription_sdk-1.0.0/tests/test_poller.py +46 -0
- subscription_sdk-1.0.0/tests/test_route_matcher.py +92 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: subscription-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python Client SDK for Subscription Platform
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: urllib3>=2.0.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
|
|
15
|
+
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
16
|
+
Requires-Dist: flask>=2.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: fastapi>=0.80.0; extra == "dev"
|
|
18
|
+
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
19
|
+
Requires-Dist: uvicorn>=0.15.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# subscription-sdk-python
|
|
22
|
+
|
|
23
|
+
Python SDK for the Subscription and Entitlements Platform.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install subscription-sdk
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Middleware Adapters
|
|
32
|
+
|
|
33
|
+
### Flask
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from flask import Flask, request, jsonify
|
|
37
|
+
from subscription_sdk.adapters.flask_adapter import subscription_flask
|
|
38
|
+
|
|
39
|
+
app = Flask(__name__)
|
|
40
|
+
|
|
41
|
+
# Apply Flask Middleware
|
|
42
|
+
subscription_flask(
|
|
43
|
+
app,
|
|
44
|
+
api_key="sub_live_...",
|
|
45
|
+
base_url="https://api.infosub.io",
|
|
46
|
+
get_org_id=lambda: request.headers.get("X-Org-Id")
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@app.route("/api/v1/employees", methods=["GET"])
|
|
50
|
+
def get_employees():
|
|
51
|
+
return jsonify({"data": []}), 200
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### FastAPI
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from fastapi import FastAPI, Request
|
|
58
|
+
from subscription_sdk.adapters.fastapi_adapter import SubscriptionMiddleware
|
|
59
|
+
|
|
60
|
+
app = FastAPI()
|
|
61
|
+
|
|
62
|
+
# Apply FastAPI Middleware
|
|
63
|
+
app.add_middleware(
|
|
64
|
+
SubscriptionMiddleware,
|
|
65
|
+
api_key="sub_live_...",
|
|
66
|
+
base_url="https://api.infosub.io",
|
|
67
|
+
get_org_id=lambda req: req.headers.get("x-org-id")
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@app.get("/api/v1/employees")
|
|
71
|
+
def get_employees(request: Request):
|
|
72
|
+
return {"data": []}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Manual Client Usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from subscription_sdk import SubscriptionClient
|
|
79
|
+
|
|
80
|
+
client = SubscriptionClient(api_key="sub_live_...", base_url="https://api.infosub.io")
|
|
81
|
+
client.start() # Starts background poller thread
|
|
82
|
+
|
|
83
|
+
mapping = client.match_route("GET", "/api/v1/employees")
|
|
84
|
+
if mapping:
|
|
85
|
+
check = client.check_entitlement(
|
|
86
|
+
organization_external_id="tenant-acme",
|
|
87
|
+
resource_mapping_id=mapping.id,
|
|
88
|
+
quantity=1.0
|
|
89
|
+
)
|
|
90
|
+
if check.allowed:
|
|
91
|
+
try:
|
|
92
|
+
# Execute business logic...
|
|
93
|
+
|
|
94
|
+
client.commit_reservation(check.reservationId)
|
|
95
|
+
except Exception:
|
|
96
|
+
client.cancel_reservation(check.reservationId)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Alternatively, you can simplify the manual transaction pattern using `.execute(...)`, which handles the check-reserve-commit-cancel flow automatically.
|
|
100
|
+
|
|
101
|
+
**Basic Example:**
|
|
102
|
+
```python
|
|
103
|
+
# The execute method takes a callback function to run
|
|
104
|
+
result = client.execute(
|
|
105
|
+
organization_id="tenant-acme",
|
|
106
|
+
resource_mapping_id=mapping.id,
|
|
107
|
+
callback=lambda: save_employee(employee),
|
|
108
|
+
quantity=1.0
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Flask / FastAPI Route Handler Example:**
|
|
113
|
+
```python
|
|
114
|
+
@app.route("/employees-manual", methods=["POST"])
|
|
115
|
+
def create_employee_manual():
|
|
116
|
+
org_id = request.headers.get("X-Org-Id") or request.args.get("orgId")
|
|
117
|
+
mapping = client.match_route("POST", "/employees")
|
|
118
|
+
|
|
119
|
+
if not mapping:
|
|
120
|
+
return jsonify({"error": "Mapping not found"}), 500
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
result = client.execute(
|
|
124
|
+
organization_id=org_id,
|
|
125
|
+
resource_mapping_id=mapping.id,
|
|
126
|
+
quantity=1.0,
|
|
127
|
+
callback=lambda: {
|
|
128
|
+
"status": "success",
|
|
129
|
+
"message": "Employee created successfully via manual execute wrapper."
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
return jsonify(result), 200
|
|
133
|
+
except PermissionError as err:
|
|
134
|
+
# Throws PermissionError if entitlement check is denied (check.allowed is False)
|
|
135
|
+
return jsonify({"status": "error", "message": str(err)}), 403
|
|
136
|
+
except Exception as err:
|
|
137
|
+
return jsonify({"status": "error", "message": str(err)}), 500
|
|
138
|
+
```
|
|
139
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# subscription-sdk-python
|
|
2
|
+
|
|
3
|
+
Python SDK for the Subscription and Entitlements Platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install subscription-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Middleware Adapters
|
|
12
|
+
|
|
13
|
+
### Flask
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from flask import Flask, request, jsonify
|
|
17
|
+
from subscription_sdk.adapters.flask_adapter import subscription_flask
|
|
18
|
+
|
|
19
|
+
app = Flask(__name__)
|
|
20
|
+
|
|
21
|
+
# Apply Flask Middleware
|
|
22
|
+
subscription_flask(
|
|
23
|
+
app,
|
|
24
|
+
api_key="sub_live_...",
|
|
25
|
+
base_url="https://api.infosub.io",
|
|
26
|
+
get_org_id=lambda: request.headers.get("X-Org-Id")
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@app.route("/api/v1/employees", methods=["GET"])
|
|
30
|
+
def get_employees():
|
|
31
|
+
return jsonify({"data": []}), 200
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### FastAPI
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from fastapi import FastAPI, Request
|
|
38
|
+
from subscription_sdk.adapters.fastapi_adapter import SubscriptionMiddleware
|
|
39
|
+
|
|
40
|
+
app = FastAPI()
|
|
41
|
+
|
|
42
|
+
# Apply FastAPI Middleware
|
|
43
|
+
app.add_middleware(
|
|
44
|
+
SubscriptionMiddleware,
|
|
45
|
+
api_key="sub_live_...",
|
|
46
|
+
base_url="https://api.infosub.io",
|
|
47
|
+
get_org_id=lambda req: req.headers.get("x-org-id")
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@app.get("/api/v1/employees")
|
|
51
|
+
def get_employees(request: Request):
|
|
52
|
+
return {"data": []}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Manual Client Usage
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from subscription_sdk import SubscriptionClient
|
|
59
|
+
|
|
60
|
+
client = SubscriptionClient(api_key="sub_live_...", base_url="https://api.infosub.io")
|
|
61
|
+
client.start() # Starts background poller thread
|
|
62
|
+
|
|
63
|
+
mapping = client.match_route("GET", "/api/v1/employees")
|
|
64
|
+
if mapping:
|
|
65
|
+
check = client.check_entitlement(
|
|
66
|
+
organization_external_id="tenant-acme",
|
|
67
|
+
resource_mapping_id=mapping.id,
|
|
68
|
+
quantity=1.0
|
|
69
|
+
)
|
|
70
|
+
if check.allowed:
|
|
71
|
+
try:
|
|
72
|
+
# Execute business logic...
|
|
73
|
+
|
|
74
|
+
client.commit_reservation(check.reservationId)
|
|
75
|
+
except Exception:
|
|
76
|
+
client.cancel_reservation(check.reservationId)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Alternatively, you can simplify the manual transaction pattern using `.execute(...)`, which handles the check-reserve-commit-cancel flow automatically.
|
|
80
|
+
|
|
81
|
+
**Basic Example:**
|
|
82
|
+
```python
|
|
83
|
+
# The execute method takes a callback function to run
|
|
84
|
+
result = client.execute(
|
|
85
|
+
organization_id="tenant-acme",
|
|
86
|
+
resource_mapping_id=mapping.id,
|
|
87
|
+
callback=lambda: save_employee(employee),
|
|
88
|
+
quantity=1.0
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Flask / FastAPI Route Handler Example:**
|
|
93
|
+
```python
|
|
94
|
+
@app.route("/employees-manual", methods=["POST"])
|
|
95
|
+
def create_employee_manual():
|
|
96
|
+
org_id = request.headers.get("X-Org-Id") or request.args.get("orgId")
|
|
97
|
+
mapping = client.match_route("POST", "/employees")
|
|
98
|
+
|
|
99
|
+
if not mapping:
|
|
100
|
+
return jsonify({"error": "Mapping not found"}), 500
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
result = client.execute(
|
|
104
|
+
organization_id=org_id,
|
|
105
|
+
resource_mapping_id=mapping.id,
|
|
106
|
+
quantity=1.0,
|
|
107
|
+
callback=lambda: {
|
|
108
|
+
"status": "success",
|
|
109
|
+
"message": "Employee created successfully via manual execute wrapper."
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
return jsonify(result), 200
|
|
113
|
+
except PermissionError as err:
|
|
114
|
+
# Throws PermissionError if entitlement check is denied (check.allowed is False)
|
|
115
|
+
return jsonify({"status": "error", "message": str(err)}), 403
|
|
116
|
+
except Exception as err:
|
|
117
|
+
return jsonify({"status": "error", "message": str(err)}), 500
|
|
118
|
+
```
|
|
119
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "subscription-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python Client SDK for Subscription Platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"urllib3>=2.0.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=7.0.0",
|
|
24
|
+
"pytest-asyncio>=0.20.0",
|
|
25
|
+
"responses>=0.23.0",
|
|
26
|
+
"flask>=2.0.0",
|
|
27
|
+
"fastapi>=0.80.0",
|
|
28
|
+
"httpx>=0.24.0",
|
|
29
|
+
"uvicorn>=0.15.0",
|
|
30
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .types import Mapping, SdkConfig, EntitlementCheckResponse
|
|
2
|
+
from .config_manager import ConfigManager
|
|
3
|
+
from .poller import Poller
|
|
4
|
+
from .route_matcher import RouteMatcher
|
|
5
|
+
from .client import SubscriptionClient
|
|
6
|
+
|
|
7
|
+
# Export flask and fastapi adapters dynamically to avoid hard dependencies if users don't have those libraries installed
|
|
8
|
+
def get_flask_adapter():
|
|
9
|
+
from .adapters.flask_adapter import subscription_flask
|
|
10
|
+
return subscription_flask
|
|
11
|
+
|
|
12
|
+
def get_fastapi_middleware():
|
|
13
|
+
from .adapters.fastapi_adapter import SubscriptionMiddleware
|
|
14
|
+
return SubscriptionMiddleware
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'Mapping',
|
|
18
|
+
'SdkConfig',
|
|
19
|
+
'EntitlementCheckResponse',
|
|
20
|
+
'ConfigManager',
|
|
21
|
+
'Poller',
|
|
22
|
+
'RouteMatcher',
|
|
23
|
+
'SubscriptionClient',
|
|
24
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
from typing import Callable, Optional, Union, Awaitable
|
|
4
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
|
+
from starlette.requests import Request
|
|
6
|
+
from starlette.responses import Response, JSONResponse
|
|
7
|
+
from ..client import SubscriptionClient
|
|
8
|
+
|
|
9
|
+
class SubscriptionMiddleware(BaseHTTPMiddleware):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
app,
|
|
13
|
+
client: Optional[SubscriptionClient] = None,
|
|
14
|
+
get_org_id: Optional[Union[Callable[[Request], str], Callable[[Request], Awaitable[str]]]] = None,
|
|
15
|
+
api_key: Optional[str] = None,
|
|
16
|
+
base_url: Optional[str] = None
|
|
17
|
+
):
|
|
18
|
+
super().__init__(app)
|
|
19
|
+
if not get_org_id:
|
|
20
|
+
raise ValueError("get_org_id callback is required")
|
|
21
|
+
|
|
22
|
+
self.get_org_id = get_org_id
|
|
23
|
+
|
|
24
|
+
if client is None:
|
|
25
|
+
key = api_key or os.environ.get("SUBSCRIPTION_API_KEY")
|
|
26
|
+
url = base_url or os.environ.get("SUBSCRIPTION_BASE_URL")
|
|
27
|
+
if not key or not url:
|
|
28
|
+
raise ValueError("Subscription API Key and Base URL are required when client is not provided")
|
|
29
|
+
self.client = SubscriptionClient(key, url)
|
|
30
|
+
self.client.start()
|
|
31
|
+
else:
|
|
32
|
+
self.client = client
|
|
33
|
+
|
|
34
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
35
|
+
path = request.url.path
|
|
36
|
+
method = request.method
|
|
37
|
+
|
|
38
|
+
mapping = self.client.match_route(method, path)
|
|
39
|
+
if not mapping:
|
|
40
|
+
return await call_next(request)
|
|
41
|
+
|
|
42
|
+
# Resolve org identifier
|
|
43
|
+
if inspect.iscoroutinefunction(self.get_org_id):
|
|
44
|
+
org_id = await self.get_org_id(request)
|
|
45
|
+
else:
|
|
46
|
+
org_id = self.get_org_id(request)
|
|
47
|
+
|
|
48
|
+
if not org_id:
|
|
49
|
+
return JSONResponse(
|
|
50
|
+
status_code=400,
|
|
51
|
+
content={
|
|
52
|
+
"title": "Organization Identity Missing",
|
|
53
|
+
"status": 400,
|
|
54
|
+
"detail": "No organization external identifier resolved for the matching route."
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
check_result = self.client.check_entitlement(org_id, mapping.id, 1.0)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return JSONResponse(
|
|
62
|
+
status_code=500,
|
|
63
|
+
content={
|
|
64
|
+
"title": "Entitlement Resolution Error",
|
|
65
|
+
"status": 500,
|
|
66
|
+
"detail": str(e)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not check_result.allowed:
|
|
71
|
+
detail = check_result.detail or f"Subscription limit reached or feature not enabled: {check_result.featureCode}"
|
|
72
|
+
code = check_result.code or "ENTITLEMENT_DENIED"
|
|
73
|
+
return JSONResponse(
|
|
74
|
+
status_code=403,
|
|
75
|
+
content={
|
|
76
|
+
"title": "Entitlement Denied",
|
|
77
|
+
"status": 403,
|
|
78
|
+
"detail": detail,
|
|
79
|
+
"code": code
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Attach reservation context to request state
|
|
84
|
+
request.state.subscription_reservation_id = check_result.reservationId
|
|
85
|
+
|
|
86
|
+
success = False
|
|
87
|
+
try:
|
|
88
|
+
response = await call_next(request)
|
|
89
|
+
if response.status_code < 400:
|
|
90
|
+
success = True
|
|
91
|
+
return response
|
|
92
|
+
finally:
|
|
93
|
+
reservation_id = getattr(request.state, 'subscription_reservation_id', None)
|
|
94
|
+
if reservation_id:
|
|
95
|
+
try:
|
|
96
|
+
if success:
|
|
97
|
+
self.client.commit_reservation(reservation_id)
|
|
98
|
+
else:
|
|
99
|
+
self.client.cancel_reservation(reservation_id)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
import sys
|
|
102
|
+
print(
|
|
103
|
+
f"[SubscriptionSDK] Failed to {'commit' if success else 'cancel'} reservation {reservation_id}: {e}",
|
|
104
|
+
file=sys.stderr
|
|
105
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
from flask import request, jsonify, g
|
|
4
|
+
from ..client import SubscriptionClient
|
|
5
|
+
|
|
6
|
+
def subscription_flask(
|
|
7
|
+
app,
|
|
8
|
+
client: Optional[SubscriptionClient] = None,
|
|
9
|
+
get_org_id: Optional[Callable[[], Optional[str]]] = None,
|
|
10
|
+
api_key: Optional[str] = None,
|
|
11
|
+
base_url: Optional[str] = None
|
|
12
|
+
) -> SubscriptionClient:
|
|
13
|
+
|
|
14
|
+
if not get_org_id:
|
|
15
|
+
raise ValueError("get_org_id callback is required")
|
|
16
|
+
|
|
17
|
+
if client is None:
|
|
18
|
+
key = api_key or os.environ.get("SUBSCRIPTION_API_KEY")
|
|
19
|
+
url = base_url or os.environ.get("SUBSCRIPTION_BASE_URL")
|
|
20
|
+
if not key or not url:
|
|
21
|
+
raise ValueError("Subscription API Key and Base URL are required when client is not provided")
|
|
22
|
+
client = SubscriptionClient(key, url)
|
|
23
|
+
client.start()
|
|
24
|
+
|
|
25
|
+
@app.before_request
|
|
26
|
+
def before_req():
|
|
27
|
+
path = request.path
|
|
28
|
+
method = request.method
|
|
29
|
+
|
|
30
|
+
mapping = client.match_route(method, path)
|
|
31
|
+
if not mapping:
|
|
32
|
+
return None # Bypass
|
|
33
|
+
|
|
34
|
+
org_id = get_org_id()
|
|
35
|
+
if not org_id:
|
|
36
|
+
return jsonify({
|
|
37
|
+
"title": "Organization Identity Missing",
|
|
38
|
+
"status": 400,
|
|
39
|
+
"detail": "No organization external identifier resolved for the matching route."
|
|
40
|
+
}), 400
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
check_result = client.check_entitlement(org_id, mapping.id, 1.0)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return jsonify({
|
|
46
|
+
"title": "Entitlement Resolution Error",
|
|
47
|
+
"status": 500,
|
|
48
|
+
"detail": str(e)
|
|
49
|
+
}), 500
|
|
50
|
+
|
|
51
|
+
if not check_result.allowed:
|
|
52
|
+
detail = check_result.detail or f"Subscription limit reached or feature not enabled: {check_result.featureCode}"
|
|
53
|
+
code = check_result.code or "ENTITLEMENT_DENIED"
|
|
54
|
+
return jsonify({
|
|
55
|
+
"title": "Entitlement Denied",
|
|
56
|
+
"status": 403,
|
|
57
|
+
"detail": detail,
|
|
58
|
+
"code": code
|
|
59
|
+
}), 403
|
|
60
|
+
|
|
61
|
+
# Attach reservation context to request state
|
|
62
|
+
g.subscription_reservation_id = check_result.reservationId
|
|
63
|
+
request.subscription_reservation_id = check_result.reservationId
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@app.after_request
|
|
67
|
+
def after_req(response):
|
|
68
|
+
reservation_id = getattr(g, 'subscription_reservation_id', None)
|
|
69
|
+
if reservation_id:
|
|
70
|
+
success = response.status_code < 400
|
|
71
|
+
try:
|
|
72
|
+
if success:
|
|
73
|
+
client.commit_reservation(reservation_id)
|
|
74
|
+
else:
|
|
75
|
+
client.cancel_reservation(reservation_id)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
import sys
|
|
78
|
+
print(
|
|
79
|
+
f"[SubscriptionSDK] Failed to {'commit' if success else 'cancel'} reservation {reservation_id}: {e}",
|
|
80
|
+
file=sys.stderr
|
|
81
|
+
)
|
|
82
|
+
return response
|
|
83
|
+
|
|
84
|
+
return client
|