permitcore 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.
- permitcore-1.0.0/PKG-INFO +277 -0
- permitcore-1.0.0/README.md +250 -0
- permitcore-1.0.0/permitcore/__init__.py +670 -0
- permitcore-1.0.0/permitcore.egg-info/PKG-INFO +277 -0
- permitcore-1.0.0/permitcore.egg-info/SOURCES.txt +9 -0
- permitcore-1.0.0/permitcore.egg-info/dependency_links.txt +1 -0
- permitcore-1.0.0/permitcore.egg-info/requires.txt +7 -0
- permitcore-1.0.0/permitcore.egg-info/top_level.txt +1 -0
- permitcore-1.0.0/pyproject.toml +34 -0
- permitcore-1.0.0/setup.cfg +4 -0
- permitcore-1.0.0/tests/test_vectors.py +148 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: permitcore
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for PermitCore license management
|
|
5
|
+
Author-email: PermitCore <sdk@permitcore.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://permitcore.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/permitCore-spec/PermitCore/tree/main/SDKs/python
|
|
9
|
+
Keywords: license,licensing,sdk,permitcore,activation
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: offline
|
|
23
|
+
Requires-Dist: cryptography>=41; extra == "offline"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
26
|
+
Requires-Dist: cryptography>=41; extra == "test"
|
|
27
|
+
|
|
28
|
+
# PermitCore Python SDK
|
|
29
|
+
|
|
30
|
+
Official Python client for [PermitCore](https://permitcore.dev) license management.
|
|
31
|
+
|
|
32
|
+
**Requirements:** Python 3.8+, zero required dependencies. The optional `cryptography` package
|
|
33
|
+
(`pip install permitcore[offline]`) is only needed for offline license token verification.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install permitcore
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or, for offline license token verification support:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install permitcore[offline]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from permitcore import PermitCoreClient
|
|
55
|
+
|
|
56
|
+
client = PermitCoreClient("https://your-instance.com")
|
|
57
|
+
result = client.validate("PERMIT-XXXX-XXXX-XXXX-XXXX")
|
|
58
|
+
|
|
59
|
+
if result.is_valid:
|
|
60
|
+
print(f"Valid! Product: {result.product_name}")
|
|
61
|
+
if result.has_feature("export"):
|
|
62
|
+
enable_export()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Validate
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
result = client.validate(license_key, version="2.3.1") # version is optional
|
|
71
|
+
|
|
72
|
+
# result.is_valid bool
|
|
73
|
+
# result.product_name Optional[str]
|
|
74
|
+
# result.remaining_activations Optional[int]
|
|
75
|
+
# result.expires_at Optional[str] (ISO 8601)
|
|
76
|
+
# result.features Optional[List[str]]
|
|
77
|
+
# result.custom_fields Optional[Dict[str, str]]
|
|
78
|
+
# result.is_trial bool
|
|
79
|
+
# result.trial_days_remaining Optional[int]
|
|
80
|
+
# result.node_locked bool
|
|
81
|
+
# result.offline_grace_days Optional[int]
|
|
82
|
+
# result.min_version Optional[str]
|
|
83
|
+
# result.max_version Optional[str]
|
|
84
|
+
# result.vendor_warning Optional[str]
|
|
85
|
+
# result.message Optional[str]
|
|
86
|
+
# result.is_offline bool (true when served from local cache)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`validate()` never consumes an activation slot. It falls back to the local disk cache when the
|
|
90
|
+
server is unreachable, as long as the license has `offline_grace_days` configured. Passing
|
|
91
|
+
`version` lets the server enforce `min_version`/`max_version` restrictions on the license.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Activate
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
result = client.activate(
|
|
99
|
+
license_key,
|
|
100
|
+
device_id=None, # auto-generated HWID when omitted
|
|
101
|
+
device_name="Production Server #1",
|
|
102
|
+
version="2.3.1", # optional
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not result.is_valid:
|
|
106
|
+
raise RuntimeError(f"Activation failed: {result.message}")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Call `activate()` **once** per installation. Use `validate()` on every subsequent launch.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Meter (usage events)
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# Record a single API call
|
|
117
|
+
recorded = client.meter(license_key, "api_call")
|
|
118
|
+
|
|
119
|
+
# Record bulk usage with metadata
|
|
120
|
+
recorded = client.meter(license_key, "export", quantity=5, meta={"format": "pdf", "pages": 12})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Returns `True` if the event was recorded on the server, `False` on any failure (network error, or
|
|
124
|
+
the server rejecting the event).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Floating licenses
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Check out a seat at session start
|
|
132
|
+
session = client.checkout(license_key)
|
|
133
|
+
if not session.success:
|
|
134
|
+
raise RuntimeError(f"No seats available: {session.message}")
|
|
135
|
+
|
|
136
|
+
token = session.session_token
|
|
137
|
+
|
|
138
|
+
# Heartbeat every 4-5 minutes to keep the seat alive
|
|
139
|
+
client.heartbeat(token)
|
|
140
|
+
|
|
141
|
+
# Release the seat when done
|
|
142
|
+
client.checkin(token)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Offline license tokens
|
|
148
|
+
|
|
149
|
+
An offline activation token (`pc_offline_v1.<payload>.<signature>`) lets your app verify a
|
|
150
|
+
license with **zero network calls**, using ECDSA P-256 signature verification against your
|
|
151
|
+
tenant's public key (`GET /api/v1/{tenantSlug}/public-key`). Useful for air-gapped or
|
|
152
|
+
intermittently-connected deployments. Requires the optional `cryptography` package
|
|
153
|
+
(`pip install permitcore[offline]`) — a plain `pip install permitcore` install raises
|
|
154
|
+
`OfflineVerificationUnavailable` if you call these without it.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Pure local verification — no network call. Never throws (except OfflineVerificationUnavailable
|
|
158
|
+
# if 'cryptography' isn't installed).
|
|
159
|
+
result = PermitCoreClient.verify_offline_token(token, public_key_base64)
|
|
160
|
+
|
|
161
|
+
if result.is_valid:
|
|
162
|
+
print(f"Valid! Product: {result.payload.product_name}")
|
|
163
|
+
else:
|
|
164
|
+
print(f"Invalid: {result.message}")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Verify + bind to this device + persist locally (call once, e.g. at install time)
|
|
169
|
+
result = PermitCoreClient.activate_offline(token, public_key_base64, device_id)
|
|
170
|
+
|
|
171
|
+
# On every later launch — no token needed, reads the local cache, still no network call
|
|
172
|
+
result = PermitCoreClient.validate_offline(device_id)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# Optional: ask the server to verify the token AND check its revocation status (requires network)
|
|
177
|
+
result = client.verify_offline_online(token)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
All four methods return an `OfflineTokenResult(is_valid, message, payload)`. `payload` (an
|
|
181
|
+
`OfflineTokenPayload`) carries `token_id`, `tenant_slug`, `kid`, `tenant_id`, `license_id`,
|
|
182
|
+
`license_key_hash`, `device_id`, `device_name`, `product_name`, `max_activations`, `issued_at`,
|
|
183
|
+
`expires_at`. `kid` identifies which of the tenant's signing keys produced the token (`None` on
|
|
184
|
+
tokens issued before key versioning existed) — informational only, `verify_offline_token()` still
|
|
185
|
+
verifies against whatever `public_key_base64` you pass it. `verify_offline_token()` and
|
|
186
|
+
`validate_offline()` never throw for malformed,
|
|
187
|
+
tampered, expired, or missing input — they just return `is_valid=False` with a descriptive
|
|
188
|
+
`message`.
|
|
189
|
+
|
|
190
|
+
`activate_offline()`'s local cache is stored under the user's home directory as
|
|
191
|
+
`.permitcore_offline_<hash>` (same convention as the `validate()`/`activate()` cache, keyed by
|
|
192
|
+
device ID instead of license key).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Version enforcement
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
result = client.validate(license_key)
|
|
200
|
+
|
|
201
|
+
my_version = "2.3.0"
|
|
202
|
+
if result.min_version and my_version < result.min_version:
|
|
203
|
+
raise RuntimeError(f"Please update to version {result.min_version} or newer.")
|
|
204
|
+
if result.max_version and my_version > result.max_version:
|
|
205
|
+
raise RuntimeError(f"This build ({my_version}) is not licensed for versions above {result.max_version}.")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Pass `version=my_version` to `validate()`/`activate()` to also have the *server* enforce this —
|
|
209
|
+
otherwise only client-side comparison happens.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Offline grace pattern
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
result = client.validate(license_key) # falls back to cache automatically
|
|
217
|
+
|
|
218
|
+
if not result.is_valid:
|
|
219
|
+
raise RuntimeError(f"License invalid: {result.message}")
|
|
220
|
+
|
|
221
|
+
if result.is_offline:
|
|
222
|
+
# Server unreachable — running on cached result
|
|
223
|
+
show_notice("Running in offline mode. Connect to the internet to refresh your license.")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The cache is stored under the user's home directory as `.permitcore_cache_<hash>`. It expires
|
|
227
|
+
after `offline_grace_days` days.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Constructor options
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
client = PermitCoreClient(
|
|
235
|
+
base_url="https://your-instance.com",
|
|
236
|
+
enable_offline_cache=True, # default — set False to always require network
|
|
237
|
+
timeout=5, # HTTP timeout in seconds
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## LicenseResult reference
|
|
244
|
+
|
|
245
|
+
| Field | Type | Description |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| `is_valid` | `bool` | True if the license is active and valid |
|
|
248
|
+
| `product_name` | `Optional[str]` | Product the license belongs to |
|
|
249
|
+
| `remaining_activations` | `Optional[int]` | Slots left before MaxActivations is reached |
|
|
250
|
+
| `expires_at` | `Optional[str]` | Expiry date (ISO 8601 UTC), None if perpetual |
|
|
251
|
+
| `features` | `Optional[List[str]]` | Feature flag list, e.g. `["export", "api"]` |
|
|
252
|
+
| `custom_fields` | `Optional[Dict[str, str]]` | Arbitrary key/value metadata set on the license |
|
|
253
|
+
| `is_trial` | `bool` | True for trial licenses |
|
|
254
|
+
| `trial_days_remaining` | `Optional[int]` | Days until trial expires |
|
|
255
|
+
| `node_locked` | `bool` | True if bound to a specific device |
|
|
256
|
+
| `offline_grace_days` | `Optional[int]` | How many days the cache is valid |
|
|
257
|
+
| `min_version` / `max_version` | `Optional[str]` | Version enforcement bounds |
|
|
258
|
+
| `vendor_warning` | `Optional[str]` | Non-fatal message from the vendor |
|
|
259
|
+
| `message` | `Optional[str]` | Reason when `is_valid = False` |
|
|
260
|
+
| `is_offline` | `bool` | True when result came from local cache |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Development
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pip install -e ".[test]" # installs pytest + cryptography for the test run
|
|
268
|
+
pytest
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`tests/test_vectors.py` runs this SDK's `verify_offline_token`/`verify_grace_cache_token` against
|
|
272
|
+
the shared, language-agnostic cross-SDK protocol vectors in `../../test-vectors/vectors.json`
|
|
273
|
+
(fixed ECDSA P-256/SHA-256 tokens every PermitCore SDK verifies identically — see that file's own
|
|
274
|
+
`schemaNote`) and checks the `validate`/`activate` request bodies this SDK builds match the shared
|
|
275
|
+
`requestShapes` key sets exactly.
|
|
276
|
+
|
|
277
|
+
`has_feature(feature: str) -> bool` — case-insensitive feature check.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# PermitCore Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for [PermitCore](https://permitcore.dev) license management.
|
|
4
|
+
|
|
5
|
+
**Requirements:** Python 3.8+, zero required dependencies. The optional `cryptography` package
|
|
6
|
+
(`pip install permitcore[offline]`) is only needed for offline license token verification.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install permitcore
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or, for offline license token verification support:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install permitcore[offline]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from permitcore import PermitCoreClient
|
|
28
|
+
|
|
29
|
+
client = PermitCoreClient("https://your-instance.com")
|
|
30
|
+
result = client.validate("PERMIT-XXXX-XXXX-XXXX-XXXX")
|
|
31
|
+
|
|
32
|
+
if result.is_valid:
|
|
33
|
+
print(f"Valid! Product: {result.product_name}")
|
|
34
|
+
if result.has_feature("export"):
|
|
35
|
+
enable_export()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Validate
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
result = client.validate(license_key, version="2.3.1") # version is optional
|
|
44
|
+
|
|
45
|
+
# result.is_valid bool
|
|
46
|
+
# result.product_name Optional[str]
|
|
47
|
+
# result.remaining_activations Optional[int]
|
|
48
|
+
# result.expires_at Optional[str] (ISO 8601)
|
|
49
|
+
# result.features Optional[List[str]]
|
|
50
|
+
# result.custom_fields Optional[Dict[str, str]]
|
|
51
|
+
# result.is_trial bool
|
|
52
|
+
# result.trial_days_remaining Optional[int]
|
|
53
|
+
# result.node_locked bool
|
|
54
|
+
# result.offline_grace_days Optional[int]
|
|
55
|
+
# result.min_version Optional[str]
|
|
56
|
+
# result.max_version Optional[str]
|
|
57
|
+
# result.vendor_warning Optional[str]
|
|
58
|
+
# result.message Optional[str]
|
|
59
|
+
# result.is_offline bool (true when served from local cache)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`validate()` never consumes an activation slot. It falls back to the local disk cache when the
|
|
63
|
+
server is unreachable, as long as the license has `offline_grace_days` configured. Passing
|
|
64
|
+
`version` lets the server enforce `min_version`/`max_version` restrictions on the license.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Activate
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
result = client.activate(
|
|
72
|
+
license_key,
|
|
73
|
+
device_id=None, # auto-generated HWID when omitted
|
|
74
|
+
device_name="Production Server #1",
|
|
75
|
+
version="2.3.1", # optional
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not result.is_valid:
|
|
79
|
+
raise RuntimeError(f"Activation failed: {result.message}")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Call `activate()` **once** per installation. Use `validate()` on every subsequent launch.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Meter (usage events)
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Record a single API call
|
|
90
|
+
recorded = client.meter(license_key, "api_call")
|
|
91
|
+
|
|
92
|
+
# Record bulk usage with metadata
|
|
93
|
+
recorded = client.meter(license_key, "export", quantity=5, meta={"format": "pdf", "pages": 12})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Returns `True` if the event was recorded on the server, `False` on any failure (network error, or
|
|
97
|
+
the server rejecting the event).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Floating licenses
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# Check out a seat at session start
|
|
105
|
+
session = client.checkout(license_key)
|
|
106
|
+
if not session.success:
|
|
107
|
+
raise RuntimeError(f"No seats available: {session.message}")
|
|
108
|
+
|
|
109
|
+
token = session.session_token
|
|
110
|
+
|
|
111
|
+
# Heartbeat every 4-5 minutes to keep the seat alive
|
|
112
|
+
client.heartbeat(token)
|
|
113
|
+
|
|
114
|
+
# Release the seat when done
|
|
115
|
+
client.checkin(token)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Offline license tokens
|
|
121
|
+
|
|
122
|
+
An offline activation token (`pc_offline_v1.<payload>.<signature>`) lets your app verify a
|
|
123
|
+
license with **zero network calls**, using ECDSA P-256 signature verification against your
|
|
124
|
+
tenant's public key (`GET /api/v1/{tenantSlug}/public-key`). Useful for air-gapped or
|
|
125
|
+
intermittently-connected deployments. Requires the optional `cryptography` package
|
|
126
|
+
(`pip install permitcore[offline]`) — a plain `pip install permitcore` install raises
|
|
127
|
+
`OfflineVerificationUnavailable` if you call these without it.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# Pure local verification — no network call. Never throws (except OfflineVerificationUnavailable
|
|
131
|
+
# if 'cryptography' isn't installed).
|
|
132
|
+
result = PermitCoreClient.verify_offline_token(token, public_key_base64)
|
|
133
|
+
|
|
134
|
+
if result.is_valid:
|
|
135
|
+
print(f"Valid! Product: {result.payload.product_name}")
|
|
136
|
+
else:
|
|
137
|
+
print(f"Invalid: {result.message}")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# Verify + bind to this device + persist locally (call once, e.g. at install time)
|
|
142
|
+
result = PermitCoreClient.activate_offline(token, public_key_base64, device_id)
|
|
143
|
+
|
|
144
|
+
# On every later launch — no token needed, reads the local cache, still no network call
|
|
145
|
+
result = PermitCoreClient.validate_offline(device_id)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# Optional: ask the server to verify the token AND check its revocation status (requires network)
|
|
150
|
+
result = client.verify_offline_online(token)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
All four methods return an `OfflineTokenResult(is_valid, message, payload)`. `payload` (an
|
|
154
|
+
`OfflineTokenPayload`) carries `token_id`, `tenant_slug`, `kid`, `tenant_id`, `license_id`,
|
|
155
|
+
`license_key_hash`, `device_id`, `device_name`, `product_name`, `max_activations`, `issued_at`,
|
|
156
|
+
`expires_at`. `kid` identifies which of the tenant's signing keys produced the token (`None` on
|
|
157
|
+
tokens issued before key versioning existed) — informational only, `verify_offline_token()` still
|
|
158
|
+
verifies against whatever `public_key_base64` you pass it. `verify_offline_token()` and
|
|
159
|
+
`validate_offline()` never throw for malformed,
|
|
160
|
+
tampered, expired, or missing input — they just return `is_valid=False` with a descriptive
|
|
161
|
+
`message`.
|
|
162
|
+
|
|
163
|
+
`activate_offline()`'s local cache is stored under the user's home directory as
|
|
164
|
+
`.permitcore_offline_<hash>` (same convention as the `validate()`/`activate()` cache, keyed by
|
|
165
|
+
device ID instead of license key).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Version enforcement
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
result = client.validate(license_key)
|
|
173
|
+
|
|
174
|
+
my_version = "2.3.0"
|
|
175
|
+
if result.min_version and my_version < result.min_version:
|
|
176
|
+
raise RuntimeError(f"Please update to version {result.min_version} or newer.")
|
|
177
|
+
if result.max_version and my_version > result.max_version:
|
|
178
|
+
raise RuntimeError(f"This build ({my_version}) is not licensed for versions above {result.max_version}.")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Pass `version=my_version` to `validate()`/`activate()` to also have the *server* enforce this —
|
|
182
|
+
otherwise only client-side comparison happens.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Offline grace pattern
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
result = client.validate(license_key) # falls back to cache automatically
|
|
190
|
+
|
|
191
|
+
if not result.is_valid:
|
|
192
|
+
raise RuntimeError(f"License invalid: {result.message}")
|
|
193
|
+
|
|
194
|
+
if result.is_offline:
|
|
195
|
+
# Server unreachable — running on cached result
|
|
196
|
+
show_notice("Running in offline mode. Connect to the internet to refresh your license.")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The cache is stored under the user's home directory as `.permitcore_cache_<hash>`. It expires
|
|
200
|
+
after `offline_grace_days` days.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Constructor options
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
client = PermitCoreClient(
|
|
208
|
+
base_url="https://your-instance.com",
|
|
209
|
+
enable_offline_cache=True, # default — set False to always require network
|
|
210
|
+
timeout=5, # HTTP timeout in seconds
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## LicenseResult reference
|
|
217
|
+
|
|
218
|
+
| Field | Type | Description |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `is_valid` | `bool` | True if the license is active and valid |
|
|
221
|
+
| `product_name` | `Optional[str]` | Product the license belongs to |
|
|
222
|
+
| `remaining_activations` | `Optional[int]` | Slots left before MaxActivations is reached |
|
|
223
|
+
| `expires_at` | `Optional[str]` | Expiry date (ISO 8601 UTC), None if perpetual |
|
|
224
|
+
| `features` | `Optional[List[str]]` | Feature flag list, e.g. `["export", "api"]` |
|
|
225
|
+
| `custom_fields` | `Optional[Dict[str, str]]` | Arbitrary key/value metadata set on the license |
|
|
226
|
+
| `is_trial` | `bool` | True for trial licenses |
|
|
227
|
+
| `trial_days_remaining` | `Optional[int]` | Days until trial expires |
|
|
228
|
+
| `node_locked` | `bool` | True if bound to a specific device |
|
|
229
|
+
| `offline_grace_days` | `Optional[int]` | How many days the cache is valid |
|
|
230
|
+
| `min_version` / `max_version` | `Optional[str]` | Version enforcement bounds |
|
|
231
|
+
| `vendor_warning` | `Optional[str]` | Non-fatal message from the vendor |
|
|
232
|
+
| `message` | `Optional[str]` | Reason when `is_valid = False` |
|
|
233
|
+
| `is_offline` | `bool` | True when result came from local cache |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
pip install -e ".[test]" # installs pytest + cryptography for the test run
|
|
241
|
+
pytest
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`tests/test_vectors.py` runs this SDK's `verify_offline_token`/`verify_grace_cache_token` against
|
|
245
|
+
the shared, language-agnostic cross-SDK protocol vectors in `../../test-vectors/vectors.json`
|
|
246
|
+
(fixed ECDSA P-256/SHA-256 tokens every PermitCore SDK verifies identically — see that file's own
|
|
247
|
+
`schemaNote`) and checks the `validate`/`activate` request bodies this SDK builds match the shared
|
|
248
|
+
`requestShapes` key sets exactly.
|
|
249
|
+
|
|
250
|
+
`has_feature(feature: str) -> bool` — case-insensitive feature check.
|