Skip to content

Commit ec9dfe4

Browse files
committed
feat: add support for request idempotency keys
1 parent 4b2d29c commit ec9dfe4

4 files changed

Lines changed: 57 additions & 31 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ Keymint provides utilities to uniquely identify machines for node-locking:
8282
|-------------------------|--------------------------------------------------|
8383
| `verify_webhook_signature`| Verifies the signature of a webhook request payload. |
8484

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+
8595
## License
8696
MIT
8797

keymint/__init__.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ def __init__(self, api_key: str, base_url: str = "https://api.keymint.dev"):
1616
'Content-Type': 'application/json'
1717
}
1818

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):
2020
url = f'{self.base_url}{endpoint}'
21+
headers = self.headers.copy()
22+
if idempotency_key:
23+
headers['Idempotency-Key'] = idempotency_key
24+
2125
try:
2226
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)
2428
elif method.upper() == 'POST':
25-
response = requests.post(url, json=params, headers=self.headers)
29+
response = requests.post(url, json=params, headers=headers)
2630
elif method.upper() == 'PUT':
27-
response = requests.put(url, json=params, headers=self.headers)
31+
response = requests.put(url, json=params, headers=headers)
2832
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)
3034
else:
3135
raise ValueError(f"Unsupported HTTP method: {method}")
3236

@@ -49,15 +53,16 @@ def _handle_request(self, method: str, endpoint: str, params: dict = None, query
4953
except Exception as err:
5054
raise KeyMintApiError(message=str(err), code=-1)
5155

52-
def create_key(self, params: CreateKeyParams) -> CreateKeyResponse:
56+
def create_key(self, params: CreateKeyParams, idempotency_key: str = None) -> CreateKeyResponse:
5357
"""
5458
Creates a new license key.
5559
:param params: Parameters for creating the key.
60+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
5661
:returns: The created key information.
5762
"""
58-
return self._handle_request('POST', '/key', params)
63+
return self._handle_request('POST', '/key', params, idempotency_key=idempotency_key)
5964

60-
def activate_key(self, params: ActivateKeyParams) -> ActivateKeyResponse:
65+
def activate_key(self, params: ActivateKeyParams, idempotency_key: str = None) -> ActivateKeyResponse:
6166
"""
6267
Activates a license key for a specific device.
6368
@@ -67,41 +72,46 @@ def activate_key(self, params: ActivateKeyParams) -> ActivateKeyResponse:
6772
MUST cache the validation result locally.
6873
6974
:param params: Activation parameters including productId, licenseKey, and optional hostId.
75+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
7076
:return: Activation success message and licensee information.
7177
"""
72-
return self._handle_request('POST', '/key/activate', params)
78+
return self._handle_request('POST', '/key/activate', params, idempotency_key=idempotency_key)
7379

74-
def deactivate_key(self, params: DeactivateKeyParams) -> DeactivateKeyResponse:
80+
def deactivate_key(self, params: DeactivateKeyParams, idempotency_key: str = None) -> DeactivateKeyResponse:
7581
"""
7682
Deactivates a device from a license key.
7783
:param params: Parameters for deactivating the key.
84+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
7885
:returns: The deactivation confirmation.
7986
"""
80-
return self._handle_request('POST', '/key/deactivate', params)
87+
return self._handle_request('POST', '/key/deactivate', params, idempotency_key=idempotency_key)
8188

82-
def floating_checkout(self, params: FloatingCheckoutParams) -> FloatingCheckoutResponse:
89+
def floating_checkout(self, params: FloatingCheckoutParams, idempotency_key: str = None) -> FloatingCheckoutResponse:
8390
"""
8491
Checks out a floating license seat.
8592
:param params: Parameters for checking out the license.
93+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
8694
:returns: The checkout response containing sessionId and sessionSecret.
8795
"""
88-
return self._handle_request('POST', '/key/checkout', params)
96+
return self._handle_request('POST', '/key/checkout', params, idempotency_key=idempotency_key)
8997

90-
def floating_heartbeat(self, params: FloatingHeartbeatParams) -> FloatingHeartbeatResponse:
98+
def floating_heartbeat(self, params: FloatingHeartbeatParams, idempotency_key: str = None) -> FloatingHeartbeatResponse:
9199
"""
92100
Sends a heartbeat to keep a floating license session alive.
93101
:param params: Parameters for the heartbeat (includes rotating signature).
102+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
94103
:returns: The heartbeat response with extended expiry and new nonce.
95104
"""
96-
return self._handle_request('POST', '/key/heartbeat', params)
105+
return self._handle_request('POST', '/key/heartbeat', params, idempotency_key=idempotency_key)
97106

98-
def floating_checkin(self, params: FloatingCheckinParams) -> FloatingCheckinResponse:
107+
def floating_checkin(self, params: FloatingCheckinParams, idempotency_key: str = None) -> FloatingCheckinResponse:
99108
"""
100109
Checks in a floating license session, releasing the seat.
101110
:param params: Parameters for checking in the license (includes rotating signature).
111+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
102112
:returns: The checkin confirmation.
103113
"""
104-
return self._handle_request('POST', '/key/checkin', params)
114+
return self._handle_request('POST', '/key/checkin', params, idempotency_key=idempotency_key)
105115

106116
def get_key(self, params: GetKeyParams) -> GetKeyResponse:
107117
"""
@@ -115,31 +125,34 @@ def get_key(self, params: GetKeyParams) -> GetKeyResponse:
115125
}
116126
return self._handle_request('GET', '/key', query_params=query_params)
117127

118-
def block_key(self, params: BlockKeyParams) -> BlockKeyResponse:
128+
def block_key(self, params: BlockKeyParams, idempotency_key: str = None) -> BlockKeyResponse:
119129
"""
120130
Blocks a specific license key.
121131
:param params: Parameters for blocking the key.
132+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
122133
:returns: The block confirmation.
123134
"""
124-
return self._handle_request('POST', '/key/block', params)
135+
return self._handle_request('POST', '/key/block', params, idempotency_key=idempotency_key)
125136

126-
def unblock_key(self, params: UnblockKeyParams) -> UnblockKeyResponse:
137+
def unblock_key(self, params: UnblockKeyParams, idempotency_key: str = None) -> UnblockKeyResponse:
127138
"""
128139
Unblocks a previously blocked license key.
129140
:param params: Parameters for unblocking the key.
141+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
130142
:returns: The unblock confirmation.
131143
"""
132-
return self._handle_request('POST', '/key/unblock', params)
144+
return self._handle_request('POST', '/key/unblock', params, idempotency_key=idempotency_key)
133145

134146
# Customer Management Methods
135147

136-
def create_customer(self, params: CreateCustomerParams) -> CreateCustomerResponse:
148+
def create_customer(self, params: CreateCustomerParams, idempotency_key: str = None) -> CreateCustomerResponse:
137149
"""
138150
Creates a new customer.
139151
:param params: Parameters for creating the customer.
152+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
140153
:returns: The created customer information.
141154
"""
142-
return self._handle_request('POST', '/customer', params)
155+
return self._handle_request('POST', '/customer', params, idempotency_key=idempotency_key)
143156

144157
def get_all_customers(self, params: Optional[GetAllCustomersParams] = None) -> GetAllCustomersResponse:
145158
"""
@@ -158,22 +171,24 @@ def get_customer_by_id(self, params: GetCustomerByIdParams) -> GetCustomerByIdRe
158171
query_params = {'customerId': params['customerId']}
159172
return self._handle_request('GET', '/customer/by-id', query_params=query_params)
160173

161-
def update_customer(self, params: UpdateCustomerParams) -> UpdateCustomerResponse:
174+
def update_customer(self, params: UpdateCustomerParams, idempotency_key: str = None) -> UpdateCustomerResponse:
162175
"""
163176
Updates an existing customer's information.
164177
:param params: Parameters for updating the customer.
178+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
165179
:returns: The update confirmation.
166180
"""
167-
return self._handle_request('PUT', '/customer/by-id', params)
181+
return self._handle_request('PUT', '/customer/by-id', params, idempotency_key=idempotency_key)
168182

169-
def delete_customer(self, params: DeleteCustomerParams) -> DeleteCustomerResponse:
183+
def delete_customer(self, params: DeleteCustomerParams, idempotency_key: str = None) -> DeleteCustomerResponse:
170184
"""
171185
Permanently deletes a customer and all associated license keys.
172186
:param params: Parameters containing the customer ID.
187+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
173188
:returns: The deletion confirmation.
174189
"""
175190
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)
177192

178193
def get_customer_with_keys(self, params: GetCustomerWithKeysParams) -> GetCustomerWithKeysResponse:
179194
"""
@@ -184,14 +199,15 @@ def get_customer_with_keys(self, params: GetCustomerWithKeysParams) -> GetCustom
184199
query_params = {'customerId': params['customerId']}
185200
return self._handle_request('GET', '/customer/keys', query_params=query_params)
186201

187-
def toggle_customer_status(self, params: ToggleCustomerStatusParams) -> ToggleCustomerStatusResponse:
202+
def toggle_customer_status(self, params: ToggleCustomerStatusParams, idempotency_key: str = None) -> ToggleCustomerStatusResponse:
188203
"""
189204
Toggles the active status of a customer account (disable or enable).
190205
:param params: Parameters containing the customer ID.
206+
:param idempotency_key: Optional unique identifier to ensure request idempotency.
191207
:returns: The status toggle confirmation.
192208
"""
193209
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)
195211

196212
@staticmethod
197213
def verify_webhook_signature(payload: str, header: str, secret: str, tolerance_seconds: int = 300) -> bool:

keymint/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""KeyMint Python SDK version information."""
22

3-
__version__ = "2.2.0"
3+
__version__ = "2.3.0"
44
__author__ = "KeyMint"
55
__email__ = "cliff@keymint.dev"
66
__url__ = "https://github.com/keymint-dev/keymint-python"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="keymint",
8-
version="2.2.0",
8+
version="2.3.0",
99
author="KeyMint",
1010
author_email="cliff@keymint.dev",
1111
description="Official Python SDK for KeyMint license management with comprehensive API coverage.",

0 commit comments

Comments
 (0)