keymint 2.4.0__tar.gz → 2.6.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.
- {keymint-2.4.0/keymint.egg-info → keymint-2.6.0}/PKG-INFO +28 -8
- {keymint-2.4.0 → keymint-2.6.0}/README.md +25 -1
- {keymint-2.4.0 → keymint-2.6.0}/keymint/__init__.py +16 -7
- {keymint-2.4.0 → keymint-2.6.0}/keymint/_version.py +1 -1
- {keymint-2.4.0 → keymint-2.6.0}/keymint/types.py +16 -5
- {keymint-2.4.0 → keymint-2.6.0/keymint.egg-info}/PKG-INFO +28 -8
- {keymint-2.4.0 → keymint-2.6.0}/keymint.egg-info/SOURCES.txt +2 -1
- {keymint-2.4.0 → keymint-2.6.0}/keymint.egg-info/requires.txt +1 -1
- {keymint-2.4.0 → keymint-2.6.0}/setup.py +3 -7
- keymint-2.6.0/tests/test_contracts.py +85 -0
- {keymint-2.4.0 → keymint-2.6.0}/LICENSE +0 -0
- {keymint-2.4.0 → keymint-2.6.0}/MANIFEST.in +0 -0
- {keymint-2.4.0 → keymint-2.6.0}/keymint/identity.py +0 -0
- {keymint-2.4.0 → keymint-2.6.0}/keymint.egg-info/dependency_links.txt +0 -0
- {keymint-2.4.0 → keymint-2.6.0}/keymint.egg-info/top_level.txt +0 -0
- {keymint-2.4.0 → keymint-2.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keymint
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: License key validation, activation, and management for Python. Supports node-locking, offline licensing, and hardware fingerprinting.
|
|
5
5
|
Home-page: https://github.com/keymint-dev/keymint-python
|
|
6
6
|
Author: KeyMint
|
|
@@ -16,18 +16,14 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
16
16
|
Classifier: Topic :: System :: Software Distribution
|
|
17
17
|
Classifier: License :: OSI Approved :: MIT License
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
23
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
26
22
|
Classifier: Operating System :: OS Independent
|
|
27
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
28
24
|
Description-Content-Type: text/markdown
|
|
29
25
|
License-File: LICENSE
|
|
30
|
-
Requires-Dist: requests>=2.
|
|
26
|
+
Requires-Dist: requests>=2.32.4
|
|
31
27
|
Provides-Extra: dev
|
|
32
28
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
33
29
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -91,6 +87,30 @@ if result and 'key' in result:
|
|
|
91
87
|
print(f"Created Key: {result['key']}")
|
|
92
88
|
```
|
|
93
89
|
|
|
90
|
+
## License Expiry
|
|
91
|
+
|
|
92
|
+
`activate_key` and `floating_checkout` return `expirationDate`: the license expiry as an ISO 8601 string (e.g. `"2027-03-01T00:00:00.000Z"`), or `None` when the license never expires. On `floating_checkout`, `expiresAt` is the seat lease, not the license expiry.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from datetime import datetime, timezone
|
|
96
|
+
import math
|
|
97
|
+
|
|
98
|
+
result = sdk.activate_key({'productId': product_id, 'licenseKey': license_key, 'hostId': host_id})
|
|
99
|
+
|
|
100
|
+
expiration = result.get('expirationDate')
|
|
101
|
+
if expiration is None:
|
|
102
|
+
print('License never expires')
|
|
103
|
+
else:
|
|
104
|
+
expires_at = datetime.fromisoformat(expiration.replace('Z', '+00:00'))
|
|
105
|
+
seconds_left = (expires_at - datetime.now(timezone.utc)).total_seconds()
|
|
106
|
+
days_left = max(0, math.ceil(seconds_left / 86400))
|
|
107
|
+
print(f'License expires {expires_at:%Y-%m-%d} ({days_left} days left)')
|
|
108
|
+
|
|
109
|
+
# Store the last value you received (e.g. in your app's settings file).
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Store the last `expirationDate` you received. An expired license fails activation with the same generic invalid-key error as a wrong key, so the stored date is how your app can tell the user the license expired on that date instead of calling the key invalid.
|
|
113
|
+
|
|
94
114
|
## Machine Identity
|
|
95
115
|
Keymint provides utilities to uniquely identify machines for node-locking:
|
|
96
116
|
|
|
@@ -110,7 +130,7 @@ Keymint provides utilities to uniquely identify machines for node-locking:
|
|
|
110
130
|
| `update_key` | Updates an existing license key. |
|
|
111
131
|
| `block_key` | Blocks a license key. |
|
|
112
132
|
| `unblock_key` | Unblocks a previously blocked license key. |
|
|
113
|
-
| `sign_key` | Signs a key for offline
|
|
133
|
+
| `sign_key` | Signs a key for offline validation using an admin API key.|
|
|
114
134
|
| `floating_checkout` | Checks out a floating license seat. |
|
|
115
135
|
| `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
|
|
116
136
|
| `floating_checkin` | Checks in a session, releasing the seat. |
|
|
@@ -42,6 +42,30 @@ if result and 'key' in result:
|
|
|
42
42
|
print(f"Created Key: {result['key']}")
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## License Expiry
|
|
46
|
+
|
|
47
|
+
`activate_key` and `floating_checkout` return `expirationDate`: the license expiry as an ISO 8601 string (e.g. `"2027-03-01T00:00:00.000Z"`), or `None` when the license never expires. On `floating_checkout`, `expiresAt` is the seat lease, not the license expiry.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from datetime import datetime, timezone
|
|
51
|
+
import math
|
|
52
|
+
|
|
53
|
+
result = sdk.activate_key({'productId': product_id, 'licenseKey': license_key, 'hostId': host_id})
|
|
54
|
+
|
|
55
|
+
expiration = result.get('expirationDate')
|
|
56
|
+
if expiration is None:
|
|
57
|
+
print('License never expires')
|
|
58
|
+
else:
|
|
59
|
+
expires_at = datetime.fromisoformat(expiration.replace('Z', '+00:00'))
|
|
60
|
+
seconds_left = (expires_at - datetime.now(timezone.utc)).total_seconds()
|
|
61
|
+
days_left = max(0, math.ceil(seconds_left / 86400))
|
|
62
|
+
print(f'License expires {expires_at:%Y-%m-%d} ({days_left} days left)')
|
|
63
|
+
|
|
64
|
+
# Store the last value you received (e.g. in your app's settings file).
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Store the last `expirationDate` you received. An expired license fails activation with the same generic invalid-key error as a wrong key, so the stored date is how your app can tell the user the license expired on that date instead of calling the key invalid.
|
|
68
|
+
|
|
45
69
|
## Machine Identity
|
|
46
70
|
Keymint provides utilities to uniquely identify machines for node-locking:
|
|
47
71
|
|
|
@@ -61,7 +85,7 @@ Keymint provides utilities to uniquely identify machines for node-locking:
|
|
|
61
85
|
| `update_key` | Updates an existing license key. |
|
|
62
86
|
| `block_key` | Blocks a license key. |
|
|
63
87
|
| `unblock_key` | Unblocks a previously blocked license key. |
|
|
64
|
-
| `sign_key` | Signs a key for offline
|
|
88
|
+
| `sign_key` | Signs a key for offline validation using an admin API key.|
|
|
65
89
|
| `floating_checkout` | Checks out a floating license seat. |
|
|
66
90
|
| `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
|
|
67
91
|
| `floating_checkin` | Checks in a session, releasing the seat. |
|
|
@@ -17,11 +17,13 @@ class KeyMint:
|
|
|
17
17
|
'Content-Type': 'application/json'
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
def _handle_request(self, method: str, endpoint: str, params: dict | None = None, query_params: dict | None = None, idempotency_key: str | None = None):
|
|
20
|
+
def _handle_request(self, method: str, endpoint: str, params: dict | None = None, query_params: dict | None = None, idempotency_key: str | None = None, extra_headers: dict | None = None):
|
|
21
21
|
url = f'{self.base_url}{endpoint}'
|
|
22
22
|
headers = self.headers.copy()
|
|
23
23
|
if idempotency_key:
|
|
24
24
|
headers['Idempotency-Key'] = idempotency_key
|
|
25
|
+
if extra_headers:
|
|
26
|
+
headers.update(extra_headers)
|
|
25
27
|
|
|
26
28
|
try:
|
|
27
29
|
if method.upper() == 'GET':
|
|
@@ -42,8 +44,9 @@ class KeyMint:
|
|
|
42
44
|
except requests.exceptions.HTTPError as http_err:
|
|
43
45
|
try:
|
|
44
46
|
error_data = http_err.response.json()
|
|
47
|
+
nested_error = error_data.get('error') if isinstance(error_data.get('error'), dict) else {}
|
|
45
48
|
raise KeyMintApiError(
|
|
46
|
-
message=error_data.get('message'
|
|
49
|
+
message=error_data.get('message') or nested_error.get('message') or 'An API error occurred',
|
|
47
50
|
code=error_data.get('code', -1),
|
|
48
51
|
status=http_err.response.status_code
|
|
49
52
|
)
|
|
@@ -76,7 +79,10 @@ class KeyMint:
|
|
|
76
79
|
|
|
77
80
|
:param params: Activation parameters including productId, licenseKey, and optional hostId.
|
|
78
81
|
:param idempotency_key: Optional unique identifier to ensure request idempotency.
|
|
79
|
-
:return: Activation success message and licensee information.
|
|
82
|
+
:return: Activation success message and licensee information. Includes
|
|
83
|
+
``expirationDate``: the license expiry as an ISO 8601 string, or ``None``
|
|
84
|
+
when the license never expires. An expired license fails activation with
|
|
85
|
+
the same generic invalid-key error, so store the last value you received.
|
|
80
86
|
"""
|
|
81
87
|
return self._handle_request('POST', '/key/activate', params, idempotency_key=idempotency_key)
|
|
82
88
|
|
|
@@ -95,6 +101,8 @@ class KeyMint:
|
|
|
95
101
|
:param params: Parameters for checking out the license.
|
|
96
102
|
:param idempotency_key: Optional unique identifier to ensure request idempotency.
|
|
97
103
|
:returns: The checkout response containing sessionId and sessionSecret.
|
|
104
|
+
``expiresAt`` is the seat lease; ``expirationDate`` is the license expiry
|
|
105
|
+
as an ISO 8601 string, or ``None`` when the license never expires.
|
|
98
106
|
"""
|
|
99
107
|
return self._handle_request('POST', '/key/checkout', params, idempotency_key=idempotency_key)
|
|
100
108
|
|
|
@@ -123,10 +131,12 @@ class KeyMint:
|
|
|
123
131
|
:returns: The license key details.
|
|
124
132
|
"""
|
|
125
133
|
query_params = {
|
|
126
|
-
'productId': params['productId']
|
|
127
|
-
'licenseKey': params['licenseKey']
|
|
134
|
+
'productId': params['productId']
|
|
128
135
|
}
|
|
129
|
-
return self._handle_request(
|
|
136
|
+
return self._handle_request(
|
|
137
|
+
'GET', '/key', query_params=query_params,
|
|
138
|
+
extra_headers={'x-license-key': params['licenseKey']}
|
|
139
|
+
)
|
|
130
140
|
|
|
131
141
|
def block_key(self, params: BlockKeyParams, idempotency_key: str | None = None) -> BlockKeyResponse:
|
|
132
142
|
"""
|
|
@@ -288,4 +298,3 @@ class KeyMint:
|
|
|
288
298
|
except Exception:
|
|
289
299
|
return False
|
|
290
300
|
|
|
291
|
-
|
|
@@ -30,9 +30,10 @@ class CreateKeyParams(TypedDict):
|
|
|
30
30
|
heartbeatInterval: int | None
|
|
31
31
|
sessionLeaseDuration: int | None
|
|
32
32
|
|
|
33
|
-
class CreateKeyResponse(TypedDict):
|
|
33
|
+
class CreateKeyResponse(TypedDict, total=False):
|
|
34
34
|
code: int
|
|
35
35
|
key: str
|
|
36
|
+
keys: list[str]
|
|
36
37
|
|
|
37
38
|
class KeyMintApiError(Exception):
|
|
38
39
|
def __init__(self, message: str, code: int, status: int | None = None):
|
|
@@ -58,6 +59,9 @@ class ActivateKeyResponse(TypedDict):
|
|
|
58
59
|
versionId: str | None
|
|
59
60
|
version: dict[str, Any] | None
|
|
60
61
|
allowedHosts: list[str] | None
|
|
62
|
+
# License expiry as an ISO 8601 string (e.g. "2027-03-01T00:00:00.000Z"),
|
|
63
|
+
# or None when the license never expires.
|
|
64
|
+
expirationDate: str | None
|
|
61
65
|
|
|
62
66
|
class DeactivateKeyParams(TypedDict):
|
|
63
67
|
productId: str
|
|
@@ -67,6 +71,7 @@ class DeactivateKeyParams(TypedDict):
|
|
|
67
71
|
class DeactivateKeyResponse(TypedDict):
|
|
68
72
|
message: str
|
|
69
73
|
code: int
|
|
74
|
+
devicesRemoved: int
|
|
70
75
|
|
|
71
76
|
class DeviceDetails(TypedDict):
|
|
72
77
|
hostId: str
|
|
@@ -179,11 +184,13 @@ class DeleteCustomerResponse(TypedDict):
|
|
|
179
184
|
class ToggleCustomerStatusParams(TypedDict):
|
|
180
185
|
customerId: str
|
|
181
186
|
|
|
182
|
-
class ToggleCustomerStatusResponse(TypedDict):
|
|
187
|
+
class ToggleCustomerStatusResponse(TypedDict, total=False):
|
|
183
188
|
action: str
|
|
184
189
|
status: bool
|
|
185
190
|
message: str
|
|
186
191
|
code: int
|
|
192
|
+
customerName: str
|
|
193
|
+
active: bool
|
|
187
194
|
|
|
188
195
|
class CustomerLicenseKey(TypedDict):
|
|
189
196
|
id: str
|
|
@@ -210,6 +217,8 @@ class FloatingCheckoutParams(TypedDict):
|
|
|
210
217
|
deviceTag: str | None
|
|
211
218
|
userIdentifier: str | None
|
|
212
219
|
apiKey: str | None
|
|
220
|
+
timestamp: Any | None
|
|
221
|
+
signature: str | None
|
|
213
222
|
|
|
214
223
|
class FloatingCheckoutResponse(TypedDict):
|
|
215
224
|
code: int
|
|
@@ -217,9 +226,12 @@ class FloatingCheckoutResponse(TypedDict):
|
|
|
217
226
|
sessionId: str
|
|
218
227
|
sessionSecret: str
|
|
219
228
|
nextNonce: str
|
|
220
|
-
expiresAt: str
|
|
229
|
+
expiresAt: str # Floating seat lease expiry (not the license expiry)
|
|
221
230
|
heartbeatInterval: int
|
|
222
231
|
metadata: dict[str, Any] | None
|
|
232
|
+
# License expiry as an ISO 8601 string, or None when the license never
|
|
233
|
+
# expires. Separate from ``expiresAt``, which is the seat lease.
|
|
234
|
+
expirationDate: str | None
|
|
223
235
|
currentSessions: int | None
|
|
224
236
|
maxSessions: int | None
|
|
225
237
|
licenseeName: str | None
|
|
@@ -278,5 +290,4 @@ class SignKeyParams(TypedDict):
|
|
|
278
290
|
ttl: int | None
|
|
279
291
|
|
|
280
292
|
class SignKeyResponse(TypedDict):
|
|
281
|
-
|
|
282
|
-
file: dict[str, Any] # { signedKey, keyId, publicKeyFingerprint }
|
|
293
|
+
file: str # Serialized signed license file returned by the API
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keymint
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: License key validation, activation, and management for Python. Supports node-locking, offline licensing, and hardware fingerprinting.
|
|
5
5
|
Home-page: https://github.com/keymint-dev/keymint-python
|
|
6
6
|
Author: KeyMint
|
|
@@ -16,18 +16,14 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
16
16
|
Classifier: Topic :: System :: Software Distribution
|
|
17
17
|
Classifier: License :: OSI Approved :: MIT License
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
23
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
26
22
|
Classifier: Operating System :: OS Independent
|
|
27
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
28
24
|
Description-Content-Type: text/markdown
|
|
29
25
|
License-File: LICENSE
|
|
30
|
-
Requires-Dist: requests>=2.
|
|
26
|
+
Requires-Dist: requests>=2.32.4
|
|
31
27
|
Provides-Extra: dev
|
|
32
28
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
33
29
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -91,6 +87,30 @@ if result and 'key' in result:
|
|
|
91
87
|
print(f"Created Key: {result['key']}")
|
|
92
88
|
```
|
|
93
89
|
|
|
90
|
+
## License Expiry
|
|
91
|
+
|
|
92
|
+
`activate_key` and `floating_checkout` return `expirationDate`: the license expiry as an ISO 8601 string (e.g. `"2027-03-01T00:00:00.000Z"`), or `None` when the license never expires. On `floating_checkout`, `expiresAt` is the seat lease, not the license expiry.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from datetime import datetime, timezone
|
|
96
|
+
import math
|
|
97
|
+
|
|
98
|
+
result = sdk.activate_key({'productId': product_id, 'licenseKey': license_key, 'hostId': host_id})
|
|
99
|
+
|
|
100
|
+
expiration = result.get('expirationDate')
|
|
101
|
+
if expiration is None:
|
|
102
|
+
print('License never expires')
|
|
103
|
+
else:
|
|
104
|
+
expires_at = datetime.fromisoformat(expiration.replace('Z', '+00:00'))
|
|
105
|
+
seconds_left = (expires_at - datetime.now(timezone.utc)).total_seconds()
|
|
106
|
+
days_left = max(0, math.ceil(seconds_left / 86400))
|
|
107
|
+
print(f'License expires {expires_at:%Y-%m-%d} ({days_left} days left)')
|
|
108
|
+
|
|
109
|
+
# Store the last value you received (e.g. in your app's settings file).
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Store the last `expirationDate` you received. An expired license fails activation with the same generic invalid-key error as a wrong key, so the stored date is how your app can tell the user the license expired on that date instead of calling the key invalid.
|
|
113
|
+
|
|
94
114
|
## Machine Identity
|
|
95
115
|
Keymint provides utilities to uniquely identify machines for node-locking:
|
|
96
116
|
|
|
@@ -110,7 +130,7 @@ Keymint provides utilities to uniquely identify machines for node-locking:
|
|
|
110
130
|
| `update_key` | Updates an existing license key. |
|
|
111
131
|
| `block_key` | Blocks a license key. |
|
|
112
132
|
| `unblock_key` | Unblocks a previously blocked license key. |
|
|
113
|
-
| `sign_key` | Signs a key for offline
|
|
133
|
+
| `sign_key` | Signs a key for offline validation using an admin API key.|
|
|
114
134
|
| `floating_checkout` | Checks out a floating license seat. |
|
|
115
135
|
| `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
|
|
116
136
|
| `floating_checkin` | Checks in a session, releasing the seat. |
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="keymint",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.6.0",
|
|
9
9
|
author="KeyMint",
|
|
10
10
|
author_email="cliff@keymint.dev",
|
|
11
11
|
description="License key validation, activation, and management for Python. Supports node-locking, offline licensing, and hardware fingerprinting.",
|
|
@@ -26,19 +26,15 @@ setup(
|
|
|
26
26
|
"Topic :: System :: Software Distribution",
|
|
27
27
|
"License :: OSI Approved :: MIT License",
|
|
28
28
|
"Programming Language :: Python :: 3",
|
|
29
|
-
"Programming Language :: Python :: 3.6",
|
|
30
|
-
"Programming Language :: Python :: 3.7",
|
|
31
|
-
"Programming Language :: Python :: 3.8",
|
|
32
|
-
"Programming Language :: Python :: 3.9",
|
|
33
29
|
"Programming Language :: Python :: 3.10",
|
|
34
30
|
"Programming Language :: Python :: 3.11",
|
|
35
31
|
"Programming Language :: Python :: 3.12",
|
|
36
32
|
"Operating System :: OS Independent",
|
|
37
33
|
],
|
|
38
34
|
keywords="license-key validation activation node-lock hardware-id copy-protection product-key drm offline-licensing keymint license licensing api",
|
|
39
|
-
python_requires='>=3.
|
|
35
|
+
python_requires='>=3.10',
|
|
40
36
|
install_requires=[
|
|
41
|
-
'requests>=2.
|
|
37
|
+
'requests>=2.32.4',
|
|
42
38
|
],
|
|
43
39
|
extras_require={
|
|
44
40
|
'dev': [
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from unittest.mock import Mock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from keymint import KeyMint, KeyMintApiError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def response(payload, status=200):
|
|
10
|
+
result = Mock()
|
|
11
|
+
result.json.return_value = payload
|
|
12
|
+
result.status_code = status
|
|
13
|
+
if status >= 400:
|
|
14
|
+
error = requests.exceptions.HTTPError()
|
|
15
|
+
error.response = result
|
|
16
|
+
result.raise_for_status.side_effect = error
|
|
17
|
+
return result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@patch("keymint.requests.get")
|
|
21
|
+
def test_get_key_uses_header_not_query(mock_get):
|
|
22
|
+
mock_get.return_value = response({"code": 0, "data": {"license": {"productId": "product 123"}}})
|
|
23
|
+
|
|
24
|
+
KeyMint("readonly_test").get_key({"productId": "product 123", "licenseKey": "secret/license+key"})
|
|
25
|
+
|
|
26
|
+
_, kwargs = mock_get.call_args
|
|
27
|
+
assert kwargs["params"] == {"productId": "product 123"}
|
|
28
|
+
assert kwargs["headers"]["x-license-key"] == "secret/license+key"
|
|
29
|
+
assert "licenseKey" not in kwargs["params"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@patch("keymint.requests.post")
|
|
33
|
+
def test_sign_key_returns_serialized_file(mock_post):
|
|
34
|
+
mock_post.return_value = response({"file": '{"signedKey":"signature","keyId":"key_123"}'})
|
|
35
|
+
|
|
36
|
+
result = KeyMint("admin_test").sign_key({
|
|
37
|
+
"productId": "product_123", "licenseKey": "license_123", "hostId": "host_123"
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
assert isinstance(result["file"], str)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@patch("keymint.requests.post")
|
|
44
|
+
def test_nested_error_preserves_server_message(mock_post):
|
|
45
|
+
mock_post.return_value = response({
|
|
46
|
+
"success": False,
|
|
47
|
+
"error": {"code": "CUSTOMER_EMAIL_EXISTS", "message": "Customer email already exists in team"},
|
|
48
|
+
"code": 1,
|
|
49
|
+
}, 409)
|
|
50
|
+
|
|
51
|
+
with pytest.raises(KeyMintApiError, match="Customer email already exists in team"):
|
|
52
|
+
KeyMint("admin_test").create_key({"productId": "product_123"})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("expiration_date", ["2027-03-01T00:00:00.000Z", None])
|
|
56
|
+
@patch("keymint.requests.post")
|
|
57
|
+
def test_activate_key_returns_expiration_date(mock_post, expiration_date):
|
|
58
|
+
mock_post.return_value = response({
|
|
59
|
+
"code": 0, "message": "License valid", "expirationDate": expiration_date,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
result = KeyMint("client_test").activate_key({"productId": "product_123", "licenseKey": "license_123"})
|
|
63
|
+
|
|
64
|
+
assert mock_post.call_args.args[0].endswith("/key/activate")
|
|
65
|
+
assert "expirationDate" in result
|
|
66
|
+
assert result["expirationDate"] == expiration_date
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize("expiration_date", ["2027-03-01T00:00:00.000Z", None])
|
|
70
|
+
@patch("keymint.requests.post")
|
|
71
|
+
def test_floating_checkout_returns_expiration_date_separate_from_lease(mock_post, expiration_date):
|
|
72
|
+
mock_post.return_value = response({
|
|
73
|
+
"code": 0, "message": "Checked out", "sessionId": "session_123",
|
|
74
|
+
"sessionSecret": "secret_123", "nextNonce": "nonce_123",
|
|
75
|
+
"expiresAt": "2026-10-26T12:05:00.000Z", "heartbeatInterval": 60,
|
|
76
|
+
"metadata": None, "expirationDate": expiration_date,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
result = KeyMint("client_test").floating_checkout({
|
|
80
|
+
"productId": "product_123", "licenseKey": "license_123", "hostId": "host_123",
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
assert mock_post.call_args.args[0].endswith("/key/checkout")
|
|
84
|
+
assert result["expirationDate"] == expiration_date
|
|
85
|
+
assert result["expiresAt"] == "2026-10-26T12:05:00.000Z"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|