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.
@@ -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.