keymint 2.1.1__tar.gz → 2.3.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.1.1
3
+ Version: 2.3.0
4
4
  Summary: Official Python SDK for KeyMint license management with comprehensive API coverage.
5
5
  Home-page: https://github.com/keymint-dev/keymint-python
6
6
  Author: KeyMint
@@ -70,13 +70,13 @@ pip install keymint
70
70
  import os
71
71
  from keymint import KeyMint, identity
72
72
 
73
- access_token = os.environ.get('KEYMINT_ACCESS_TOKEN')
73
+ api_key = os.environ.get('KEYMINT_API_KEY')
74
74
  product_id = os.environ.get('KEYMINT_PRODUCT_ID')
75
75
 
76
- if not access_token or not product_id:
77
- raise ValueError('Please set the KEYMINT_ACCESS_TOKEN and KEYMINT_PRODUCT_ID environment variables.')
76
+ if not api_key or not product_id:
77
+ raise ValueError('Please set the KEYMINT_API_KEY and KEYMINT_PRODUCT_ID environment variables.')
78
78
 
79
- sdk = KeyMint(access_token)
79
+ sdk = KeyMint(api_key)
80
80
 
81
81
  # 1. Get a stable, unique ID for this machine
82
82
  host_id = identity.get_or_create_installation_id()
@@ -109,6 +109,9 @@ Keymint provides utilities to uniquely identify machines for node-locking:
109
109
  | `get_key` | Retrieves detailed information about a key. |
110
110
  | `block_key` | Blocks a license key. |
111
111
  | `unblock_key` | Unblocks a previously blocked license key. |
112
+ | `floating_checkout` | Checks out a floating license seat. |
113
+ | `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
114
+ | `floating_checkin` | Checks in a session, releasing the seat. |
112
115
 
113
116
  ### Customer Management
114
117
 
@@ -122,6 +125,22 @@ Keymint provides utilities to uniquely identify machines for node-locking:
122
125
  | `toggle_customer_status`| Toggles customer active status. |
123
126
  | `delete_customer` | Permanently deletes a customer and their keys. |
124
127
 
128
+ ### Webhook Verification
129
+
130
+ | Method | Description |
131
+ |-------------------------|--------------------------------------------------|
132
+ | `verify_webhook_signature`| Verifies the signature of a webhook request payload. |
133
+
134
+ ## Idempotency
135
+
136
+ All mutating SDK methods support idempotency keys to safely retry requests in case of network drops. Pass a unique string (recommended: UUID v4) as the optional `idempotency_key` keyword argument:
137
+
138
+ ```python
139
+ result = sdk.create_key({
140
+ 'productId': product_id,
141
+ }, idempotency_key='9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d')
142
+ ```
143
+
125
144
  ## License
126
145
  MIT
127
146
 
@@ -21,13 +21,13 @@ pip install keymint
21
21
  import os
22
22
  from keymint import KeyMint, identity
23
23
 
24
- access_token = os.environ.get('KEYMINT_ACCESS_TOKEN')
24
+ api_key = os.environ.get('KEYMINT_API_KEY')
25
25
  product_id = os.environ.get('KEYMINT_PRODUCT_ID')
26
26
 
27
- if not access_token or not product_id:
28
- raise ValueError('Please set the KEYMINT_ACCESS_TOKEN and KEYMINT_PRODUCT_ID environment variables.')
27
+ if not api_key or not product_id:
28
+ raise ValueError('Please set the KEYMINT_API_KEY and KEYMINT_PRODUCT_ID environment variables.')
29
29
 
30
- sdk = KeyMint(access_token)
30
+ sdk = KeyMint(api_key)
31
31
 
32
32
  # 1. Get a stable, unique ID for this machine
33
33
  host_id = identity.get_or_create_installation_id()
@@ -60,6 +60,9 @@ Keymint provides utilities to uniquely identify machines for node-locking:
60
60
  | `get_key` | Retrieves detailed information about a key. |
61
61
  | `block_key` | Blocks a license key. |
62
62
  | `unblock_key` | Unblocks a previously blocked license key. |
63
+ | `floating_checkout` | Checks out a floating license seat. |
64
+ | `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
65
+ | `floating_checkin` | Checks in a session, releasing the seat. |
63
66
 
64
67
  ### Customer Management
65
68
 
@@ -73,6 +76,22 @@ Keymint provides utilities to uniquely identify machines for node-locking:
73
76
  | `toggle_customer_status`| Toggles customer active status. |
74
77
  | `delete_customer` | Permanently deletes a customer and their keys. |
75
78
 
79
+ ### Webhook Verification
80
+
81
+ | Method | Description |
82
+ |-------------------------|--------------------------------------------------|
83
+ | `verify_webhook_signature`| Verifies the signature of a webhook request payload. |
84
+
85
+ ## Idempotency
86
+
87
+ All mutating SDK methods support idempotency keys to safely retry requests in case of network drops. Pass a unique string (recommended: UUID v4) as the optional `idempotency_key` keyword argument:
88
+
89
+ ```python
90
+ result = sdk.create_key({
91
+ 'productId': product_id,
92
+ }, idempotency_key='9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d')
93
+ ```
94
+
76
95
  ## License
77
96
  MIT
78
97
 
@@ -16,17 +16,21 @@ class KeyMint:
16
16
  'Content-Type': 'application/json'
17
17
  }
18
18
 
19
- def _handle_request(self, method: str, endpoint: str, params: dict = None, query_params: dict = None):
19
+ def _handle_request(self, method: str, endpoint: str, params: dict = None, query_params: dict = None, idempotency_key: str = None):
20
20
  url = f'{self.base_url}{endpoint}'
21
+ headers = self.headers.copy()
22
+ if idempotency_key:
23
+ headers['Idempotency-Key'] = idempotency_key
24
+
21
25
  try:
22
26
  if method.upper() == 'GET':
23
- response = requests.get(url, params=query_params, headers=self.headers)
27
+ response = requests.get(url, params=query_params, headers=headers)
24
28
  elif method.upper() == 'POST':
25
- response = requests.post(url, json=params, headers=self.headers)
29
+ response = requests.post(url, json=params, headers=headers)
26
30
  elif method.upper() == 'PUT':
27
- response = requests.put(url, json=params, headers=self.headers)
31
+ response = requests.put(url, json=params, headers=headers)
28
32
  elif method.upper() == 'DELETE':
29
- response = requests.delete(url, params=query_params, headers=self.headers)
33
+ response = requests.delete(url, params=query_params, headers=headers)
30
34
  else:
31
35
  raise ValueError(f"Unsupported HTTP method: {method}")
32
36
 
@@ -49,15 +53,16 @@ class KeyMint:
49
53
  except Exception as err:
50
54
  raise KeyMintApiError(message=str(err), code=-1)
51
55
 
52
- def create_key(self, params: CreateKeyParams) -> CreateKeyResponse:
56
+ def create_key(self, params: CreateKeyParams, idempotency_key: str = None) -> CreateKeyResponse:
53
57
  """
54
58
  Creates a new license key.
55
59
  :param params: Parameters for creating the key.
60
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
56
61
  :returns: The created key information.
57
62
  """
58
- return self._handle_request('POST', '/key', params)
63
+ return self._handle_request('POST', '/key', params, idempotency_key=idempotency_key)
59
64
 
60
- def activate_key(self, params: ActivateKeyParams) -> ActivateKeyResponse:
65
+ def activate_key(self, params: ActivateKeyParams, idempotency_key: str = None) -> ActivateKeyResponse:
61
66
  """
62
67
  Activates a license key for a specific device.
63
68
 
@@ -67,41 +72,46 @@ class KeyMint:
67
72
  MUST cache the validation result locally.
68
73
 
69
74
  :param params: Activation parameters including productId, licenseKey, and optional hostId.
75
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
70
76
  :return: Activation success message and licensee information.
71
77
  """
72
- return self._handle_request('POST', '/key/activate', params)
78
+ return self._handle_request('POST', '/key/activate', params, idempotency_key=idempotency_key)
73
79
 
74
- def deactivate_key(self, params: DeactivateKeyParams) -> DeactivateKeyResponse:
80
+ def deactivate_key(self, params: DeactivateKeyParams, idempotency_key: str = None) -> DeactivateKeyResponse:
75
81
  """
76
82
  Deactivates a device from a license key.
77
83
  :param params: Parameters for deactivating the key.
84
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
78
85
  :returns: The deactivation confirmation.
79
86
  """
80
- return self._handle_request('POST', '/key/deactivate', params)
87
+ return self._handle_request('POST', '/key/deactivate', params, idempotency_key=idempotency_key)
81
88
 
82
- def floating_checkout(self, params: FloatingCheckoutParams) -> FloatingCheckoutResponse:
89
+ def floating_checkout(self, params: FloatingCheckoutParams, idempotency_key: str = None) -> FloatingCheckoutResponse:
83
90
  """
84
91
  Checks out a floating license seat.
85
92
  :param params: Parameters for checking out the license.
93
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
86
94
  :returns: The checkout response containing sessionId and sessionSecret.
87
95
  """
88
- return self._handle_request('POST', '/key/checkout', params)
96
+ return self._handle_request('POST', '/key/checkout', params, idempotency_key=idempotency_key)
89
97
 
90
- def floating_heartbeat(self, params: FloatingHeartbeatParams) -> FloatingHeartbeatResponse:
98
+ def floating_heartbeat(self, params: FloatingHeartbeatParams, idempotency_key: str = None) -> FloatingHeartbeatResponse:
91
99
  """
92
100
  Sends a heartbeat to keep a floating license session alive.
93
101
  :param params: Parameters for the heartbeat (includes rotating signature).
102
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
94
103
  :returns: The heartbeat response with extended expiry and new nonce.
95
104
  """
96
- return self._handle_request('POST', '/key/heartbeat', params)
105
+ return self._handle_request('POST', '/key/heartbeat', params, idempotency_key=idempotency_key)
97
106
 
98
- def floating_checkin(self, params: FloatingCheckinParams) -> FloatingCheckinResponse:
107
+ def floating_checkin(self, params: FloatingCheckinParams, idempotency_key: str = None) -> FloatingCheckinResponse:
99
108
  """
100
109
  Checks in a floating license session, releasing the seat.
101
110
  :param params: Parameters for checking in the license (includes rotating signature).
111
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
102
112
  :returns: The checkin confirmation.
103
113
  """
104
- return self._handle_request('POST', '/key/checkin', params)
114
+ return self._handle_request('POST', '/key/checkin', params, idempotency_key=idempotency_key)
105
115
 
106
116
  def get_key(self, params: GetKeyParams) -> GetKeyResponse:
107
117
  """
@@ -115,31 +125,34 @@ class KeyMint:
115
125
  }
116
126
  return self._handle_request('GET', '/key', query_params=query_params)
117
127
 
118
- def block_key(self, params: BlockKeyParams) -> BlockKeyResponse:
128
+ def block_key(self, params: BlockKeyParams, idempotency_key: str = None) -> BlockKeyResponse:
119
129
  """
120
130
  Blocks a specific license key.
121
131
  :param params: Parameters for blocking the key.
132
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
122
133
  :returns: The block confirmation.
123
134
  """
124
- return self._handle_request('POST', '/key/block', params)
135
+ return self._handle_request('POST', '/key/block', params, idempotency_key=idempotency_key)
125
136
 
126
- def unblock_key(self, params: UnblockKeyParams) -> UnblockKeyResponse:
137
+ def unblock_key(self, params: UnblockKeyParams, idempotency_key: str = None) -> UnblockKeyResponse:
127
138
  """
128
139
  Unblocks a previously blocked license key.
129
140
  :param params: Parameters for unblocking the key.
141
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
130
142
  :returns: The unblock confirmation.
131
143
  """
132
- return self._handle_request('POST', '/key/unblock', params)
144
+ return self._handle_request('POST', '/key/unblock', params, idempotency_key=idempotency_key)
133
145
 
134
146
  # Customer Management Methods
135
147
 
136
- def create_customer(self, params: CreateCustomerParams) -> CreateCustomerResponse:
148
+ def create_customer(self, params: CreateCustomerParams, idempotency_key: str = None) -> CreateCustomerResponse:
137
149
  """
138
150
  Creates a new customer.
139
151
  :param params: Parameters for creating the customer.
152
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
140
153
  :returns: The created customer information.
141
154
  """
142
- return self._handle_request('POST', '/customer', params)
155
+ return self._handle_request('POST', '/customer', params, idempotency_key=idempotency_key)
143
156
 
144
157
  def get_all_customers(self, params: Optional[GetAllCustomersParams] = None) -> GetAllCustomersResponse:
145
158
  """
@@ -158,22 +171,24 @@ class KeyMint:
158
171
  query_params = {'customerId': params['customerId']}
159
172
  return self._handle_request('GET', '/customer/by-id', query_params=query_params)
160
173
 
161
- def update_customer(self, params: UpdateCustomerParams) -> UpdateCustomerResponse:
174
+ def update_customer(self, params: UpdateCustomerParams, idempotency_key: str = None) -> UpdateCustomerResponse:
162
175
  """
163
176
  Updates an existing customer's information.
164
177
  :param params: Parameters for updating the customer.
178
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
165
179
  :returns: The update confirmation.
166
180
  """
167
- return self._handle_request('PUT', '/customer/by-id', params)
181
+ return self._handle_request('PUT', '/customer/by-id', params, idempotency_key=idempotency_key)
168
182
 
169
- def delete_customer(self, params: DeleteCustomerParams) -> DeleteCustomerResponse:
183
+ def delete_customer(self, params: DeleteCustomerParams, idempotency_key: str = None) -> DeleteCustomerResponse:
170
184
  """
171
185
  Permanently deletes a customer and all associated license keys.
172
186
  :param params: Parameters containing the customer ID.
187
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
173
188
  :returns: The deletion confirmation.
174
189
  """
175
190
  query_params = {'customerId': params['customerId']}
176
- return self._handle_request('DELETE', '/customer/by-id', query_params=query_params)
191
+ return self._handle_request('DELETE', '/customer/by-id', query_params=query_params, idempotency_key=idempotency_key)
177
192
 
178
193
  def get_customer_with_keys(self, params: GetCustomerWithKeysParams) -> GetCustomerWithKeysResponse:
179
194
  """
@@ -184,12 +199,70 @@ class KeyMint:
184
199
  query_params = {'customerId': params['customerId']}
185
200
  return self._handle_request('GET', '/customer/keys', query_params=query_params)
186
201
 
187
- def toggle_customer_status(self, params: ToggleCustomerStatusParams) -> ToggleCustomerStatusResponse:
202
+ def toggle_customer_status(self, params: ToggleCustomerStatusParams, idempotency_key: str = None) -> ToggleCustomerStatusResponse:
188
203
  """
189
204
  Toggles the active status of a customer account (disable or enable).
190
205
  :param params: Parameters containing the customer ID.
206
+ :param idempotency_key: Optional unique identifier to ensure request idempotency.
191
207
  :returns: The status toggle confirmation.
192
208
  """
193
209
  query_params = {'customerId': params['customerId']}
194
- return self._handle_request('POST', '/customer/disable', params=None, query_params=query_params)
210
+ return self._handle_request('POST', '/customer/disable', params=None, query_params=query_params, idempotency_key=idempotency_key)
211
+
212
+ @staticmethod
213
+ def verify_webhook_signature(payload: str, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
214
+ """
215
+ Verifies a webhook payload signature received from Keymint.
216
+ :param payload: The raw request body as a string.
217
+ :param header: The value of the "Keymint-Signature" header.
218
+ :param secret: The webhook endpoint's signing secret.
219
+ :param tolerance_seconds: Time tolerance in seconds to prevent replay attacks. Defaults to 300 (5 minutes).
220
+ :returns: True if the signature is valid, False otherwise.
221
+ """
222
+ import hmac
223
+ import hashlib
224
+ import time
225
+
226
+ if not header or not secret:
227
+ return False
228
+
229
+ try:
230
+ # Parse header (e.g. t=1719374021,v1=signature)
231
+ timestamp_str = ""
232
+ signature = ""
233
+ parts = header.split(",")
234
+ for part in parts:
235
+ kv = part.strip().split("=", 1)
236
+ if len(kv) == 2:
237
+ if kv[0] == "t":
238
+ timestamp_str = kv[1]
239
+ elif kv[0] == "v1":
240
+ signature = kv[1]
241
+
242
+ if not timestamp_str or not signature:
243
+ return False
244
+
245
+ # Check timestamp validity
246
+ try:
247
+ timestamp_int = int(timestamp_str)
248
+ except ValueError:
249
+ return False
250
+
251
+ now = int(time.time())
252
+ if abs(now - timestamp_int) > tolerance_seconds:
253
+ return False
254
+
255
+ # Verify HMAC signature
256
+ signable_content = f"{timestamp_str}.{payload}".encode("utf-8")
257
+ expected_signature = hmac.new(
258
+ secret.encode("utf-8"),
259
+ signable_content,
260
+ hashlib.sha256
261
+ ).hexdigest()
262
+
263
+ # Constant-time comparison to prevent timing attacks
264
+ return hmac.compare_digest(expected_signature, signature)
265
+ except Exception:
266
+ return False
267
+
195
268
 
@@ -1,6 +1,6 @@
1
1
  """KeyMint Python SDK version information."""
2
2
 
3
- __version__ = "2.1.1"
3
+ __version__ = "2.3.0"
4
4
  __author__ = "KeyMint"
5
5
  __email__ = "cliff@keymint.dev"
6
6
  __url__ = "https://github.com/keymint-dev/keymint-python"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keymint
3
- Version: 2.1.1
3
+ Version: 2.3.0
4
4
  Summary: Official Python SDK for KeyMint license management with comprehensive API coverage.
5
5
  Home-page: https://github.com/keymint-dev/keymint-python
6
6
  Author: KeyMint
@@ -70,13 +70,13 @@ pip install keymint
70
70
  import os
71
71
  from keymint import KeyMint, identity
72
72
 
73
- access_token = os.environ.get('KEYMINT_ACCESS_TOKEN')
73
+ api_key = os.environ.get('KEYMINT_API_KEY')
74
74
  product_id = os.environ.get('KEYMINT_PRODUCT_ID')
75
75
 
76
- if not access_token or not product_id:
77
- raise ValueError('Please set the KEYMINT_ACCESS_TOKEN and KEYMINT_PRODUCT_ID environment variables.')
76
+ if not api_key or not product_id:
77
+ raise ValueError('Please set the KEYMINT_API_KEY and KEYMINT_PRODUCT_ID environment variables.')
78
78
 
79
- sdk = KeyMint(access_token)
79
+ sdk = KeyMint(api_key)
80
80
 
81
81
  # 1. Get a stable, unique ID for this machine
82
82
  host_id = identity.get_or_create_installation_id()
@@ -109,6 +109,9 @@ Keymint provides utilities to uniquely identify machines for node-locking:
109
109
  | `get_key` | Retrieves detailed information about a key. |
110
110
  | `block_key` | Blocks a license key. |
111
111
  | `unblock_key` | Unblocks a previously blocked license key. |
112
+ | `floating_checkout` | Checks out a floating license seat. |
113
+ | `floating_heartbeat`| Sends a heartbeat to keep a session alive. |
114
+ | `floating_checkin` | Checks in a session, releasing the seat. |
112
115
 
113
116
  ### Customer Management
114
117
 
@@ -122,6 +125,22 @@ Keymint provides utilities to uniquely identify machines for node-locking:
122
125
  | `toggle_customer_status`| Toggles customer active status. |
123
126
  | `delete_customer` | Permanently deletes a customer and their keys. |
124
127
 
128
+ ### Webhook Verification
129
+
130
+ | Method | Description |
131
+ |-------------------------|--------------------------------------------------|
132
+ | `verify_webhook_signature`| Verifies the signature of a webhook request payload. |
133
+
134
+ ## Idempotency
135
+
136
+ All mutating SDK methods support idempotency keys to safely retry requests in case of network drops. Pass a unique string (recommended: UUID v4) as the optional `idempotency_key` keyword argument:
137
+
138
+ ```python
139
+ result = sdk.create_key({
140
+ 'productId': product_id,
141
+ }, idempotency_key='9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d')
142
+ ```
143
+
125
144
  ## License
126
145
  MIT
127
146
 
@@ -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.1.1",
8
+ version="2.3.0",
9
9
  author="KeyMint",
10
10
  author_email="cliff@keymint.dev",
11
11
  description="Official Python SDK for KeyMint license management with comprehensive API coverage.",
File without changes
File without changes
File without changes
File without changes
File without changes