accessgrid 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AccessGrid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.2
2
+ Name: accessgrid
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the AccessGrid API
5
+ Home-page: https://github.com/yourusername/accessgrid-python
6
+ Author: Your Name
7
+ Author-email: your.email@example.com
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.7
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests>=2.25.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
24
+ Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
26
+ Requires-Dist: black>=22.3.0; extra == "dev"
27
+ Requires-Dist: isort>=5.10.1; extra == "dev"
28
+ Requires-Dist: flake8>=4.0.1; extra == "dev"
29
+ Requires-Dist: mypy>=0.981; extra == "dev"
30
+ Requires-Dist: build>=0.10.0; extra == "dev"
31
+ Requires-Dist: twine>=4.0.2; extra == "dev"
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: classifier
35
+ Dynamic: description
36
+ Dynamic: description-content-type
37
+ Dynamic: home-page
38
+ Dynamic: provides-extra
39
+ Dynamic: requires-dist
40
+ Dynamic: requires-python
41
+ Dynamic: summary
42
+
43
+ # AccessGrid SDK
44
+
45
+ A JavaScript SDK for interacting with the [AccessGrid.com](https://www.accessgrid.com) API. This SDK provides a simple interface for managing NFC key cards and enterprise templates. Full docs at https://www.accessgrid.com/docs
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ npm install accessgrid
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```javascript
56
+ import AccessGrid from 'accessgrid';
57
+
58
+ const accountId = process.env.ACCOUNT_ID;
59
+ const secretKey = process.env.SECRET_KEY;
60
+
61
+ const client = new AccessGrid(accountId, secretKey);
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ### Access Cards
67
+
68
+ #### Provision a new card
69
+
70
+ ```javascript
71
+ const card = await client.accessCards.provision({
72
+ cardTemplateId: "0xd3adb00b5",
73
+ employeeId: "123456789",
74
+ tagId: "DDEADB33FB00B5",
75
+ allowOnMultipleDevices: true,
76
+ fullName: "Employee name",
77
+ email: "employee@yourwebsite.com",
78
+ phoneNumber: "+19547212241",
79
+ classification: "full_time",
80
+ startDate: "2025-01-31T22:46:25.601Z",
81
+ expirationDate: "2025-04-30T22:46:25.601Z",
82
+ employeePhoto: "[image_in_base64_encoded_format]"
83
+ });
84
+ ```
85
+
86
+ #### Update a card
87
+
88
+ ```javascript
89
+ const card = await client.accessCards.update({
90
+ cardId: "0xc4rd1d",
91
+ employeeId: "987654321",
92
+ fullName: "Updated Employee Name",
93
+ classification: "contractor",
94
+ expirationDate: "2025-02-22T21:04:03.664Z",
95
+ employeePhoto: "[image_in_base64_encoded_format]"
96
+ });
97
+ ```
98
+
99
+ #### Manage card states
100
+
101
+ ```javascript
102
+ // Suspend a card
103
+ await client.accessCards.suspend({
104
+ cardId: "0xc4rd1d"
105
+ });
106
+
107
+ // Resume a card
108
+ await client.accessCards.resume({
109
+ cardId: "0xc4rd1d"
110
+ });
111
+
112
+ // Unlink a card
113
+ await client.accessCards.unlink({
114
+ cardId: "0xc4rd1d"
115
+ });
116
+ ```
117
+
118
+ ### Enterprise Console
119
+
120
+ #### Create a template
121
+
122
+ ```javascript
123
+ const template = await client.console.createTemplate({
124
+ name: "Employee NFC key",
125
+ platform: "apple",
126
+ useCase: "employee_badge",
127
+ protocol: "desfire",
128
+ allowOnMultipleDevices: true,
129
+ watchCount: 2,
130
+ iphoneCount: 3,
131
+ design: {
132
+ backgroundColor: "#FFFFFF",
133
+ labelColor: "#000000",
134
+ labelSecondaryColor: "#333333",
135
+ backgroundImage: "[image_in_base64_encoded_format]",
136
+ logoImage: "[image_in_base64_encoded_format]",
137
+ iconImage: "[image_in_base64_encoded_format]"
138
+ },
139
+ supportInfo: {
140
+ supportUrl: "https://help.yourcompany.com",
141
+ supportPhoneNumber: "+1-555-123-4567",
142
+ supportEmail: "support@yourcompany.com",
143
+ privacyPolicyUrl: "https://yourcompany.com/privacy",
144
+ termsAndConditionsUrl: "https://yourcompany.com/terms"
145
+ }
146
+ });
147
+ ```
148
+
149
+ #### Update a template
150
+
151
+ ```javascript
152
+ const template = await client.console.updateTemplate({
153
+ cardTemplateId: "0xd3adb00b5",
154
+ name: "Updated Employee NFC key",
155
+ allowOnMultipleDevices: true,
156
+ watchCount: 2,
157
+ iphoneCount: 3,
158
+ supportInfo: {
159
+ supportUrl: "https://help.yourcompany.com",
160
+ supportPhoneNumber: "+1-555-123-4567",
161
+ supportEmail: "support@yourcompany.com",
162
+ privacyPolicyUrl: "https://yourcompany.com/privacy",
163
+ termsAndConditionsUrl: "https://yourcompany.com/terms"
164
+ }
165
+ });
166
+ ```
167
+
168
+ #### Read a template
169
+
170
+ ```javascript
171
+ const template = await client.console.readTemplate({
172
+ cardTemplateId: "0xd3adb00b5"
173
+ });
174
+ ```
175
+
176
+ #### Get event logs
177
+
178
+ ```javascript
179
+ const events = await client.console.eventLog({
180
+ cardTemplateId: "0xd3adb00b5",
181
+ filters: {
182
+ device: "mobile", // "mobile" or "watch"
183
+ startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
184
+ endDate: new Date().toISOString(),
185
+ eventType: "install"
186
+ }
187
+ });
188
+ ```
189
+
190
+ ## Configuration
191
+
192
+ The SDK can be configured with custom options:
193
+
194
+ ```javascript
195
+ const client = new AccessGrid(accountId, secretKey, {
196
+ baseUrl: 'https://api.staging.accessgrid.com' // Use a different API endpoint
197
+ });
198
+ ```
199
+
200
+ ## Error Handling
201
+
202
+ The SDK throws errors for various scenarios including:
203
+ - Missing required credentials
204
+ - API request failures
205
+ - Invalid parameters
206
+ - Server errors
207
+
208
+ Example error handling:
209
+
210
+ ```javascript
211
+ try {
212
+ const card = await client.accessCards.provision({
213
+ // ... parameters
214
+ });
215
+ } catch (error) {
216
+ console.error('Failed to provision card:', error.message);
217
+ }
218
+ ```
219
+
220
+ ## Requirements
221
+
222
+ - Node.js 12 or higher
223
+ - Modern browser environment with support for:
224
+ - Fetch API
225
+ - Web Crypto API
226
+ - Promises
227
+ - async/await
228
+
229
+ ## Security
230
+
231
+ The SDK automatically handles:
232
+ - Request signing using HMAC-SHA256
233
+ - Secure payload encoding
234
+ - Authentication headers
235
+ - HTTPS communication
236
+
237
+ Never expose your `secretKey` in client-side code. Always use environment variables or a secure configuration management system.
238
+
239
+ ## License
240
+
241
+ MIT License - See LICENSE file for details.
@@ -0,0 +1,199 @@
1
+ # AccessGrid SDK
2
+
3
+ A JavaScript SDK for interacting with the [AccessGrid.com](https://www.accessgrid.com) API. This SDK provides a simple interface for managing NFC key cards and enterprise templates. Full docs at https://www.accessgrid.com/docs
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install accessgrid
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ import AccessGrid from 'accessgrid';
15
+
16
+ const accountId = process.env.ACCOUNT_ID;
17
+ const secretKey = process.env.SECRET_KEY;
18
+
19
+ const client = new AccessGrid(accountId, secretKey);
20
+ ```
21
+
22
+ ## API Reference
23
+
24
+ ### Access Cards
25
+
26
+ #### Provision a new card
27
+
28
+ ```javascript
29
+ const card = await client.accessCards.provision({
30
+ cardTemplateId: "0xd3adb00b5",
31
+ employeeId: "123456789",
32
+ tagId: "DDEADB33FB00B5",
33
+ allowOnMultipleDevices: true,
34
+ fullName: "Employee name",
35
+ email: "employee@yourwebsite.com",
36
+ phoneNumber: "+19547212241",
37
+ classification: "full_time",
38
+ startDate: "2025-01-31T22:46:25.601Z",
39
+ expirationDate: "2025-04-30T22:46:25.601Z",
40
+ employeePhoto: "[image_in_base64_encoded_format]"
41
+ });
42
+ ```
43
+
44
+ #### Update a card
45
+
46
+ ```javascript
47
+ const card = await client.accessCards.update({
48
+ cardId: "0xc4rd1d",
49
+ employeeId: "987654321",
50
+ fullName: "Updated Employee Name",
51
+ classification: "contractor",
52
+ expirationDate: "2025-02-22T21:04:03.664Z",
53
+ employeePhoto: "[image_in_base64_encoded_format]"
54
+ });
55
+ ```
56
+
57
+ #### Manage card states
58
+
59
+ ```javascript
60
+ // Suspend a card
61
+ await client.accessCards.suspend({
62
+ cardId: "0xc4rd1d"
63
+ });
64
+
65
+ // Resume a card
66
+ await client.accessCards.resume({
67
+ cardId: "0xc4rd1d"
68
+ });
69
+
70
+ // Unlink a card
71
+ await client.accessCards.unlink({
72
+ cardId: "0xc4rd1d"
73
+ });
74
+ ```
75
+
76
+ ### Enterprise Console
77
+
78
+ #### Create a template
79
+
80
+ ```javascript
81
+ const template = await client.console.createTemplate({
82
+ name: "Employee NFC key",
83
+ platform: "apple",
84
+ useCase: "employee_badge",
85
+ protocol: "desfire",
86
+ allowOnMultipleDevices: true,
87
+ watchCount: 2,
88
+ iphoneCount: 3,
89
+ design: {
90
+ backgroundColor: "#FFFFFF",
91
+ labelColor: "#000000",
92
+ labelSecondaryColor: "#333333",
93
+ backgroundImage: "[image_in_base64_encoded_format]",
94
+ logoImage: "[image_in_base64_encoded_format]",
95
+ iconImage: "[image_in_base64_encoded_format]"
96
+ },
97
+ supportInfo: {
98
+ supportUrl: "https://help.yourcompany.com",
99
+ supportPhoneNumber: "+1-555-123-4567",
100
+ supportEmail: "support@yourcompany.com",
101
+ privacyPolicyUrl: "https://yourcompany.com/privacy",
102
+ termsAndConditionsUrl: "https://yourcompany.com/terms"
103
+ }
104
+ });
105
+ ```
106
+
107
+ #### Update a template
108
+
109
+ ```javascript
110
+ const template = await client.console.updateTemplate({
111
+ cardTemplateId: "0xd3adb00b5",
112
+ name: "Updated Employee NFC key",
113
+ allowOnMultipleDevices: true,
114
+ watchCount: 2,
115
+ iphoneCount: 3,
116
+ supportInfo: {
117
+ supportUrl: "https://help.yourcompany.com",
118
+ supportPhoneNumber: "+1-555-123-4567",
119
+ supportEmail: "support@yourcompany.com",
120
+ privacyPolicyUrl: "https://yourcompany.com/privacy",
121
+ termsAndConditionsUrl: "https://yourcompany.com/terms"
122
+ }
123
+ });
124
+ ```
125
+
126
+ #### Read a template
127
+
128
+ ```javascript
129
+ const template = await client.console.readTemplate({
130
+ cardTemplateId: "0xd3adb00b5"
131
+ });
132
+ ```
133
+
134
+ #### Get event logs
135
+
136
+ ```javascript
137
+ const events = await client.console.eventLog({
138
+ cardTemplateId: "0xd3adb00b5",
139
+ filters: {
140
+ device: "mobile", // "mobile" or "watch"
141
+ startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
142
+ endDate: new Date().toISOString(),
143
+ eventType: "install"
144
+ }
145
+ });
146
+ ```
147
+
148
+ ## Configuration
149
+
150
+ The SDK can be configured with custom options:
151
+
152
+ ```javascript
153
+ const client = new AccessGrid(accountId, secretKey, {
154
+ baseUrl: 'https://api.staging.accessgrid.com' // Use a different API endpoint
155
+ });
156
+ ```
157
+
158
+ ## Error Handling
159
+
160
+ The SDK throws errors for various scenarios including:
161
+ - Missing required credentials
162
+ - API request failures
163
+ - Invalid parameters
164
+ - Server errors
165
+
166
+ Example error handling:
167
+
168
+ ```javascript
169
+ try {
170
+ const card = await client.accessCards.provision({
171
+ // ... parameters
172
+ });
173
+ } catch (error) {
174
+ console.error('Failed to provision card:', error.message);
175
+ }
176
+ ```
177
+
178
+ ## Requirements
179
+
180
+ - Node.js 12 or higher
181
+ - Modern browser environment with support for:
182
+ - Fetch API
183
+ - Web Crypto API
184
+ - Promises
185
+ - async/await
186
+
187
+ ## Security
188
+
189
+ The SDK automatically handles:
190
+ - Request signing using HMAC-SHA256
191
+ - Secure payload encoding
192
+ - Authentication headers
193
+ - HTTPS communication
194
+
195
+ Never expose your `secretKey` in client-side code. Always use environment variables or a secure configuration management system.
196
+
197
+ ## License
198
+
199
+ MIT License - See LICENSE file for details.
@@ -0,0 +1,39 @@
1
+ """
2
+ AccessGrid Python SDK
3
+ ~~~~~~~~~~~~~~~~~~~~
4
+
5
+ A Python SDK for interacting with the AccessGrid.com API.
6
+
7
+ Basic usage:
8
+
9
+ >>> from accessgrid import AccessGrid
10
+ >>> client = AccessGrid(account_id="your_id", secret_key="your_key")
11
+ >>> card = client.access_cards.provision(
12
+ ... card_template_id="template_id",
13
+ ... full_name="Employee Name"
14
+ ... )
15
+ >>> print(card.url)
16
+
17
+ For more information, see https://www.accessgrid.com/docs
18
+ """
19
+
20
+ # Import all public components
21
+ from .client import (
22
+ AccessGrid,
23
+ AccessGridError,
24
+ AuthenticationError,
25
+ AccessCard,
26
+ Template
27
+ )
28
+
29
+ # Version of the accessgrid package
30
+ __version__ = "0.1.0"
31
+
32
+ # List of public objects that will be exported with "from accessgrid import *"
33
+ __all__ = [
34
+ 'AccessGrid',
35
+ 'AccessGridError',
36
+ 'AuthenticationError',
37
+ 'AccessCard',
38
+ 'Template'
39
+ ]
@@ -0,0 +1,183 @@
1
+ import base64
2
+ import hmac
3
+ import hashlib
4
+ import json
5
+ import requests
6
+ from datetime import datetime, timezone
7
+ from urllib.parse import quote
8
+ from typing import Optional, Dict, Any, List
9
+
10
+ class AccessGridError(Exception):
11
+ """Base exception for AccessGrid SDK"""
12
+ pass
13
+
14
+ class AuthenticationError(AccessGridError):
15
+ """Raised when authentication fails"""
16
+ pass
17
+
18
+ class AccessCard:
19
+ def __init__(self, client, data: Dict[str, Any]):
20
+ self._client = client
21
+ self.id = data.get('id')
22
+ self.url = data.get('install_url')
23
+ self.state = data.get('state')
24
+ self.full_name = data.get('full_name')
25
+ self.expiration_date = data.get('expiration_date')
26
+
27
+ class Template:
28
+ def __init__(self, client, data: Dict[str, Any]):
29
+ self._client = client
30
+ self.id = data.get('id')
31
+ self.name = data.get('name')
32
+ self.platform = data.get('platform')
33
+ self.use_case = data.get('use_case')
34
+ self.protocol = data.get('protocol')
35
+ self.created_at = data.get('created_at')
36
+ self.last_published_at = data.get('last_published_at')
37
+ self.issued_keys_count = data.get('issued_keys_count')
38
+ self.active_keys_count = data.get('active_keys_count')
39
+ self.allowed_device_counts = data.get('allowed_device_counts')
40
+ self.support_settings = data.get('support_settings')
41
+ self.terms_settings = data.get('terms_settings')
42
+ self.style_settings = data.get('style_settings')
43
+
44
+ class AccessCards:
45
+ def __init__(self, client):
46
+ self._client = client
47
+
48
+ def provision(self, card_template_id: str, **kwargs) -> AccessCard:
49
+ """Provision a new access card"""
50
+ payload = {
51
+ 'card_template_id': card_template_id,
52
+ **kwargs
53
+ }
54
+ response = self._client._post('/api/v1/nfc_keys/issue', payload)
55
+ return AccessCard(self._client, response)
56
+
57
+ def update(self, card_id: str, **kwargs) -> AccessCard:
58
+ """Update an existing access card"""
59
+ payload = {
60
+ 'card_id': card_id,
61
+ **kwargs
62
+ }
63
+ response = self._client._post('/api/v1/nfc_keys/update', payload)
64
+ return AccessCard(self._client, response)
65
+
66
+ def _manage(self, card_id: str, action: str) -> AccessCard:
67
+ """Internal method for card management actions"""
68
+ payload = {
69
+ 'card_id': card_id,
70
+ 'manage_action': action
71
+ }
72
+ response = self._client._post('/api/v1/nfc_keys/manage', payload)
73
+ return AccessCard(self._client, response)
74
+
75
+ def suspend(self, card_id: str) -> AccessCard:
76
+ """Suspend an access card"""
77
+ return self._manage(card_id, 'suspend')
78
+
79
+ def resume(self, card_id: str) -> AccessCard:
80
+ """Resume a suspended access card"""
81
+ return self._manage(card_id, 'resume')
82
+
83
+ def unlink(self, card_id: str) -> AccessCard:
84
+ """Unlink an access card"""
85
+ return self._manage(card_id, 'unlink')
86
+
87
+ class Console:
88
+ def __init__(self, client):
89
+ self._client = client
90
+
91
+ def create_template(self, **kwargs) -> Template:
92
+ """Create a new card template"""
93
+ response = self._client._post('/api/v1/enterprise/create_template', kwargs)
94
+ return Template(self._client, response)
95
+
96
+ def update_template(self, card_template_id: str, **kwargs) -> Template:
97
+ """Update an existing card template"""
98
+ response = self._client._post(f'/api/v1/enterprise/update_template/{card_template_id}', kwargs)
99
+ return Template(self._client, response)
100
+
101
+ def read_template(self, card_template_id: str) -> Template:
102
+ """Get details of a card template"""
103
+ response = self._client._get(f'/api/v1/enterprise/read_template/{card_template_id}')
104
+ return Template(self._client, response)
105
+
106
+ def event_log(self, card_template_id: str, filters: Optional[Dict] = None,
107
+ page: int = 1, per_page: int = 50) -> Dict[str, Any]:
108
+ """Get event logs for a card template"""
109
+ params = {
110
+ 'page': page,
111
+ 'per_page': per_page
112
+ }
113
+ if filters:
114
+ params['filters'] = filters
115
+
116
+ return self._client._get(f'/api/v1/enterprise/logs/{card_template_id}', params)
117
+
118
+ class AccessGrid:
119
+ def __init__(self, account_id: str, secret_key: str, base_url: str = 'https://api.accessgrid.com'):
120
+ if not account_id:
121
+ raise ValueError("Account ID is required")
122
+ if not secret_key:
123
+ raise ValueError("Secret Key is required")
124
+
125
+ self.account_id = account_id
126
+ self.secret_key = secret_key
127
+ self.base_url = base_url.rstrip('/')
128
+ self.access_cards = AccessCards(self)
129
+ self.console = Console(self)
130
+
131
+ def _generate_signature(self, payload: str) -> str:
132
+ """Generate HMAC signature for the payload"""
133
+ encoded_payload = base64.b64encode(payload.encode()).decode()
134
+ return hmac.new(
135
+ self.secret_key.encode(),
136
+ encoded_payload.encode(),
137
+ hashlib.sha256
138
+ ).hexdigest()
139
+
140
+ def _make_request(self, method: str, endpoint: str,
141
+ data: Optional[Dict] = None,
142
+ params: Optional[Dict] = None) -> Dict[str, Any]:
143
+ """Make an HTTP request to the API"""
144
+ url = f"{self.base_url}{endpoint}"
145
+
146
+ # Prepare payload and signature
147
+ payload = json.dumps(data) if data else ""
148
+ headers = {
149
+ 'X-ACCT-ID': self.account_id,
150
+ 'X-PAYLOAD-SIG': self._generate_signature(payload),
151
+ 'Content-Type': 'application/json'
152
+ }
153
+
154
+ try:
155
+ response = requests.request(
156
+ method=method,
157
+ url=url,
158
+ headers=headers,
159
+ json=data if data else None,
160
+ params=params
161
+ )
162
+
163
+ if response.status_code == 401:
164
+ raise AuthenticationError("Invalid credentials")
165
+ elif response.status_code == 402:
166
+ raise AccessGridError("Insufficient account balance")
167
+ elif not 200 <= response.status_code < 300:
168
+ error_data = response.json() if response.text else {}
169
+ error_message = error_data.get('message', response.text)
170
+ raise AccessGridError(f"API request failed: {error_message}")
171
+
172
+ return response.json()
173
+
174
+ except requests.exceptions.RequestException as e:
175
+ raise AccessGridError(f"Request failed: {str(e)}")
176
+
177
+ def _get(self, endpoint: str, params: Optional[Dict] = None) -> Dict[str, Any]:
178
+ """Make a GET request"""
179
+ return self._make_request('GET', endpoint, params=params)
180
+
181
+ def _post(self, endpoint: str, data: Dict) -> Dict[str, Any]:
182
+ """Make a POST request"""
183
+ return self._make_request('POST', endpoint, data=data)
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.2
2
+ Name: accessgrid
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the AccessGrid API
5
+ Home-page: https://github.com/yourusername/accessgrid-python
6
+ Author: Your Name
7
+ Author-email: your.email@example.com
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.7
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests>=2.25.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
24
+ Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
26
+ Requires-Dist: black>=22.3.0; extra == "dev"
27
+ Requires-Dist: isort>=5.10.1; extra == "dev"
28
+ Requires-Dist: flake8>=4.0.1; extra == "dev"
29
+ Requires-Dist: mypy>=0.981; extra == "dev"
30
+ Requires-Dist: build>=0.10.0; extra == "dev"
31
+ Requires-Dist: twine>=4.0.2; extra == "dev"
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: classifier
35
+ Dynamic: description
36
+ Dynamic: description-content-type
37
+ Dynamic: home-page
38
+ Dynamic: provides-extra
39
+ Dynamic: requires-dist
40
+ Dynamic: requires-python
41
+ Dynamic: summary
42
+
43
+ # AccessGrid SDK
44
+
45
+ A JavaScript SDK for interacting with the [AccessGrid.com](https://www.accessgrid.com) API. This SDK provides a simple interface for managing NFC key cards and enterprise templates. Full docs at https://www.accessgrid.com/docs
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ npm install accessgrid
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```javascript
56
+ import AccessGrid from 'accessgrid';
57
+
58
+ const accountId = process.env.ACCOUNT_ID;
59
+ const secretKey = process.env.SECRET_KEY;
60
+
61
+ const client = new AccessGrid(accountId, secretKey);
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ### Access Cards
67
+
68
+ #### Provision a new card
69
+
70
+ ```javascript
71
+ const card = await client.accessCards.provision({
72
+ cardTemplateId: "0xd3adb00b5",
73
+ employeeId: "123456789",
74
+ tagId: "DDEADB33FB00B5",
75
+ allowOnMultipleDevices: true,
76
+ fullName: "Employee name",
77
+ email: "employee@yourwebsite.com",
78
+ phoneNumber: "+19547212241",
79
+ classification: "full_time",
80
+ startDate: "2025-01-31T22:46:25.601Z",
81
+ expirationDate: "2025-04-30T22:46:25.601Z",
82
+ employeePhoto: "[image_in_base64_encoded_format]"
83
+ });
84
+ ```
85
+
86
+ #### Update a card
87
+
88
+ ```javascript
89
+ const card = await client.accessCards.update({
90
+ cardId: "0xc4rd1d",
91
+ employeeId: "987654321",
92
+ fullName: "Updated Employee Name",
93
+ classification: "contractor",
94
+ expirationDate: "2025-02-22T21:04:03.664Z",
95
+ employeePhoto: "[image_in_base64_encoded_format]"
96
+ });
97
+ ```
98
+
99
+ #### Manage card states
100
+
101
+ ```javascript
102
+ // Suspend a card
103
+ await client.accessCards.suspend({
104
+ cardId: "0xc4rd1d"
105
+ });
106
+
107
+ // Resume a card
108
+ await client.accessCards.resume({
109
+ cardId: "0xc4rd1d"
110
+ });
111
+
112
+ // Unlink a card
113
+ await client.accessCards.unlink({
114
+ cardId: "0xc4rd1d"
115
+ });
116
+ ```
117
+
118
+ ### Enterprise Console
119
+
120
+ #### Create a template
121
+
122
+ ```javascript
123
+ const template = await client.console.createTemplate({
124
+ name: "Employee NFC key",
125
+ platform: "apple",
126
+ useCase: "employee_badge",
127
+ protocol: "desfire",
128
+ allowOnMultipleDevices: true,
129
+ watchCount: 2,
130
+ iphoneCount: 3,
131
+ design: {
132
+ backgroundColor: "#FFFFFF",
133
+ labelColor: "#000000",
134
+ labelSecondaryColor: "#333333",
135
+ backgroundImage: "[image_in_base64_encoded_format]",
136
+ logoImage: "[image_in_base64_encoded_format]",
137
+ iconImage: "[image_in_base64_encoded_format]"
138
+ },
139
+ supportInfo: {
140
+ supportUrl: "https://help.yourcompany.com",
141
+ supportPhoneNumber: "+1-555-123-4567",
142
+ supportEmail: "support@yourcompany.com",
143
+ privacyPolicyUrl: "https://yourcompany.com/privacy",
144
+ termsAndConditionsUrl: "https://yourcompany.com/terms"
145
+ }
146
+ });
147
+ ```
148
+
149
+ #### Update a template
150
+
151
+ ```javascript
152
+ const template = await client.console.updateTemplate({
153
+ cardTemplateId: "0xd3adb00b5",
154
+ name: "Updated Employee NFC key",
155
+ allowOnMultipleDevices: true,
156
+ watchCount: 2,
157
+ iphoneCount: 3,
158
+ supportInfo: {
159
+ supportUrl: "https://help.yourcompany.com",
160
+ supportPhoneNumber: "+1-555-123-4567",
161
+ supportEmail: "support@yourcompany.com",
162
+ privacyPolicyUrl: "https://yourcompany.com/privacy",
163
+ termsAndConditionsUrl: "https://yourcompany.com/terms"
164
+ }
165
+ });
166
+ ```
167
+
168
+ #### Read a template
169
+
170
+ ```javascript
171
+ const template = await client.console.readTemplate({
172
+ cardTemplateId: "0xd3adb00b5"
173
+ });
174
+ ```
175
+
176
+ #### Get event logs
177
+
178
+ ```javascript
179
+ const events = await client.console.eventLog({
180
+ cardTemplateId: "0xd3adb00b5",
181
+ filters: {
182
+ device: "mobile", // "mobile" or "watch"
183
+ startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
184
+ endDate: new Date().toISOString(),
185
+ eventType: "install"
186
+ }
187
+ });
188
+ ```
189
+
190
+ ## Configuration
191
+
192
+ The SDK can be configured with custom options:
193
+
194
+ ```javascript
195
+ const client = new AccessGrid(accountId, secretKey, {
196
+ baseUrl: 'https://api.staging.accessgrid.com' // Use a different API endpoint
197
+ });
198
+ ```
199
+
200
+ ## Error Handling
201
+
202
+ The SDK throws errors for various scenarios including:
203
+ - Missing required credentials
204
+ - API request failures
205
+ - Invalid parameters
206
+ - Server errors
207
+
208
+ Example error handling:
209
+
210
+ ```javascript
211
+ try {
212
+ const card = await client.accessCards.provision({
213
+ // ... parameters
214
+ });
215
+ } catch (error) {
216
+ console.error('Failed to provision card:', error.message);
217
+ }
218
+ ```
219
+
220
+ ## Requirements
221
+
222
+ - Node.js 12 or higher
223
+ - Modern browser environment with support for:
224
+ - Fetch API
225
+ - Web Crypto API
226
+ - Promises
227
+ - async/await
228
+
229
+ ## Security
230
+
231
+ The SDK automatically handles:
232
+ - Request signing using HMAC-SHA256
233
+ - Secure payload encoding
234
+ - Authentication headers
235
+ - HTTPS communication
236
+
237
+ Never expose your `secretKey` in client-side code. Always use environment variables or a secure configuration management system.
238
+
239
+ ## License
240
+
241
+ MIT License - See LICENSE file for details.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ accessgrid/__init__.py
6
+ accessgrid/client.py
7
+ accessgrid.egg-info/PKG-INFO
8
+ accessgrid.egg-info/SOURCES.txt
9
+ accessgrid.egg-info/dependency_links.txt
10
+ accessgrid.egg-info/requires.txt
11
+ accessgrid.egg-info/top_level.txt
12
+ tests/__init__.py
13
+ tests/test_accessgrid.py
@@ -0,0 +1,12 @@
1
+ requests>=2.25.0
2
+
3
+ [dev]
4
+ pytest>=7.0.0
5
+ pytest-mock>=3.10.0
6
+ pytest-cov>=4.0.0
7
+ black>=22.3.0
8
+ isort>=5.10.1
9
+ flake8>=4.0.1
10
+ mypy>=0.981
11
+ build>=0.10.0
12
+ twine>=4.0.2
@@ -0,0 +1,2 @@
1
+ accessgrid
2
+ tests
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=45", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,46 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ # Read README for the long description
4
+ with open("README.md", "r", encoding="utf-8") as fh:
5
+ long_description = fh.read()
6
+
7
+ setup(
8
+ name="accessgrid",
9
+ version="0.1.0",
10
+ author="Your Name",
11
+ author_email="your.email@example.com",
12
+ description="Python SDK for the AccessGrid API",
13
+ long_description=long_description,
14
+ long_description_content_type="text/markdown",
15
+ url="https://github.com/yourusername/accessgrid-python",
16
+ packages=find_packages(),
17
+ classifiers=[
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.7",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ],
29
+ python_requires=">=3.7",
30
+ install_requires=[
31
+ "requests>=2.25.0",
32
+ ],
33
+ extras_require={
34
+ "dev": [
35
+ "pytest>=7.0.0",
36
+ "pytest-mock>=3.10.0",
37
+ "pytest-cov>=4.0.0",
38
+ "black>=22.3.0",
39
+ "isort>=5.10.1",
40
+ "flake8>=4.0.1",
41
+ "mypy>=0.981",
42
+ "build>=0.10.0",
43
+ "twine>=4.0.2",
44
+ ],
45
+ },
46
+ )
File without changes
@@ -0,0 +1,181 @@
1
+ import pytest
2
+ from unittest.mock import patch, Mock
3
+ from accessgrid import AccessGrid, AccessGridError, AuthenticationError
4
+
5
+ MOCK_ACCOUNT_ID = 'test-account-id'
6
+ MOCK_SECRET_KEY = 'test-secret-key'
7
+
8
+ @pytest.fixture
9
+ def client():
10
+ return AccessGrid(MOCK_ACCOUNT_ID, MOCK_SECRET_KEY)
11
+
12
+ @pytest.fixture
13
+ def mock_response():
14
+ mock = Mock()
15
+ mock.json.return_value = {'status': 'success'}
16
+ mock.status_code = 200
17
+ mock.text = '{"status": "success"}'
18
+ return mock
19
+
20
+ class TestAccessGrid:
21
+ def test_constructor_missing_account_id(self):
22
+ with pytest.raises(ValueError, match='Account ID is required'):
23
+ AccessGrid(None, MOCK_SECRET_KEY)
24
+
25
+ def test_constructor_missing_secret_key(self):
26
+ with pytest.raises(ValueError, match='Secret Key is required'):
27
+ AccessGrid(MOCK_ACCOUNT_ID, None)
28
+
29
+ def test_constructor_with_custom_base_url(self):
30
+ custom_url = 'https://custom.api.com'
31
+ client = AccessGrid(MOCK_ACCOUNT_ID, MOCK_SECRET_KEY, base_url=custom_url)
32
+ assert client.base_url == custom_url.rstrip('/')
33
+
34
+ class TestAccessCards:
35
+ @pytest.fixture
36
+ def mock_provision_params(self):
37
+ return {
38
+ 'card_template_id': '0xd3adb00b5',
39
+ 'employee_id': '123456789',
40
+ 'tag_id': 'DDEADB33FB00B5',
41
+ 'allow_on_multiple_devices': True,
42
+ 'full_name': 'Employee name',
43
+ 'email': 'employee@yourwebsite.com',
44
+ 'phone_number': '+19547212241',
45
+ 'classification': 'full_time',
46
+ 'start_date': '2025-01-31T22:46:25.601Z',
47
+ 'expiration_date': '2025-04-30T22:46:25.601Z',
48
+ 'employee_photo': 'base64photo'
49
+ }
50
+
51
+ @patch('requests.request')
52
+ def test_provision_card(self, mock_request, client, mock_response, mock_provision_params):
53
+ mock_request.return_value = mock_response
54
+
55
+ card = client.access_cards.provision(**mock_provision_params)
56
+
57
+ mock_request.assert_called_once()
58
+ call_args = mock_request.call_args[1]
59
+ assert call_args['method'] == 'POST'
60
+ assert call_args['url'] == f"{client.base_url}/api/v1/nfc_keys/issue"
61
+ assert call_args['json'] == mock_provision_params
62
+ assert call_args['headers']['X-ACCT-ID'] == MOCK_ACCOUNT_ID
63
+ assert 'X-PAYLOAD-SIG' in call_args['headers']
64
+ assert call_args['headers']['Content-Type'] == 'application/json'
65
+
66
+ @patch('requests.request')
67
+ def test_provision_card_error(self, mock_request, client, mock_provision_params):
68
+ error_response = Mock()
69
+ error_response.status_code = 400
70
+ error_response.text = '{"message": "Invalid template ID"}'
71
+ error_response.json.return_value = {"message": "Invalid template ID"}
72
+ mock_request.return_value = error_response
73
+
74
+ with pytest.raises(AccessGridError, match='API request failed: Invalid template ID'):
75
+ client.access_cards.provision(**mock_provision_params)
76
+
77
+ @patch('requests.request')
78
+ def test_update_card(self, mock_request, client, mock_response):
79
+ mock_request.return_value = mock_response
80
+ update_params = {
81
+ 'card_id': '0xc4rd1d',
82
+ 'employee_id': '987654321',
83
+ 'full_name': 'Updated Employee Name',
84
+ 'classification': 'contractor',
85
+ 'expiration_date': '2025-02-22T21:04:03.664Z'
86
+ }
87
+
88
+ card = client.access_cards.update(**update_params)
89
+
90
+ mock_request.assert_called_once()
91
+ call_args = mock_request.call_args[1]
92
+ assert call_args['method'] == 'POST'
93
+ assert call_args['url'] == f"{client.base_url}/api/v1/nfc_keys/update"
94
+ assert call_args['json'] == update_params
95
+
96
+ @patch('requests.request')
97
+ def test_manage_operations(self, mock_request, client, mock_response):
98
+ mock_request.return_value = mock_response
99
+ card_id = '0xc4rd1d'
100
+
101
+ # Test suspend
102
+ client.access_cards.suspend(card_id)
103
+ call_args = mock_request.call_args[1]
104
+ assert call_args['method'] == 'POST'
105
+ assert call_args['url'] == f"{client.base_url}/api/v1/nfc_keys/manage"
106
+ assert call_args['json'] == {'card_id': card_id, 'manage_action': 'suspend'}
107
+
108
+ # Test resume
109
+ client.access_cards.resume(card_id)
110
+ call_args = mock_request.call_args[1]
111
+ assert call_args['json'] == {'card_id': card_id, 'manage_action': 'resume'}
112
+
113
+ # Test unlink
114
+ client.access_cards.unlink(card_id)
115
+ call_args = mock_request.call_args[1]
116
+ assert call_args['json'] == {'card_id': card_id, 'manage_action': 'unlink'}
117
+
118
+ class TestConsole:
119
+ @pytest.fixture
120
+ def mock_template_params(self):
121
+ return {
122
+ 'name': 'Employee NFC key',
123
+ 'platform': 'apple',
124
+ 'use_case': 'employee_badge',
125
+ 'protocol': 'desfire',
126
+ 'allow_on_multiple_devices': True,
127
+ 'watch_count': 2,
128
+ 'iphone_count': 3,
129
+ 'design': {
130
+ 'background_color': '#FFFFFF',
131
+ 'label_color': '#000000',
132
+ 'label_secondary_color': '#333333'
133
+ },
134
+ 'support_info': {
135
+ 'support_url': 'https://help.yourcompany.com',
136
+ 'support_phone_number': '+1-555-123-4567',
137
+ 'support_email': 'support@yourcompany.com',
138
+ 'privacy_policy_url': 'https://yourcompany.com/privacy',
139
+ 'terms_and_conditions_url': 'https://yourcompany.com/terms'
140
+ }
141
+ }
142
+
143
+ @patch('requests.request')
144
+ def test_create_template(self, mock_request, client, mock_response, mock_template_params):
145
+ mock_request.return_value = mock_response
146
+
147
+ template = client.console.create_template(**mock_template_params)
148
+
149
+ call_args = mock_request.call_args[1]
150
+ assert call_args['method'] == 'POST'
151
+ assert call_args['url'] == f"{client.base_url}/api/v1/enterprise/create_template"
152
+ assert call_args['json'] == mock_template_params
153
+
154
+ @patch('requests.request')
155
+ def test_read_template(self, mock_request, client, mock_response):
156
+ mock_request.return_value = mock_response
157
+ template_id = '0xd3adb00b5'
158
+
159
+ template = client.console.read_template(template_id)
160
+
161
+ call_args = mock_request.call_args[1]
162
+ assert call_args['method'] == 'GET'
163
+ assert call_args['url'] == f"{client.base_url}/api/v1/enterprise/read_template/{template_id}"
164
+
165
+ @patch('requests.request')
166
+ def test_event_log(self, mock_request, client, mock_response):
167
+ mock_request.return_value = mock_response
168
+ template_id = '0xd3adb00b5'
169
+ filters = {
170
+ 'device': 'mobile',
171
+ 'start_date': '2025-01-01T00:00:00Z',
172
+ 'end_date': '2025-02-01T00:00:00Z',
173
+ 'event_type': 'install'
174
+ }
175
+
176
+ events = client.console.event_log(template_id, filters=filters)
177
+
178
+ call_args = mock_request.call_args[1]
179
+ assert call_args['method'] == 'GET'
180
+ assert call_args['url'] == f"{client.base_url}/api/v1/enterprise/logs/{template_id}"
181
+ assert call_args['params'] == {'filters': filters, 'page': 1, 'per_page': 50}