qubesync 0.1.0__py3-none-any.whl

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.
qubesync/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ QubeSync - Python library for QuickBooks integration via QubeSync API
3
+
4
+ This package provides:
5
+ - QubeSync: Main API client for interacting with QubeSync service
6
+ - RequestBuilder: DSL for building QuickBooks XML requests
7
+ - Exception classes for error handling
8
+
9
+ Usage:
10
+ from qubesync import QubeSync, RequestBuilder
11
+
12
+ # API operations
13
+ connection = QubeSync.create_connection(options)
14
+
15
+ # Request building
16
+ request = RequestBuilder(version="16.0")
17
+ with request as r:
18
+ with r.QBXML() as qbxml:
19
+ # ... build request
20
+ """
21
+
22
+ # Import main classes for convenient access
23
+ from .client import (
24
+ QubeSync,
25
+ QubeSyncError,
26
+ StaleWebhookError,
27
+ InvalidWebhookSignatureError,
28
+ ConfigError
29
+ )
30
+
31
+ from .request_builder import (
32
+ RequestBuilder,
33
+ RequestElement,
34
+ FluentRequestBuilder
35
+ )
36
+
37
+ # Version info
38
+ __version__ = "0.1.0"
39
+
40
+ # Public API
41
+ __all__ = [
42
+ # Main API client
43
+ "QubeSync",
44
+
45
+ # Request builders
46
+ "RequestBuilder",
47
+ "RequestElement",
48
+ "FluentRequestBuilder",
49
+
50
+ # Exceptions
51
+ "QubeSyncError",
52
+ "StaleWebhookError",
53
+ "InvalidWebhookSignatureError",
54
+ "ConfigError",
55
+
56
+ # Version
57
+ "__version__"
58
+ ]
qubesync/client.py ADDED
@@ -0,0 +1,134 @@
1
+ import os
2
+ import json
3
+ import requests
4
+ import hmac
5
+ import hashlib
6
+ import time
7
+
8
+ class QubeSyncError(Exception): pass
9
+ class StaleWebhookError(QubeSyncError): pass
10
+ class InvalidWebhookSignatureError(QubeSyncError): pass
11
+ class ConfigError(QubeSyncError): pass
12
+
13
+ class QubeSync:
14
+ EXPECTED_SIGNATURE_SCHEMES = ["v1"]
15
+
16
+ def base_url():
17
+ return os.getenv("QUBE_URL", "https://qubesync.com/api/v1")
18
+
19
+ def default_headers():
20
+ return {
21
+ "Content-Type": "application/json",
22
+ "Accept": "application/json",
23
+ }
24
+
25
+ def api_key():
26
+ try:
27
+ return os.environ["QUBE_API_KEY"]
28
+ except KeyError:
29
+ raise ConfigError("QUBE_API_KEY not set in environment.")
30
+
31
+ def api_secret():
32
+ try:
33
+ return os.environ["QUBE_WEBHOOK_SECRET"]
34
+ except KeyError:
35
+ raise ConfigError("QUBE_WEBHOOK_SECRET not set in environment.")
36
+
37
+ def connection():
38
+ session = requests.Session()
39
+ session.auth = (QubeSync.api_key(), "")
40
+ return session
41
+
42
+ def get(url, headers={}):
43
+ resp = QubeSync.connection().get(f"{QubeSync.base_url()}/{url}", headers={**QubeSync.default_headers(), **headers})
44
+ if resp.status_code >= 200 and resp.status_code < 300:
45
+ return resp.json()
46
+ raise QubeSyncError(f"Unexpected response: {resp.status_code}\n{resp.text}")
47
+
48
+ def post(url, body=None, headers={}):
49
+ resp = QubeSync.connection().post(f"{QubeSync.base_url()}/{url}", headers={**QubeSync.default_headers(), **headers}, json=body)
50
+ if resp.status_code >= 200 and resp.status_code < 300:
51
+ return resp.json()
52
+ raise QubeSyncError(f"Unexpected response: {resp.status_code}\n{resp.text}")
53
+
54
+ def delete(url, headers={}):
55
+ resp = QubeSync.connection().delete(f"{QubeSync.base_url()}/{url}", headers={**QubeSync.default_headers(), **headers})
56
+ if resp.status_code >= 200 and resp.status_code < 300:
57
+ return True
58
+ raise QubeSyncError(f"Unexpected response: {resp.status_code}\n{resp.text}")
59
+
60
+ def create_connection(options={}):
61
+ resp = QubeSync.post("connections", body=options)
62
+ conn = resp.get("data", {})
63
+ if not conn.get("id"):
64
+ raise QubeSyncError(f"Could not create connection: {resp}")
65
+ return conn
66
+
67
+ def delete_connection(id):
68
+ return QubeSync.delete(f"connections/{id}")
69
+
70
+ def get_connection(id):
71
+ return QubeSync.get(f"connections/{id}").get("data")
72
+
73
+ def queue_request(connection_id, request):
74
+ if not (request.get("request_xml") or request.get("request_json")):
75
+ raise ValueError("must have either request_xml or request_json")
76
+ if not request.get("webhook_url"):
77
+ print("Warning: no webhook_url provided")
78
+ return QubeSync.post(f"connections/{connection_id}/queued_requests", {"queued_request": request}).get("data")
79
+
80
+ def get_request(id):
81
+ return QubeSync.get(f"queued_requests/{id}").get("data")
82
+
83
+ def get_requests(connection_id):
84
+ return QubeSync.get(f"connections/{connection_id}/queued_requests").get("data")
85
+
86
+ def delete_request(id):
87
+ return QubeSync.delete(f"queued_requests/{id}")
88
+
89
+ def get_qwc(connection_id):
90
+ return QubeSync.post(f"connections/{connection_id}/qwc").get("qwc")
91
+
92
+ def generate_password(connection_id):
93
+ resp = QubeSync.post(f"connections/{connection_id}/password")
94
+ password = resp.get("data", {}).get("password")
95
+ if not password:
96
+ raise QubeSyncError(f"Password not found: {resp}")
97
+ return password
98
+
99
+ def extract_signature_meta(header, expected_schemes=["v1"]):
100
+ # Example header: t=timestamp,v1=signature1,v1=signature2
101
+ # note: signatures may include '=' characters
102
+ parts = header.split(',')
103
+ timestamp = None
104
+ signatures = []
105
+ for part in parts:
106
+ k, v = part.split('=', 1)
107
+ if k == 't':
108
+ timestamp = int(v)
109
+ elif k in expected_schemes:
110
+ signatures.append(v)
111
+ if timestamp is None or not signatures:
112
+ raise InvalidWebhookSignatureError("Invalid signature header format.")
113
+ return timestamp, signatures
114
+
115
+ def sign_payload(payload):
116
+ return hmac.new(
117
+ QubeSync.api_secret().encode("utf-8"),
118
+ payload.encode("utf-8"),
119
+ hashlib.sha256
120
+ ).hexdigest()
121
+
122
+ def verify_and_build_webhook(body, signature, max_age=500):
123
+ timestamp, signatures = QubeSync.extract_signature_meta(signature, QubeSync.EXPECTED_SIGNATURE_SCHEMES)
124
+
125
+ # print for debugging
126
+ print(f"Timestamp: {timestamp}, Signatures: {signatures}")
127
+ if timestamp < time.time() - max_age:
128
+ raise StaleWebhookError(f"Timestamp more than {max_age} seconds old.")
129
+
130
+ signed_payload = QubeSync.sign_payload(str(timestamp) + '.' + body)
131
+ if any(hmac.compare_digest(signed_payload, sig) for sig in signatures):
132
+ return json.loads(body)
133
+
134
+ raise InvalidWebhookSignatureError("Webhook signature mismatch")
@@ -0,0 +1,194 @@
1
+ """
2
+ RequestBuilder - A Python DSL for building QuickBooks XML requests
3
+
4
+ This module provides a fluent interface for constructing QBXML requests
5
+ in a JSON format for API consumption. QuBeSync API will clean the data of any special
6
+ characters that cause QuickBooks errors and then convert this JSON back into
7
+ QBXML when sending requests to QuickBooks.
8
+
9
+ Example usage:
10
+ from qubesync import RequestBuilder
11
+
12
+ request = RequestBuilder(version="16.0")
13
+ with request as r:
14
+ with r.QBXML() as qbxml:
15
+ with qbxml.QBXMLMsgsRq(onError='stopOnError') as msgs:
16
+ with msgs.CustomerQueryRq(requestID='asdf1234', iterator='Start') as query:
17
+ query.MaxReturned(20)
18
+ query.IncludeRetElement('ListID')
19
+ query.IncludeRetElement('Name')
20
+ query.IncludeRetElement('FullName')
21
+ query.IncludeRetElement('IsActive')
22
+
23
+ json_data = request.as_json()
24
+ """
25
+
26
+
27
+ class RequestElement:
28
+ """Represents a single element in the request structure."""
29
+
30
+ def __init__(self, name, attributes=None, text=None, parent=None):
31
+ self.name = name
32
+ self.attributes = attributes or {}
33
+ self.text = text
34
+ self.children = []
35
+ self.parent = parent
36
+
37
+ def __enter__(self):
38
+ return self
39
+
40
+ def __exit__(self, exc_type, exc_val, exc_tb):
41
+ return False
42
+
43
+ def __getattr__(self, name):
44
+ """Create a new child element with the given name."""
45
+ def element_creator(*args, **kwargs):
46
+ # Handle text content as first positional argument
47
+ text = args[0] if args else None
48
+
49
+ # Convert kwargs to attributes, handling Python reserved words
50
+ attributes = {}
51
+ for key, value in kwargs.items():
52
+ # Convert snake_case to camelCase if needed, but preserve exact case for QB fields
53
+ if key.endswith('_'):
54
+ key = key[:-1] # Remove trailing underscore for reserved words
55
+ attributes[key] = value
56
+
57
+ child = RequestElement(name, attributes, text, self)
58
+ self.children.append(child)
59
+ return child
60
+
61
+ return element_creator
62
+
63
+ def as_dict(self):
64
+ """Convert this element to a dictionary representation."""
65
+ result = {"name": self.name}
66
+
67
+ if self.attributes:
68
+ result["attributes"] = self.attributes
69
+
70
+ if self.text is not None:
71
+ result["text"] = self.text
72
+
73
+ if self.children:
74
+ result["children"] = [child.as_dict() for child in self.children]
75
+
76
+ return result
77
+
78
+
79
+ class RequestBuilder:
80
+ """A fluent interface for building QuickBooks XML requests."""
81
+
82
+ def __init__(self, version="16.0"):
83
+ self.version = version
84
+ self.root_elements = []
85
+
86
+ def __enter__(self):
87
+ return self
88
+
89
+ def __exit__(self, exc_type, exc_val, exc_tb):
90
+ return False
91
+
92
+ def __getattr__(self, name):
93
+ """Create a new root element with the given name."""
94
+ def element_creator(*args, **kwargs):
95
+ # Handle text content as first positional argument
96
+ text = args[0] if args else None
97
+
98
+ # Convert kwargs to attributes
99
+ attributes = {}
100
+ for key, value in kwargs.items():
101
+ # Handle Python reserved words by removing trailing underscore
102
+ if key.endswith('_'):
103
+ key = key[:-1]
104
+ attributes[key] = value
105
+
106
+ element = RequestElement(name, attributes, text)
107
+ self.root_elements.append(element)
108
+ return element
109
+
110
+ return element_creator
111
+
112
+ def as_json(self):
113
+ """Convert the request to JSON format."""
114
+ return {
115
+ "version": self.version,
116
+ "request": [element.as_dict() for element in self.root_elements]
117
+ }
118
+
119
+ def as_dict(self):
120
+ """Alias for as_json() for consistency."""
121
+ return self.as_json()
122
+
123
+
124
+ class FluentRequestBuilder(RequestBuilder):
125
+ """
126
+ Alternative builder with method chaining support for a more Ruby-like feel.
127
+
128
+ Example:
129
+ request = FluentRequestBuilder(version="16.0")
130
+ request.QBXML().QBXMLMsgsRq(onError='stopOnError').CustomerQueryRq(
131
+ requestID='asdf1234', iterator='Start'
132
+ ).add_children([
133
+ ('MaxReturned', 20),
134
+ ('IncludeRetElement', 'ListID'),
135
+ ('IncludeRetElement', 'Name'),
136
+ ('IncludeRetElement', 'FullName'),
137
+ ('IncludeRetElement', 'IsActive')
138
+ ])
139
+ """
140
+
141
+ def __init__(self, version="16.0"):
142
+ super().__init__(version)
143
+ self._current_element = None
144
+
145
+ def __getattr__(self, name):
146
+ """Create a new element and return self for chaining."""
147
+ def element_creator(*args, **kwargs):
148
+ text = args[0] if args else None
149
+ attributes = {k.rstrip('_'): v for k, v in kwargs.items()}
150
+
151
+ if self._current_element is None:
152
+ # Creating root element
153
+ element = RequestElement(name, attributes, text)
154
+ self.root_elements.append(element)
155
+ self._current_element = element
156
+ else:
157
+ # Creating child element
158
+ element = RequestElement(name, attributes, text, self._current_element)
159
+ self._current_element.children.append(element)
160
+ self._current_element = element
161
+
162
+ return self
163
+
164
+ return element_creator
165
+
166
+ def add_children(self, children_data):
167
+ """Add multiple children to the current element."""
168
+ if self._current_element is None:
169
+ raise ValueError("No current element to add children to")
170
+
171
+ for child_data in children_data:
172
+ if isinstance(child_data, tuple) and len(child_data) == 2:
173
+ name, text = child_data
174
+ child = RequestElement(name, text=text, parent=self._current_element)
175
+ self._current_element.children.append(child)
176
+ elif isinstance(child_data, dict):
177
+ name = child_data.get('name')
178
+ attributes = child_data.get('attributes', {})
179
+ text = child_data.get('text')
180
+ child = RequestElement(name, attributes, text, self._current_element)
181
+ self._current_element.children.append(child)
182
+
183
+ return self
184
+
185
+ def parent(self):
186
+ """Move back to the parent element for continued chaining."""
187
+ if self._current_element and self._current_element.parent:
188
+ self._current_element = self._current_element.parent
189
+ return self
190
+
191
+ def root(self):
192
+ """Return to the root level."""
193
+ self._current_element = None
194
+ return self
@@ -0,0 +1,388 @@
1
+ Metadata-Version: 2.4
2
+ Name: qubesync
3
+ Version: 0.1.0
4
+ Summary: A Python library for QuickBooks Desktop / Enterprise integration via QubeSync API with intuitive DSL for building QBXML requests.
5
+ Author-email: QubeSync <support@qubesync.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/qubeintegrations/qube_sync_py
8
+ Project-URL: Documentation, https://docs.qubesync.com
9
+ Project-URL: Repository, https://github.com/qubeintegrations/qube_sync_py
10
+ Project-URL: Issues, https://github.com/qubeintegrations/qube_sync_py/issues
11
+ Keywords: quickbooks,qbxml,accounting,integration,api,dsl
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: requests
26
+ Dynamic: license-file
27
+
28
+ # QubeSync Python Library
29
+
30
+ A Python library for seamless QuickBooks integration via the QubeSync API. This library provides both a robust API client for QubeSync operations and a powerful DSL (Domain Specific Language) for building QuickBooks XML requests.
31
+
32
+ ## Features
33
+
34
+ - 🔌 **Complete QubeSync API client** - Connect, queue requests, handle webhooks
35
+ - 🏗️ **Intuitive Request Builder DSL** - Build QBXML requests with Python syntax
36
+ - 🔒 **Webhook security** - Built-in signature verification for webhooks
37
+ - 🚀 **Multiple syntax styles** - Context managers, fluent chaining, or Ruby-like syntax
38
+ - ✅ **Fully tested** - comprehensive tests ensuring reliability
39
+ - 📦 **Clean imports** - Simple `from qubesync import ...` syntax
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install qubesync
45
+ ```
46
+
47
+ Or install from source:
48
+
49
+ ```bash
50
+ git clone https://github.com/qubeintegrations/qube_sync_py
51
+ cd qube_sync_py
52
+ pip install -e .
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ### Environment Setup
58
+
59
+ Set your QubeSync credentials:
60
+
61
+ ```bash
62
+ export QUBE_API_KEY="your_api_key_here"
63
+ export QUBE_WEBHOOK_SECRET="your_webhook_secret_here"
64
+ export QUBE_URL="https://qubesync.com/api/v1" # Optional, defaults to this
65
+ ```
66
+
67
+ ### Basic Usage
68
+
69
+ ```python
70
+ from qubesync import QubeSync, RequestBuilder
71
+
72
+ # 1. Create a connection
73
+ connection = QubeSync.create_connection({
74
+ "name": "My Customer Inc.", # optional (will populate from QuickBooks on first sync if blank)
75
+ "redirect_url": "https://your-app.com/qbd-setup/callback"
76
+ })
77
+
78
+ # 2. Build a request using the DSL
79
+ request = RequestBuilder(version="16.0")
80
+ with request as r:
81
+ with r.QBXML() as qbxml:
82
+ with qbxml.QBXMLMsgsRq(onError='stopOnError') as msgs:
83
+ with msgs.CustomerQueryRq(requestID='customer_123', iterator='Start') as query:
84
+ query.MaxReturned(50)
85
+ query.IncludeRetElement('ListID')
86
+ query.IncludeRetElement('Name')
87
+ query.IncludeRetElement('Email')
88
+
89
+ # 3. Queue the request
90
+ response = QubeSync.queue_request(connection['id'], {
91
+ 'request_json': request.as_json(),
92
+ 'webhook_url': 'https://your-app.com/webhooks/qb_response'
93
+ })
94
+
95
+ print(f"Request queued with ID: {response['id']}")
96
+ ```
97
+
98
+ ## API Client Reference
99
+
100
+ ### Connection Management
101
+
102
+ ```python
103
+ from qubesync import QubeSync
104
+
105
+ # Create a new connection
106
+ connection = QubeSync.create_connection({
107
+ "name": "My Customer Inc.", # optional (will populate from QuickBooks on first sync if blank)
108
+ "redirect_url": "https://your-app.com/qbd-setup/callback"
109
+ })
110
+
111
+ # Get connection details
112
+ connection = QubeSync.get_connection(connection_id)
113
+
114
+ # Delete a connection
115
+ QubeSync.delete_connection(connection_id)
116
+
117
+ # Generate QWC file for QuickBooks
118
+ qwc_content = QubeSync.get_qwc(connection_id)
119
+
120
+ # Generate password for QuickBooks connection
121
+ password = QubeSync.generate_password(connection_id)
122
+ ```
123
+
124
+ ### Request Management
125
+
126
+ ```python
127
+ # Queue a request
128
+ request_data = {
129
+ 'request_json': {...}, # Built using RequestBuilder
130
+ 'webhook_url': 'https://your-app.com/webhook'
131
+ }
132
+ response = QubeSync.queue_request(connection_id, request_data)
133
+
134
+ # Get request status
135
+ request = QubeSync.get_request(request_id)
136
+
137
+ # Get all requests for a connection
138
+ requests = QubeSync.get_requests(connection_id)
139
+
140
+ # Delete a request
141
+ QubeSync.delete_request(request_id)
142
+ ```
143
+
144
+ ### Webhook Handling
145
+
146
+ ```python
147
+ # Verify and parse webhook payload
148
+ def handle_webhook(request):
149
+ signature = request.headers.get('X-Qube-Signature')
150
+ body = request.body.decode('utf-8')
151
+
152
+ try:
153
+ webhook_data = QubeSync.verify_and_build_webhook(body, signature)
154
+ # Process the verified webhook data in the background
155
+ # to ensure a quick response
156
+ print(f"Received response for request: {webhook_data['request_id']}")
157
+ return {"status": "success"}
158
+ except (StaleWebhookError, InvalidWebhookSignatureError) as e:
159
+ print(f"Webhook verification failed: {e}")
160
+ return {"status": "error"}, 400
161
+ ```
162
+
163
+ ## RequestBuilder DSL
164
+
165
+ The RequestBuilder provides an intuitive Python DSL for constructing QBXML requests that get converted to JSON format for the QubeSync API.
166
+
167
+ ### Context Manager Style (Recommended)
168
+
169
+ ```python
170
+ from qubesync import RequestBuilder
171
+
172
+ request = RequestBuilder(version="16.0")
173
+ with request as r:
174
+ with r.QBXML() as qbxml:
175
+ with qbxml.QBXMLMsgsRq(onError='stopOnError') as msgs:
176
+ with msgs.InvoiceQueryRq(requestID='inv_query_123') as query:
177
+ query.MaxReturned(25)
178
+ query.IncludeRetElement('TxnID')
179
+ query.IncludeRetElement('TxnNumber')
180
+ query.IncludeRetElement('CustomerRef')
181
+ query.IncludeRetElement('TxnDate')
182
+ query.IncludeRetElement('DueDate')
183
+ query.IncludeRetElement('BalanceRemaining')
184
+
185
+ json_data = request.as_json()
186
+ ```
187
+
188
+ ### Fluent Chaining Style
189
+
190
+ ```python
191
+ from qubesync import FluentRequestBuilder
192
+
193
+ request = FluentRequestBuilder(version="16.0")
194
+ request.QBXML().QBXMLMsgsRq(onError='stopOnError').ItemQueryRq(
195
+ requestID='item_query_456'
196
+ ).add_children([
197
+ ('MaxReturned', 100),
198
+ ('IncludeRetElement', 'ListID'),
199
+ ('IncludeRetElement', 'Name'),
200
+ ('IncludeRetElement', 'Type'),
201
+ ('IncludeRetElement', 'SalePrice')
202
+ ])
203
+
204
+ json_data = request.as_json()
205
+ ```
206
+
207
+ ### Ruby-like Style
208
+
209
+ ```python
210
+ from qubesync import RequestBuilder
211
+
212
+ request = RequestBuilder(version="16.0")
213
+ qbxml = request.QBXML()
214
+ msgs = qbxml.QBXMLMsgsRq(onError='stopOnError')
215
+ query = msgs.CustomerQueryRq(requestID='customer_789', iterator='Start')
216
+ query.MaxReturned(30)
217
+ query.IncludeRetElement('ListID')
218
+ query.IncludeRetElement('Name')
219
+ query.IncludeRetElement('CompanyName')
220
+
221
+ json_data = request.as_json()
222
+ ```
223
+
224
+ ### Handling Python Reserved Words
225
+
226
+ Use trailing underscores for Python reserved words:
227
+
228
+ ```python
229
+ request = RequestBuilder(version="16.0")
230
+ with request as r:
231
+ with r.QBXML() as qbxml:
232
+ with qbxml.QBXMLMsgsRq(onError='stopOnError') as msgs:
233
+ # Use trailing underscore for 'class', 'for', 'if', etc.
234
+ with msgs.CustomQueryRq(class_='invoice', for_='reports') as query:
235
+ query.MaxReturned(100)
236
+ ```
237
+
238
+ ### Complex Nested Structures
239
+
240
+ ```python
241
+ request = RequestBuilder(version="16.0")
242
+ with request as r:
243
+ with r.QBXML() as qbxml:
244
+ with qbxml.QBXMLMsgsRq(onError='stopOnError') as msgs:
245
+ with msgs.InvoiceAddRq(requestID='invoice_add_999') as add_req:
246
+ with add_req.InvoiceAdd() as invoice:
247
+ with invoice.CustomerRef() as customer_ref:
248
+ customer_ref.ListID('80000001-1234567890')
249
+
250
+ invoice.TxnDate('2024-01-15')
251
+ invoice.RefNumber('INV-2024-001')
252
+
253
+ with invoice.InvoiceLineAdd() as line:
254
+ with line.ItemRef() as item_ref:
255
+ item_ref.ListID('80000002-1234567890')
256
+ line.Desc('Professional Services')
257
+ line.Quantity(10)
258
+ line.Rate(150.00)
259
+ ```
260
+
261
+ ## Example Output
262
+
263
+ The RequestBuilder generates clean JSON that the QubeSync API processes:
264
+
265
+ ```json
266
+ {
267
+ "version": "16.0",
268
+ "request": [
269
+ {
270
+ "name": "QBXML",
271
+ "children": [
272
+ {
273
+ "name": "QBXMLMsgsRq",
274
+ "attributes": {"onError": "stopOnError"},
275
+ "children": [
276
+ {
277
+ "name": "CustomerQueryRq",
278
+ "attributes": {"requestID": "customer_123", "iterator": "Start"},
279
+ "children": [
280
+ {"name": "MaxReturned", "text": 50},
281
+ {"name": "IncludeRetElement", "text": "ListID"},
282
+ {"name": "IncludeRetElement", "text": "Name"},
283
+ {"name": "IncludeRetElement", "text": "Email"}
284
+ ]
285
+ }
286
+ ]
287
+ }
288
+ ]
289
+ }
290
+ ]
291
+ }
292
+ ```
293
+
294
+ ## Error Handling
295
+
296
+ ```python
297
+ from qubesync import (
298
+ QubeSync,
299
+ QubeSyncError,
300
+ ConfigError,
301
+ StaleWebhookError,
302
+ InvalidWebhookSignatureError
303
+ )
304
+
305
+ try:
306
+ connection = QubeSync.create_connection({"name": "Test"})
307
+ except ConfigError as e:
308
+ print(f"Configuration error: {e}")
309
+ except QubeSyncError as e:
310
+ print(f"API error: {e}")
311
+
312
+ # Webhook verification
313
+ try:
314
+ webhook_data = QubeSync.verify_and_build_webhook(body, signature)
315
+ except StaleWebhookError:
316
+ print("Webhook is too old")
317
+ except InvalidWebhookSignatureError:
318
+ print("Invalid webhook signature")
319
+ ```
320
+
321
+ ## Available Classes
322
+
323
+ ### API Client
324
+ - `QubeSync` - Main API client for all QubeSync operations
325
+ - `QubeSyncError` - Base exception class
326
+ - `ConfigError` - Configuration/environment variable errors
327
+ - `StaleWebhookError` - Webhook timestamp too old
328
+ - `InvalidWebhookSignatureError` - Webhook signature verification failed
329
+
330
+ ### Request Building
331
+ - `RequestBuilder` - Context manager style DSL (recommended)
332
+ - `FluentRequestBuilder` - Method chaining style DSL
333
+ - `RequestElement` - Individual request element (advanced usage)
334
+
335
+ ## Development
336
+
337
+ ### Running Tests
338
+
339
+ ```bash
340
+ # Run all tests
341
+ python run_tests.py
342
+
343
+ # Run with verbose output
344
+ python run_tests.py --verbose
345
+
346
+ # Run specific test file
347
+ python run_tests.py tests/test_request_builder.py
348
+ ```
349
+
350
+ ### Running Examples
351
+
352
+ ```bash
353
+ # Run all examples
354
+ python examples.py
355
+
356
+ # Use virtual environment
357
+ ./bin/python examples.py
358
+ ```
359
+
360
+ ## Contributing
361
+
362
+ 1. Fork the repository
363
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
364
+ 3. Make your changes
365
+ 4. Run tests (`python run_tests.py`)
366
+ 5. Commit your changes (`git commit -m 'Add amazing feature'`)
367
+ 6. Push to the branch (`git push origin feature/amazing-feature`)
368
+ 7. Open a Pull Request
369
+
370
+ ## License
371
+
372
+ This project is licensed under the MIT License - see the LICENSE file for details.
373
+
374
+ ## Support
375
+
376
+ - 📧 Email: support@qubesync.com
377
+ - 📖 Documentation: [QubeSync Docs](https://docs.qubesync.com)
378
+ - 🐛 Issues: [GitHub Issues](https://github.com/qubeintegrations/qube_sync_py/issues)
379
+
380
+ ## Changelog
381
+
382
+ ### v0.1.0
383
+ - Initial release
384
+ - Complete QubeSync API client
385
+ - RequestBuilder DSL with multiple syntax styles
386
+ - Comprehensive test suite
387
+ - Webhook security verification
388
+ - Clean module structure with intuitive imports
@@ -0,0 +1,8 @@
1
+ qubesync/__init__.py,sha256=lTF0dXZlrvBB0swsDQRZGP5NWpsf_x1rznlM2a_3W6Q,1205
2
+ qubesync/client.py,sha256=mBZJZ9mO7rVdJVNJkoWZ4ktGKagR4VT1LfIOCVcUbBY,5128
3
+ qubesync/request_builder.py,sha256=ONxD2KQ7EWinnajE5fyy0JP_PDtF-rJmtjjic-MnHO4,6987
4
+ qubesync-0.1.0.dist-info/licenses/LICENSE,sha256=8JPn8nIUYBRz5p6R6QACgOXQHYRYRYctC2kV7hzS5jQ,1065
5
+ qubesync-0.1.0.dist-info/METADATA,sha256=O_P5ZQcwJT_6scCBSCcezHJnbW4uX4FlmEeN25QxGA4,11412
6
+ qubesync-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ qubesync-0.1.0.dist-info/top_level.txt,sha256=9iCcZOQbm8qaIUhIqiPo7G9hYVJCP6M2H50Rtie_8Vo,9
8
+ qubesync-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 QubeSync
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 @@
1
+ qubesync