keymint 2.3.1__tar.gz → 2.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keymint
3
- Version: 2.3.1
3
+ Version: 2.5.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.6
23
+ Requires-Python: >=3.10
28
24
  Description-Content-Type: text/markdown
29
25
  License-File: LICENSE
30
- Requires-Dist: requests>=2.25.0
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"
@@ -107,8 +103,10 @@ Keymint provides utilities to uniquely identify machines for node-locking:
107
103
  | `activate_key` | Activates a license key for a device. |
108
104
  | `deactivate_key` | Deactivates a device from a license key. |
109
105
  | `get_key` | Retrieves detailed information about a key. |
106
+ | `update_key` | Updates an existing license key. |
110
107
  | `block_key` | Blocks a license key. |
111
108
  | `unblock_key` | Unblocks a previously blocked license key. |
109
+ | `sign_key` | Signs a key for offline validation using an admin API key.|
112
110
  | `floating_checkout` | Checks out a floating license seat. |
113
111
  | `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
114
112
  | `floating_checkin` | Checks in a session, releasing the seat. |
@@ -58,8 +58,10 @@ Keymint provides utilities to uniquely identify machines for node-locking:
58
58
  | `activate_key` | Activates a license key for a device. |
59
59
  | `deactivate_key` | Deactivates a device from a license key. |
60
60
  | `get_key` | Retrieves detailed information about a key. |
61
+ | `update_key` | Updates an existing license key. |
61
62
  | `block_key` | Blocks a license key. |
62
63
  | `unblock_key` | Unblocks a previously blocked license key. |
64
+ | `sign_key` | Signs a key for offline validation using an admin API key.|
63
65
  | `floating_checkout` | Checks out a floating license seat. |
64
66
  | `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
65
67
  | `floating_checkin` | Checks in a session, releasing the seat. |
@@ -1,6 +1,7 @@
1
1
  import requests
2
- from .types import *
2
+
3
3
  from ._version import __version__
4
+ from .types import *
4
5
 
5
6
  __all__ = ['KeyMint', 'KeyMintApiError', '__version__']
6
7
 
@@ -16,19 +17,23 @@ class KeyMint:
16
17
  'Content-Type': 'application/json'
17
18
  }
18
19
 
19
- def _handle_request(self, method: str, endpoint: str, params: dict = None, query_params: dict = None, idempotency_key: str = 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):
20
21
  url = f'{self.base_url}{endpoint}'
21
22
  headers = self.headers.copy()
22
23
  if idempotency_key:
23
24
  headers['Idempotency-Key'] = idempotency_key
25
+ if extra_headers:
26
+ headers.update(extra_headers)
24
27
 
25
28
  try:
26
29
  if method.upper() == 'GET':
27
30
  response = requests.get(url, params=query_params, headers=headers)
28
31
  elif method.upper() == 'POST':
29
- response = requests.post(url, json=params, headers=headers)
32
+ response = requests.post(url, json=params, params=query_params, headers=headers)
30
33
  elif method.upper() == 'PUT':
31
- response = requests.put(url, json=params, headers=headers)
34
+ response = requests.put(url, json=params, params=query_params, headers=headers)
35
+ elif method.upper() == 'PATCH':
36
+ response = requests.patch(url, json=params, params=query_params, headers=headers)
32
37
  elif method.upper() == 'DELETE':
33
38
  response = requests.delete(url, params=query_params, headers=headers)
34
39
  else:
@@ -39,8 +44,9 @@ class KeyMint:
39
44
  except requests.exceptions.HTTPError as http_err:
40
45
  try:
41
46
  error_data = http_err.response.json()
47
+ nested_error = error_data.get('error') if isinstance(error_data.get('error'), dict) else {}
42
48
  raise KeyMintApiError(
43
- message=error_data.get('message', 'An API error occurred'),
49
+ message=error_data.get('message') or nested_error.get('message') or 'An API error occurred',
44
50
  code=error_data.get('code', -1),
45
51
  status=http_err.response.status_code
46
52
  )
@@ -53,7 +59,7 @@ class KeyMint:
53
59
  except Exception as err:
54
60
  raise KeyMintApiError(message=str(err), code=-1)
55
61
 
56
- def create_key(self, params: CreateKeyParams, idempotency_key: str = None) -> CreateKeyResponse:
62
+ def create_key(self, params: CreateKeyParams, idempotency_key: str | None = None) -> CreateKeyResponse:
57
63
  """
58
64
  Creates a new license key.
59
65
  :param params: Parameters for creating the key.
@@ -62,7 +68,7 @@ class KeyMint:
62
68
  """
63
69
  return self._handle_request('POST', '/key', params, idempotency_key=idempotency_key)
64
70
 
65
- def activate_key(self, params: ActivateKeyParams, idempotency_key: str = None) -> ActivateKeyResponse:
71
+ def activate_key(self, params: ActivateKeyParams, idempotency_key: str | None = None) -> ActivateKeyResponse:
66
72
  """
67
73
  Activates a license key for a specific device.
68
74
 
@@ -77,7 +83,7 @@ class KeyMint:
77
83
  """
78
84
  return self._handle_request('POST', '/key/activate', params, idempotency_key=idempotency_key)
79
85
 
80
- def deactivate_key(self, params: DeactivateKeyParams, idempotency_key: str = None) -> DeactivateKeyResponse:
86
+ def deactivate_key(self, params: DeactivateKeyParams, idempotency_key: str | None = None) -> DeactivateKeyResponse:
81
87
  """
82
88
  Deactivates a device from a license key.
83
89
  :param params: Parameters for deactivating the key.
@@ -86,7 +92,7 @@ class KeyMint:
86
92
  """
87
93
  return self._handle_request('POST', '/key/deactivate', params, idempotency_key=idempotency_key)
88
94
 
89
- def floating_checkout(self, params: FloatingCheckoutParams, idempotency_key: str = None) -> FloatingCheckoutResponse:
95
+ def floating_checkout(self, params: FloatingCheckoutParams, idempotency_key: str | None = None) -> FloatingCheckoutResponse:
90
96
  """
91
97
  Checks out a floating license seat.
92
98
  :param params: Parameters for checking out the license.
@@ -95,7 +101,7 @@ class KeyMint:
95
101
  """
96
102
  return self._handle_request('POST', '/key/checkout', params, idempotency_key=idempotency_key)
97
103
 
98
- def floating_heartbeat(self, params: FloatingHeartbeatParams, idempotency_key: str = None) -> FloatingHeartbeatResponse:
104
+ def floating_heartbeat(self, params: FloatingHeartbeatParams, idempotency_key: str | None = None) -> FloatingHeartbeatResponse:
99
105
  """
100
106
  Sends a heartbeat to keep a floating license session alive.
101
107
  :param params: Parameters for the heartbeat (includes rotating signature).
@@ -104,7 +110,7 @@ class KeyMint:
104
110
  """
105
111
  return self._handle_request('POST', '/key/heartbeat', params, idempotency_key=idempotency_key)
106
112
 
107
- def floating_checkin(self, params: FloatingCheckinParams, idempotency_key: str = None) -> FloatingCheckinResponse:
113
+ def floating_checkin(self, params: FloatingCheckinParams, idempotency_key: str | None = None) -> FloatingCheckinResponse:
108
114
  """
109
115
  Checks in a floating license session, releasing the seat.
110
116
  :param params: Parameters for checking in the license (includes rotating signature).
@@ -120,12 +126,14 @@ class KeyMint:
120
126
  :returns: The license key details.
121
127
  """
122
128
  query_params = {
123
- 'productId': params['productId'],
124
- 'licenseKey': params['licenseKey']
129
+ 'productId': params['productId']
125
130
  }
126
- return self._handle_request('GET', '/key', query_params=query_params)
131
+ return self._handle_request(
132
+ 'GET', '/key', query_params=query_params,
133
+ extra_headers={'x-license-key': params['licenseKey']}
134
+ )
127
135
 
128
- def block_key(self, params: BlockKeyParams, idempotency_key: str = None) -> BlockKeyResponse:
136
+ def block_key(self, params: BlockKeyParams, idempotency_key: str | None = None) -> BlockKeyResponse:
129
137
  """
130
138
  Blocks a specific license key.
131
139
  :param params: Parameters for blocking the key.
@@ -134,7 +142,7 @@ class KeyMint:
134
142
  """
135
143
  return self._handle_request('POST', '/key/block', params, idempotency_key=idempotency_key)
136
144
 
137
- def unblock_key(self, params: UnblockKeyParams, idempotency_key: str = None) -> UnblockKeyResponse:
145
+ def unblock_key(self, params: UnblockKeyParams, idempotency_key: str | None = None) -> UnblockKeyResponse:
138
146
  """
139
147
  Unblocks a previously blocked license key.
140
148
  :param params: Parameters for unblocking the key.
@@ -143,9 +151,29 @@ class KeyMint:
143
151
  """
144
152
  return self._handle_request('POST', '/key/unblock', params, idempotency_key=idempotency_key)
145
153
 
154
+ def update_key(self, params: 'UpdateKeyParams', idempotency_key: str | None = None) -> 'UpdateKeyResponse':
155
+ """
156
+ Updates an existing license key.
157
+ Requires productId and licenseKey. All other fields are optional.
158
+ :param params: Parameters for updating the key.
159
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
160
+ :returns: The update confirmation.
161
+ """
162
+ return self._handle_request('PATCH', '/key', params, idempotency_key=idempotency_key)
163
+
164
+ def sign_key(self, params: 'SignKeyParams', idempotency_key: str | None = None) -> 'SignKeyResponse':
165
+ """
166
+ Signs a license key for offline (air-gapped) validation.
167
+ Requires admin API key scope and Standard plan.
168
+ :param params: Signing parameters (hostId is required).
169
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
170
+ :returns: The signed license file.
171
+ """
172
+ return self._handle_request('POST', '/key/sign', params, idempotency_key=idempotency_key)
173
+
146
174
  # Customer Management Methods
147
175
 
148
- def create_customer(self, params: CreateCustomerParams, idempotency_key: str = None) -> CreateCustomerResponse:
176
+ def create_customer(self, params: CreateCustomerParams, idempotency_key: str | None = None) -> CreateCustomerResponse:
149
177
  """
150
178
  Creates a new customer.
151
179
  :param params: Parameters for creating the customer.
@@ -171,7 +199,7 @@ class KeyMint:
171
199
  query_params = {'customerId': params['customerId']}
172
200
  return self._handle_request('GET', '/customer/by-id', query_params=query_params)
173
201
 
174
- def update_customer(self, params: UpdateCustomerParams, idempotency_key: str = None) -> UpdateCustomerResponse:
202
+ def update_customer(self, params: UpdateCustomerParams, idempotency_key: str | None = None) -> UpdateCustomerResponse:
175
203
  """
176
204
  Updates an existing customer's information.
177
205
  :param params: Parameters for updating the customer.
@@ -180,7 +208,7 @@ class KeyMint:
180
208
  """
181
209
  return self._handle_request('PUT', '/customer/by-id', params, idempotency_key=idempotency_key)
182
210
 
183
- def delete_customer(self, params: DeleteCustomerParams, idempotency_key: str = None) -> DeleteCustomerResponse:
211
+ def delete_customer(self, params: DeleteCustomerParams, idempotency_key: str | None = None) -> DeleteCustomerResponse:
184
212
  """
185
213
  Permanently deletes a customer and all associated license keys.
186
214
  :param params: Parameters containing the customer ID.
@@ -190,16 +218,16 @@ class KeyMint:
190
218
  query_params = {'customerId': params['customerId']}
191
219
  return self._handle_request('DELETE', '/customer/by-id', query_params=query_params, idempotency_key=idempotency_key)
192
220
 
193
- def get_customer_with_keys(self, params: GetCustomerWithKeysParams) -> GetCustomerWithKeysResponse:
221
+ def get_customer_with_keys(self, params: GetCustomerWithKeysParams) -> List[Dict[str, Any]]:
194
222
  """
195
- Retrieves detailed information about a customer along with their license keys.
223
+ Retrieves license keys belonging to a specific customer.
196
224
  :param params: Parameters containing the customer ID.
197
- :returns: The customer information with associated license keys.
225
+ :returns: A flat list of license key objects.
198
226
  """
199
227
  query_params = {'customerId': params['customerId']}
200
228
  return self._handle_request('GET', '/customer/keys', query_params=query_params)
201
229
 
202
- def toggle_customer_status(self, params: ToggleCustomerStatusParams, idempotency_key: str = None) -> ToggleCustomerStatusResponse:
230
+ def toggle_customer_status(self, params: ToggleCustomerStatusParams, idempotency_key: str | None = None) -> ToggleCustomerStatusResponse:
203
231
  """
204
232
  Toggles the active status of a customer account (disable or enable).
205
233
  :param params: Parameters containing the customer ID.
@@ -219,8 +247,8 @@ class KeyMint:
219
247
  :param tolerance_seconds: Time tolerance in seconds to prevent replay attacks. Defaults to 300 (5 minutes).
220
248
  :returns: True if the signature is valid, False otherwise.
221
249
  """
222
- import hmac
223
250
  import hashlib
251
+ import hmac
224
252
  import time
225
253
 
226
254
  if not header or not secret:
@@ -253,7 +281,7 @@ class KeyMint:
253
281
  return False
254
282
 
255
283
  # Verify HMAC signature
256
- signable_content = f"{timestamp_str}.{payload}".encode("utf-8")
284
+ signable_content = f"{timestamp_str}.{payload}".encode()
257
285
  expected_signature = hmac.new(
258
286
  secret.encode("utf-8"),
259
287
  signable_content,
@@ -265,4 +293,3 @@ class KeyMint:
265
293
  except Exception:
266
294
  return False
267
295
 
268
-
@@ -1,6 +1,6 @@
1
1
  """KeyMint Python SDK version information."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.5.0"
4
4
  __author__ = "KeyMint"
5
5
  __email__ = "cliff@keymint.dev"
6
6
  __url__ = "https://github.com/keymint-dev/keymint-python"
@@ -10,14 +10,13 @@ import hashlib
10
10
  import hmac
11
11
  import os
12
12
  import platform
13
- import subprocess
14
- import uuid
15
13
  import re
14
+ import subprocess
16
15
  import time
16
+ import uuid
17
17
  from pathlib import Path
18
18
  from typing import Optional
19
19
 
20
-
21
20
  # ─── Garbage Detection ──────────────────────────────────────────────────
22
21
 
23
22
  _GARBAGE_PATTERNS = [
@@ -54,7 +53,7 @@ def _hash(value: str) -> str:
54
53
 
55
54
  # ─── Fingerprint Layers ────────────────────────────────────────────────
56
55
 
57
- def _get_bios_uuid() -> Optional[str]:
56
+ def _get_bios_uuid() -> str | None:
58
57
  """Layer 1: BIOS / Hardware UUID."""
59
58
  system = platform.system()
60
59
  try:
@@ -82,7 +81,7 @@ def _get_bios_uuid() -> Optional[str]:
82
81
  return None
83
82
 
84
83
 
85
- def _get_os_machine_id() -> Optional[str]:
84
+ def _get_os_machine_id() -> str | None:
86
85
  """Layer 2: OS-level persistent machine ID."""
87
86
  system = platform.system()
88
87
  try:
@@ -112,11 +111,11 @@ def _get_os_machine_id() -> Optional[str]:
112
111
  return None
113
112
 
114
113
 
115
- def _get_primary_mac() -> Optional[str]:
114
+ def _get_primary_mac() -> str | None:
116
115
  """Layer 3: Primary network interface MAC address."""
117
116
  try:
118
- import socket
119
117
  import fcntl
118
+ import socket
120
119
  import struct
121
120
  except ImportError:
122
121
  pass
@@ -136,7 +135,7 @@ def _get_primary_mac() -> Optional[str]:
136
135
 
137
136
  # ─── Public API ─────────────────────────────────────────────────────────
138
137
 
139
- def get_machine_id() -> Optional[str]:
138
+ def get_machine_id() -> str | None:
140
139
  """
141
140
  Best-effort hardware fingerprint. Attempts to read the machine's
142
141
  BIOS/System UUID, then falls back through OS-level IDs and network
@@ -162,7 +161,7 @@ def get_machine_id() -> Optional[str]:
162
161
  return None
163
162
 
164
163
 
165
- def get_or_create_installation_id(storage_path: Optional[str] = None) -> str:
164
+ def get_or_create_installation_id(storage_path: str | None = None) -> str:
166
165
  """
167
166
  Returns a guaranteed-unique, guaranteed-stable installation identifier.
168
167
  On first call, generates a UUIDv4 seeded with whatever hardware info
@@ -218,5 +217,5 @@ def generate_session_signature(session_id: str, nonce: str, session_secret: str)
218
217
  A 64-character hexadecimal signature string.
219
218
  """
220
219
  key_bytes = session_secret.encode('utf-8')
221
- msg_bytes = f"{session_id}:{nonce}".encode('utf-8')
220
+ msg_bytes = f"{session_id}:{nonce}".encode()
222
221
  return hmac.new(key_bytes, msg_bytes, hashlib.sha256).hexdigest()
@@ -0,0 +1,287 @@
1
+ from typing import Any, Dict, List, Optional, TypedDict
2
+
3
+
4
+ class NewCustomer(TypedDict):
5
+ name: str
6
+ email: str | None
7
+
8
+ class KeyFormat(TypedDict):
9
+ sections: int | None
10
+ sectionLength: int | None
11
+ separator: str | None
12
+ charset: str | None
13
+ prefix: str | None
14
+ suffix: str | None
15
+ case: str | None # 'upper' | 'lower' | 'mixed'
16
+
17
+ class CreateKeyParams(TypedDict):
18
+ productId: str
19
+ maxActivations: str | None
20
+ expiryDate: str | None
21
+ customerId: str | None
22
+ versionId: str | None
23
+ metadata: dict[str, Any] | None
24
+ newCustomer: NewCustomer | None
25
+ allowedHosts: list[str] | None
26
+ format: KeyFormat | None
27
+ amountKeys: str | None
28
+ licenseType: str | None # 'node-locked' | 'floating'
29
+ maxConcurrentSessions: int | None
30
+ heartbeatInterval: int | None
31
+ sessionLeaseDuration: int | None
32
+
33
+ class CreateKeyResponse(TypedDict, total=False):
34
+ code: int
35
+ key: str
36
+ keys: list[str]
37
+
38
+ class KeyMintApiError(Exception):
39
+ def __init__(self, message: str, code: int, status: int | None = None):
40
+ super().__init__(message)
41
+ self.message = message
42
+ self.code = code
43
+ self.status = status
44
+
45
+ class ActivateKeyParams(TypedDict):
46
+ productId: str
47
+ licenseKey: str
48
+ hostId: str | None
49
+ deviceTag: str | None
50
+ licensee: dict[str, str] | None # { name, email }
51
+ version: str | None
52
+
53
+ class ActivateKeyResponse(TypedDict):
54
+ code: int
55
+ message: str
56
+ licenseeName: str | None
57
+ licenseeEmail: str | None
58
+ metadata: dict[str, Any] | None
59
+ versionId: str | None
60
+ version: dict[str, Any] | None
61
+ allowedHosts: list[str] | None
62
+
63
+ class DeactivateKeyParams(TypedDict):
64
+ productId: str
65
+ licenseKey: str
66
+ hostId: str | None
67
+
68
+ class DeactivateKeyResponse(TypedDict):
69
+ message: str
70
+ code: int
71
+ devicesRemoved: int
72
+
73
+ class DeviceDetails(TypedDict):
74
+ hostId: str
75
+ deviceTag: str | None
76
+ ipAddress: str | None
77
+ activationTime: str
78
+
79
+ class LicenseDetails(TypedDict):
80
+ id: str
81
+ key: str
82
+ productId: str
83
+ maxActivations: int
84
+ activations: int
85
+ devices: list[DeviceDetails]
86
+ activated: bool
87
+ expirationDate: str | None
88
+ versionId: str | None
89
+ metadata: dict[str, Any] | None
90
+ allowedHosts: list[str] | None
91
+ version: dict[str, Any] | None
92
+
93
+ class CustomerDetails(TypedDict):
94
+ id: str
95
+ name: str | None
96
+ email: str | None
97
+ active: bool
98
+
99
+ class GetKeyParams(TypedDict):
100
+ productId: str
101
+ licenseKey: str
102
+
103
+ class GetKeyResponse(TypedDict):
104
+ data: dict[str, Any]
105
+ code: int
106
+
107
+ class BlockKeyParams(TypedDict):
108
+ productId: str
109
+ licenseKey: str
110
+
111
+ class BlockKeyResponse(TypedDict):
112
+ message: str
113
+ code: int
114
+
115
+ class UnblockKeyParams(TypedDict):
116
+ productId: str
117
+ licenseKey: str
118
+
119
+ class UnblockKeyResponse(TypedDict):
120
+ message: str
121
+ code: int
122
+
123
+ # Customer Management Types
124
+
125
+ class CreateCustomerParams(TypedDict):
126
+ name: str
127
+ email: str | None
128
+
129
+ class CreateCustomerResponse(TypedDict):
130
+ action: str
131
+ status: bool
132
+ message: str
133
+ data: dict[str, Any]
134
+ code: int
135
+
136
+ class GetAllCustomersParams(TypedDict):
137
+ page: int | None
138
+ limit: int | None
139
+ email: str | None
140
+
141
+ class PaginationMeta(TypedDict):
142
+ total: int
143
+ page: int
144
+ limit: int
145
+ totalPages: int
146
+
147
+ class GetAllCustomersResponse(TypedDict):
148
+ action: str
149
+ status: bool
150
+ data: list[dict[str, Any]]
151
+ meta: PaginationMeta | None
152
+ code: int
153
+
154
+ class GetCustomerByIdParams(TypedDict):
155
+ customerId: str
156
+
157
+ class GetCustomerByIdResponse(TypedDict):
158
+ action: str
159
+ status: bool
160
+ data: list[dict[str, Any]]
161
+ code: int
162
+
163
+ class UpdateCustomerParams(TypedDict):
164
+ customerId: str
165
+ name: str | None # Optional: Updated customer name
166
+ email: str | None # Optional: Updated customer email
167
+
168
+ class UpdateCustomerResponse(TypedDict):
169
+ action: str
170
+ status: bool
171
+ code: int
172
+
173
+ class DeleteCustomerParams(TypedDict):
174
+ customerId: str
175
+
176
+ class DeleteCustomerResponse(TypedDict):
177
+ action: str
178
+ status: bool
179
+ code: int
180
+
181
+ class ToggleCustomerStatusParams(TypedDict):
182
+ customerId: str
183
+
184
+ class ToggleCustomerStatusResponse(TypedDict, total=False):
185
+ action: str
186
+ status: bool
187
+ message: str
188
+ code: int
189
+ customerName: str
190
+ active: bool
191
+
192
+ class CustomerLicenseKey(TypedDict):
193
+ id: str
194
+ key: str
195
+ productId: str
196
+ maxActivations: int
197
+ activations: int
198
+ activated: bool
199
+ expirationDate: str | None
200
+ versionId: str | None
201
+ metadata: dict[str, Any] | None
202
+ allowedHosts: list[str] | None
203
+
204
+ class GetCustomerWithKeysParams(TypedDict):
205
+ customerId: str
206
+
207
+ # GetCustomerWithKeys returns a flat LicenseKey[] — no wrapper object.
208
+ # Use List[Dict[str, Any]] for the response type.
209
+
210
+ class FloatingCheckoutParams(TypedDict):
211
+ productId: str
212
+ licenseKey: str
213
+ hostId: str
214
+ deviceTag: str | None
215
+ userIdentifier: str | None
216
+ apiKey: str | None
217
+ timestamp: Any | None
218
+ signature: str | None
219
+
220
+ class FloatingCheckoutResponse(TypedDict):
221
+ code: int
222
+ message: str
223
+ sessionId: str
224
+ sessionSecret: str
225
+ nextNonce: str
226
+ expiresAt: str
227
+ heartbeatInterval: int
228
+ metadata: dict[str, Any] | None
229
+ currentSessions: int | None
230
+ maxSessions: int | None
231
+ licenseeName: str | None
232
+ licenseeEmail: str | None
233
+
234
+ class FloatingHeartbeatParams(TypedDict):
235
+ productId: str
236
+ licenseKey: str
237
+ sessionId: str
238
+ timestamp: Any # rotating nonce (nextNonce) received from previous response
239
+ signature: str
240
+ apiKey: str | None
241
+
242
+ class FloatingHeartbeatResponse(TypedDict):
243
+ code: int
244
+ message: str
245
+ expiresAt: str
246
+ nextNonce: str
247
+
248
+ class FloatingCheckinParams(TypedDict):
249
+ productId: str
250
+ licenseKey: str
251
+ sessionId: str
252
+ timestamp: Any # rotating nonce (nextNonce) received from previous response
253
+ signature: str
254
+ apiKey: str | None
255
+
256
+ class FloatingCheckinResponse(TypedDict):
257
+ code: int
258
+ message: str
259
+
260
+ class UpdateKeyParams(TypedDict):
261
+ productId: str
262
+ licenseKey: str
263
+ maxActivations: Any | None # string or number
264
+ expiryDate: str | None
265
+ customerId: str | None
266
+ newCustomer: NewCustomer | None
267
+ metadata: dict[str, Any] | None
268
+ versionId: str | None
269
+ allowedHosts: list[str] | None
270
+ licenseType: str | None # 'node-locked' | 'floating'
271
+ maxConcurrentSessions: int | None
272
+ heartbeatInterval: int | None
273
+ sessionLeaseDuration: int | None
274
+
275
+ class UpdateKeyResponse(TypedDict):
276
+ code: int
277
+ message: str
278
+ affectedCount: int | None
279
+
280
+ class SignKeyParams(TypedDict):
281
+ productId: str
282
+ licenseKey: str
283
+ hostId: str
284
+ ttl: int | None
285
+
286
+ class SignKeyResponse(TypedDict):
287
+ 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.1
3
+ Version: 2.5.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.6
23
+ Requires-Python: >=3.10
28
24
  Description-Content-Type: text/markdown
29
25
  License-File: LICENSE
30
- Requires-Dist: requests>=2.25.0
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"
@@ -107,8 +103,10 @@ Keymint provides utilities to uniquely identify machines for node-locking:
107
103
  | `activate_key` | Activates a license key for a device. |
108
104
  | `deactivate_key` | Deactivates a device from a license key. |
109
105
  | `get_key` | Retrieves detailed information about a key. |
106
+ | `update_key` | Updates an existing license key. |
110
107
  | `block_key` | Blocks a license key. |
111
108
  | `unblock_key` | Unblocks a previously blocked license key. |
109
+ | `sign_key` | Signs a key for offline validation using an admin API key.|
112
110
  | `floating_checkout` | Checks out a floating license seat. |
113
111
  | `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
114
112
  | `floating_checkin` | Checks in a session, releasing the seat. |
@@ -10,4 +10,5 @@ keymint.egg-info/PKG-INFO
10
10
  keymint.egg-info/SOURCES.txt
11
11
  keymint.egg-info/dependency_links.txt
12
12
  keymint.egg-info/requires.txt
13
- keymint.egg-info/top_level.txt
13
+ keymint.egg-info/top_level.txt
14
+ tests/test_contracts.py
@@ -1,4 +1,4 @@
1
- requests>=2.25.0
1
+ requests>=2.32.4
2
2
 
3
3
  [dev]
4
4
  pytest>=6.0
@@ -1,11 +1,11 @@
1
- from setuptools import setup, find_packages
1
+ from setuptools import find_packages, setup
2
2
 
3
3
  with open("README.md", "r", encoding="utf-8") as fh:
4
4
  long_description = fh.read()
5
5
 
6
6
  setup(
7
7
  name="keymint",
8
- version="2.3.1",
8
+ version="2.5.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.6',
35
+ python_requires='>=3.10',
40
36
  install_requires=[
41
- 'requests>=2.25.0',
37
+ 'requests>=2.32.4',
42
38
  ],
43
39
  extras_require={
44
40
  'dev': [
@@ -0,0 +1,52 @@
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"})
@@ -1,237 +0,0 @@
1
- from typing import TypedDict, Optional, List, Dict, Any
2
-
3
- class NewCustomer(TypedDict):
4
- name: str
5
- email: Optional[str]
6
-
7
- class CreateKeyParams(TypedDict):
8
- productId: str
9
- maxActivations: Optional[str]
10
- expiryDate: Optional[str]
11
- customerId: Optional[str]
12
- versionId: Optional[str]
13
- metadata: Optional[Dict[str, Any]]
14
- newCustomer: Optional[NewCustomer]
15
- allowedHosts: Optional[List[str]]
16
-
17
- class CreateKeyResponse(TypedDict):
18
- code: int
19
- key: str
20
-
21
- class KeyMintApiError(Exception):
22
- def __init__(self, message: str, code: int, status: Optional[int] = None):
23
- super().__init__(message)
24
- self.message = message
25
- self.code = code
26
- self.status = status
27
-
28
- class ActivateKeyParams(TypedDict):
29
- productId: str
30
- licenseKey: str
31
- hostId: Optional[str]
32
- deviceTag: Optional[str]
33
-
34
- class ActivateKeyResponse(TypedDict):
35
- code: int
36
- message: str
37
- licenseeName: Optional[str]
38
- licenseeEmail: Optional[str]
39
- metadata: Optional[Dict[str, Any]]
40
- versionId: Optional[str]
41
- version: Optional[Dict[str, Any]]
42
- allowedHosts: Optional[List[str]]
43
-
44
- class DeactivateKeyParams(TypedDict):
45
- productId: str
46
- licenseKey: str
47
- hostId: Optional[str]
48
-
49
- class DeactivateKeyResponse(TypedDict):
50
- message: str
51
- code: int
52
-
53
- class DeviceDetails(TypedDict):
54
- hostId: str
55
- deviceTag: Optional[str]
56
- ipAddress: Optional[str]
57
- activationTime: str
58
-
59
- class LicenseDetails(TypedDict):
60
- id: str
61
- key: str
62
- productId: str
63
- maxActivations: int
64
- activations: int
65
- devices: List[DeviceDetails]
66
- activated: bool
67
- expirationDate: Optional[str]
68
- versionId: Optional[str]
69
- metadata: Optional[Dict[str, Any]]
70
- allowedHosts: Optional[List[str]]
71
- version: Optional[Dict[str, Any]]
72
-
73
- class CustomerDetails(TypedDict):
74
- id: str
75
- name: Optional[str]
76
- email: Optional[str]
77
- active: bool
78
-
79
- class GetKeyParams(TypedDict):
80
- productId: str
81
- licenseKey: str
82
-
83
- class GetKeyResponse(TypedDict):
84
- data: Dict[str, Any]
85
- code: int
86
-
87
- class BlockKeyParams(TypedDict):
88
- productId: str
89
- licenseKey: str
90
-
91
- class BlockKeyResponse(TypedDict):
92
- message: str
93
- code: int
94
-
95
- class UnblockKeyParams(TypedDict):
96
- productId: str
97
- licenseKey: str
98
-
99
- class UnblockKeyResponse(TypedDict):
100
- message: str
101
- code: int
102
-
103
- # Customer Management Types
104
-
105
- class CreateCustomerParams(TypedDict):
106
- name: str
107
- email: str
108
-
109
- class CreateCustomerResponse(TypedDict):
110
- action: str
111
- status: bool
112
- message: str
113
- data: Dict[str, Any]
114
- code: int
115
-
116
- class GetAllCustomersParams(TypedDict):
117
- page: Optional[int]
118
- limit: Optional[int]
119
- email: Optional[str]
120
-
121
- class PaginationMeta(TypedDict):
122
- total: int
123
- page: int
124
- limit: int
125
- totalPages: int
126
-
127
- class GetAllCustomersResponse(TypedDict):
128
- action: str
129
- status: bool
130
- data: List[Dict[str, Any]]
131
- meta: Optional[PaginationMeta]
132
- code: int
133
-
134
- class GetCustomerByIdParams(TypedDict):
135
- customerId: str
136
-
137
- class GetCustomerByIdResponse(TypedDict):
138
- action: str
139
- status: bool
140
- data: List[Dict[str, Any]]
141
- code: int
142
-
143
- class UpdateCustomerParams(TypedDict):
144
- name: str
145
- email: str
146
- customerId: str
147
-
148
- class UpdateCustomerResponse(TypedDict):
149
- action: str
150
- status: bool
151
- code: int
152
-
153
- class DeleteCustomerParams(TypedDict):
154
- customerId: str
155
-
156
- class DeleteCustomerResponse(TypedDict):
157
- action: str
158
- status: bool
159
- code: int
160
-
161
- class ToggleCustomerStatusParams(TypedDict):
162
- customerId: str
163
-
164
- class ToggleCustomerStatusResponse(TypedDict):
165
- action: str
166
- status: bool
167
- message: str
168
- code: int
169
-
170
- class CustomerLicenseKey(TypedDict):
171
- id: str
172
- key: str
173
- productId: str
174
- maxActivations: int
175
- activations: int
176
- activated: bool
177
- expirationDate: Optional[str]
178
- versionId: Optional[str]
179
- metadata: Optional[Dict[str, Any]]
180
- allowedHosts: Optional[List[str]]
181
-
182
- class GetCustomerWithKeysParams(TypedDict):
183
- customerId: str
184
-
185
- class GetCustomerWithKeysResponse(TypedDict):
186
- action: str
187
- status: bool
188
- data: Dict[str, Any] # Contains customer and licenseKeys
189
- code: int
190
-
191
- class FloatingCheckoutParams(TypedDict):
192
- productId: str
193
- licenseKey: str
194
- hostId: str
195
- deviceTag: Optional[str]
196
- userIdentifier: Optional[str]
197
- apiKey: Optional[str]
198
-
199
- class FloatingCheckoutResponse(TypedDict):
200
- code: int
201
- message: str
202
- sessionId: str
203
- sessionSecret: str
204
- nextNonce: str
205
- expiresAt: str
206
- heartbeatInterval: int
207
- metadata: Optional[Dict[str, Any]]
208
- currentSessions: Optional[int]
209
- maxSessions: Optional[int]
210
- licenseeName: Optional[str]
211
- licenseeEmail: Optional[str]
212
-
213
- class FloatingHeartbeatParams(TypedDict):
214
- productId: str
215
- licenseKey: str
216
- sessionId: str
217
- timestamp: Any # rotating nonce (nextNonce) received from previous response
218
- signature: str
219
- apiKey: Optional[str]
220
-
221
- class FloatingHeartbeatResponse(TypedDict):
222
- code: int
223
- message: str
224
- expiresAt: str
225
- nextNonce: str
226
-
227
- class FloatingCheckinParams(TypedDict):
228
- productId: str
229
- licenseKey: str
230
- sessionId: str
231
- timestamp: Any # rotating nonce (nextNonce) received from previous response
232
- signature: str
233
- apiKey: Optional[str]
234
-
235
- class FloatingCheckinResponse(TypedDict):
236
- code: int
237
- message: str
File without changes
File without changes
File without changes