qbwc-kit 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.
- qbwc_kit-0.1.0/LICENSE +21 -0
- qbwc_kit-0.1.0/PKG-INFO +209 -0
- qbwc_kit-0.1.0/README.md +179 -0
- qbwc_kit-0.1.0/pyproject.toml +55 -0
- qbwc_kit-0.1.0/setup.cfg +4 -0
- qbwc_kit-0.1.0/src/qbwc_kit/__init__.py +69 -0
- qbwc_kit-0.1.0/src/qbwc_kit/qbxml/__init__.py +40 -0
- qbwc_kit-0.1.0/src/qbwc_kit/qbxml/builder.py +202 -0
- qbwc_kit-0.1.0/src/qbwc_kit/qbxml/parser.py +210 -0
- qbwc_kit-0.1.0/src/qbwc_kit/qbxml/types.py +63 -0
- qbwc_kit-0.1.0/src/qbwc_kit/server.py +59 -0
- qbwc_kit-0.1.0/src/qbwc_kit/service.py +221 -0
- qbwc_kit-0.1.0/src/qbwc_kit/session.py +300 -0
- qbwc_kit-0.1.0/src/qbwc_kit/soap.py +151 -0
- qbwc_kit-0.1.0/src/qbwc_kit/testing.py +305 -0
- qbwc_kit-0.1.0/src/qbwc_kit/wsdl.py +180 -0
- qbwc_kit-0.1.0/src/qbwc_kit.egg-info/PKG-INFO +209 -0
- qbwc_kit-0.1.0/src/qbwc_kit.egg-info/SOURCES.txt +27 -0
- qbwc_kit-0.1.0/src/qbwc_kit.egg-info/dependency_links.txt +1 -0
- qbwc_kit-0.1.0/src/qbwc_kit.egg-info/requires.txt +9 -0
- qbwc_kit-0.1.0/src/qbwc_kit.egg-info/top_level.txt +1 -0
- qbwc_kit-0.1.0/tests/test_builder.py +107 -0
- qbwc_kit-0.1.0/tests/test_example_sync.py +105 -0
- qbwc_kit-0.1.0/tests/test_parser.py +139 -0
- qbwc_kit-0.1.0/tests/test_server.py +80 -0
- qbwc_kit-0.1.0/tests/test_service.py +235 -0
- qbwc_kit-0.1.0/tests/test_session.py +253 -0
- qbwc_kit-0.1.0/tests/test_soap.py +104 -0
- qbwc_kit-0.1.0/tests/test_wsdl.py +71 -0
qbwc_kit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eren Altuntas
|
|
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 Eren AltuntasS 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.
|
qbwc_kit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qbwc-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Talk to QuickBooks Desktop over the Web Connector: SOAP callbacks, qbXML, and test doubles.
|
|
5
|
+
Author: Eren Altuntas
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/meister5/qbwc-kit
|
|
8
|
+
Project-URL: Issues, https://github.com/meister5/qbwc-kit/issues
|
|
9
|
+
Keywords: quickbooks,qbxml,web-connector,qbwc,soap,accounting,erp
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: server
|
|
23
|
+
Requires-Dist: fastapi>=0.100; extra == "server"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
26
|
+
Requires-Dist: fastapi>=0.100; extra == "dev"
|
|
27
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# qbwc-kit
|
|
32
|
+
|
|
33
|
+
[](https://github.com/meister5/qbwc-kit/actions/workflows/ci.yml)
|
|
34
|
+
[](pyproject.toml)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
QuickBooks Desktop has no HTTP API. The only supported way in is the **Web Connector**: a
|
|
38
|
+
Windows service that polls *your* SOAP endpoint on a schedule, asks it for qbXML, hands that
|
|
39
|
+
to QuickBooks over COM, and posts the response back.
|
|
40
|
+
|
|
41
|
+
So before you can read a single invoice you have to implement eight SOAP callbacks, serve a
|
|
42
|
+
WSDL, keep a ticket-based session alive across many HTTP round trips, and hand-build XML that
|
|
43
|
+
QuickBooks rejects with unhelpful errors when the element order is wrong. This library is all
|
|
44
|
+
of that, so the only thing left to write is the part that is actually about your data.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from qbwc_kit import QBWCService, StaticAuthenticator, qbxml
|
|
48
|
+
from qbwc_kit.qbxml import QBXMLRequest
|
|
49
|
+
from qbwc_kit.server import create_app
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SyncCustomers:
|
|
53
|
+
name = "customers"
|
|
54
|
+
|
|
55
|
+
def run(self, ctx):
|
|
56
|
+
request = qbxml.query("Customer", max_returned=100, iterator="Start")
|
|
57
|
+
while True:
|
|
58
|
+
result = yield QBXMLRequest([request])
|
|
59
|
+
page = result.first().raise_for_status()
|
|
60
|
+
save(page.records)
|
|
61
|
+
if not page.has_more:
|
|
62
|
+
return
|
|
63
|
+
request.iterator = "Continue"
|
|
64
|
+
request.iterator_id = page.iterator_id
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
service = QBWCService(
|
|
68
|
+
authenticator=StaticAuthenticator("qbwc", "s3cret", [SyncCustomers()])
|
|
69
|
+
)
|
|
70
|
+
app = create_app(service, endpoint_url="https://books.example.com/qbwc")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That `while` loop is the point. Each `yield` suspends the task until the Web Connector comes
|
|
74
|
+
back with the response, so pagination, conditional writes, and read-then-write jobs stay in
|
|
75
|
+
ordinary control flow instead of being flattened into a per-request state machine.
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
Not on PyPI yet — install from the repository:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install git+https://github.com/meister5/qbwc-kit # core: standard library only
|
|
83
|
+
pip install 'qbwc-kit[server] @ git+https://github.com/meister5/qbwc-kit' # adds the FastAPI adapter
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or clone it and `pip install -e '.[dev]'` to run the tests.
|
|
87
|
+
|
|
88
|
+
The core — SOAP, qbXML, sessions, the WSDL generator — has no dependencies outside the
|
|
89
|
+
standard library. FastAPI is only needed if you use `qbwc_kit.server`; the service itself is
|
|
90
|
+
just `dispatch(soap_body) -> soap_body`, so it drops into Flask, Django, or bare WSGI without
|
|
91
|
+
this package caring.
|
|
92
|
+
|
|
93
|
+
## Testing without QuickBooks
|
|
94
|
+
|
|
95
|
+
The awkward part of a Web Connector integration is that exercising it normally requires a
|
|
96
|
+
Windows box, a QuickBooks Desktop install, an open company file, and a human clicking *Update
|
|
97
|
+
Selected*. Which means the failure modes that matter — a task that never terminates, an
|
|
98
|
+
iterator that loops forever, a non-zero status nobody checked — are exactly the ones you only
|
|
99
|
+
find in production.
|
|
100
|
+
|
|
101
|
+
`qbwc_kit.testing` replaces both ends:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from qbwc_kit.testing import FakeQuickBooks, FakeWebConnector, service_transport
|
|
105
|
+
|
|
106
|
+
connector = FakeWebConnector(transport=service_transport(service),
|
|
107
|
+
username="qbwc", password="s3cret")
|
|
108
|
+
quickbooks = FakeQuickBooks(entities={"Customer": [{"ListID": "1", "Name": "Acme"}]})
|
|
109
|
+
|
|
110
|
+
result = connector.run_update(quickbooks)
|
|
111
|
+
|
|
112
|
+
assert result.progress[-1] == 100
|
|
113
|
+
assert quickbooks.seen[0].count("iterator=\"Start\"") == 1
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`FakeWebConnector` replays the real call sequence — `clientVersion`, `authenticate`, the
|
|
117
|
+
`sendRequestXML`/`receiveResponseXML` loop, `closeConnection` — and raises if the session
|
|
118
|
+
doesn't terminate, which catches runaway iterators in milliseconds instead of in the
|
|
119
|
+
connector's log. `FakeQuickBooks` answers qbXML the way QuickBooks does: paged iterators,
|
|
120
|
+
`MaxReturned`, status 1 for an empty result, status 3100 for an unsupported request.
|
|
121
|
+
|
|
122
|
+
## The parts that bite
|
|
123
|
+
|
|
124
|
+
**Status codes ride on successful envelopes.** A request QuickBooks refused (status 3100,
|
|
125
|
+
"not available in this edition") comes back as a perfectly well-formed response document with
|
|
126
|
+
no records in it. If you parse for rows and ignore the status, an unsupported request is
|
|
127
|
+
indistinguishable from an empty table, and a cache built on top of it degrades into empty
|
|
128
|
+
results without anything logging an error. Every `Response` here carries its status, `ok`
|
|
129
|
+
distinguishes "nothing found" (status 1, genuinely fine) from "never ran", and
|
|
130
|
+
`raise_for_status()` is one call away.
|
|
131
|
+
|
|
132
|
+
**Element order is part of the schema.** qbXML is a sequence, not a bag. `MaxReturned` before
|
|
133
|
+
the filters, `EditSequence` before the fields being changed. The builders emit the right order
|
|
134
|
+
so you pass a dict and stop thinking about it.
|
|
135
|
+
|
|
136
|
+
**Writes use optimistic concurrency.** Every `Mod` request must carry the `EditSequence` from
|
|
137
|
+
the last read, and a stale one is rejected rather than silently clobbering another user's
|
|
138
|
+
edit. `qbxml.mod()` requires it as a keyword argument for that reason.
|
|
139
|
+
|
|
140
|
+
**Iterators only exist on some entities.** Asking for one on an entity that doesn't support it
|
|
141
|
+
is an opaque parse error from QuickBooks, so `qbwc-kit` raises at build time instead.
|
|
142
|
+
|
|
143
|
+
**Returning 100 ends the session.** `receiveResponseXML` returns percent complete, and a
|
|
144
|
+
progress calculation that rounds up too early silently truncates the sync. `Session.progress()`
|
|
145
|
+
caps at 99 until the work is genuinely done.
|
|
146
|
+
|
|
147
|
+
**An unknown ticket is normal.** Restart the server mid-update and the next callback arrives
|
|
148
|
+
with a ticket that no longer exists. Faulting makes the Web Connector retry forever; this
|
|
149
|
+
service tells it the session is over instead.
|
|
150
|
+
|
|
151
|
+
## Layout
|
|
152
|
+
|
|
153
|
+
| Module | What it does |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `qbwc_kit.soap` | The small SOAP 1.1 slice QBWC actually uses — parse a call, build a response or fault |
|
|
156
|
+
| `qbwc_kit.qbxml` | Request builders (`query`, `add`, `mod`) and a status-aware response parser |
|
|
157
|
+
| `qbwc_kit.session` | Generator-based tasks, the request/response loop, ticket store with TTL |
|
|
158
|
+
| `qbwc_kit.service` | The eight callbacks, framework-agnostic |
|
|
159
|
+
| `qbwc_kit.wsdl` | WSDL generation, plus the `.qwc` file users import into the connector |
|
|
160
|
+
| `qbwc_kit.server` | Optional FastAPI adapter |
|
|
161
|
+
| `qbwc_kit.testing` | `FakeWebConnector`, `FakeQuickBooks` |
|
|
162
|
+
|
|
163
|
+
## Example
|
|
164
|
+
|
|
165
|
+
[`examples/sync_to_sqlite.py`](examples/sync_to_sqlite.py) is a complete integration: it
|
|
166
|
+
mirrors customers and invoices into SQLite, syncs incrementally off a stored watermark, and
|
|
167
|
+
generates the `.qwc` file to import into the Web Connector.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
python examples/sync_to_sqlite.py --write-qwc mirror.qwc # generate the connector file
|
|
171
|
+
python examples/sync_to_sqlite.py --url https://books.example.com/qbwc
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Two details in there are worth stealing. The watermark only advances after every page has been
|
|
175
|
+
written, so an interrupted run repeats work instead of skipping it. And it is rewound by a
|
|
176
|
+
minute, because QuickBooks stamps `TimeModified` from the workstation clock — a record saved
|
|
177
|
+
during the sync can otherwise land just behind the watermark and never be picked up again.
|
|
178
|
+
|
|
179
|
+
The example is covered by the test suite, so it can't drift from the library.
|
|
180
|
+
|
|
181
|
+
## Deployment notes
|
|
182
|
+
|
|
183
|
+
- QBWC will not accept an endpoint on plain HTTP unless it is `localhost`. Use TLS.
|
|
184
|
+
- `soap:address` in the WSDL has to be the URL the connector actually posts to. Behind a
|
|
185
|
+
reverse proxy, that is the public URL, not `http://localhost:8000`.
|
|
186
|
+
- `OwnerID` and `FileID` in the `.qwc` file identify your integration to QuickBooks.
|
|
187
|
+
Generate them once and keep them; changing them forces every user to re-authorise.
|
|
188
|
+
- The connector authenticates with a password stored in Windows' credential store. Treat the
|
|
189
|
+
`authenticate` callback as a real auth boundary — `StaticAuthenticator` compares with
|
|
190
|
+
`secrets.compare_digest`, and anything you write should too.
|
|
191
|
+
|
|
192
|
+
## Scope
|
|
193
|
+
|
|
194
|
+
Read and write access to list and transaction entities through qbXML, which is what the Web
|
|
195
|
+
Connector exposes. Not covered: QuickBooks Online (that has a real REST API — use it),
|
|
196
|
+
qbposXML for Point of Sale, and the direct COM `QBFC` interface, which needs code running on
|
|
197
|
+
the same Windows machine as QuickBooks.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pip install -e '.[dev]'
|
|
203
|
+
pytest -q
|
|
204
|
+
ruff check . && ruff format --check .
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
qbwc_kit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# qbwc-kit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/meister5/qbwc-kit/actions/workflows/ci.yml)
|
|
4
|
+
[](pyproject.toml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
QuickBooks Desktop has no HTTP API. The only supported way in is the **Web Connector**: a
|
|
8
|
+
Windows service that polls *your* SOAP endpoint on a schedule, asks it for qbXML, hands that
|
|
9
|
+
to QuickBooks over COM, and posts the response back.
|
|
10
|
+
|
|
11
|
+
So before you can read a single invoice you have to implement eight SOAP callbacks, serve a
|
|
12
|
+
WSDL, keep a ticket-based session alive across many HTTP round trips, and hand-build XML that
|
|
13
|
+
QuickBooks rejects with unhelpful errors when the element order is wrong. This library is all
|
|
14
|
+
of that, so the only thing left to write is the part that is actually about your data.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from qbwc_kit import QBWCService, StaticAuthenticator, qbxml
|
|
18
|
+
from qbwc_kit.qbxml import QBXMLRequest
|
|
19
|
+
from qbwc_kit.server import create_app
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SyncCustomers:
|
|
23
|
+
name = "customers"
|
|
24
|
+
|
|
25
|
+
def run(self, ctx):
|
|
26
|
+
request = qbxml.query("Customer", max_returned=100, iterator="Start")
|
|
27
|
+
while True:
|
|
28
|
+
result = yield QBXMLRequest([request])
|
|
29
|
+
page = result.first().raise_for_status()
|
|
30
|
+
save(page.records)
|
|
31
|
+
if not page.has_more:
|
|
32
|
+
return
|
|
33
|
+
request.iterator = "Continue"
|
|
34
|
+
request.iterator_id = page.iterator_id
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
service = QBWCService(
|
|
38
|
+
authenticator=StaticAuthenticator("qbwc", "s3cret", [SyncCustomers()])
|
|
39
|
+
)
|
|
40
|
+
app = create_app(service, endpoint_url="https://books.example.com/qbwc")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That `while` loop is the point. Each `yield` suspends the task until the Web Connector comes
|
|
44
|
+
back with the response, so pagination, conditional writes, and read-then-write jobs stay in
|
|
45
|
+
ordinary control flow instead of being flattened into a per-request state machine.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
Not on PyPI yet — install from the repository:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install git+https://github.com/meister5/qbwc-kit # core: standard library only
|
|
53
|
+
pip install 'qbwc-kit[server] @ git+https://github.com/meister5/qbwc-kit' # adds the FastAPI adapter
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or clone it and `pip install -e '.[dev]'` to run the tests.
|
|
57
|
+
|
|
58
|
+
The core — SOAP, qbXML, sessions, the WSDL generator — has no dependencies outside the
|
|
59
|
+
standard library. FastAPI is only needed if you use `qbwc_kit.server`; the service itself is
|
|
60
|
+
just `dispatch(soap_body) -> soap_body`, so it drops into Flask, Django, or bare WSGI without
|
|
61
|
+
this package caring.
|
|
62
|
+
|
|
63
|
+
## Testing without QuickBooks
|
|
64
|
+
|
|
65
|
+
The awkward part of a Web Connector integration is that exercising it normally requires a
|
|
66
|
+
Windows box, a QuickBooks Desktop install, an open company file, and a human clicking *Update
|
|
67
|
+
Selected*. Which means the failure modes that matter — a task that never terminates, an
|
|
68
|
+
iterator that loops forever, a non-zero status nobody checked — are exactly the ones you only
|
|
69
|
+
find in production.
|
|
70
|
+
|
|
71
|
+
`qbwc_kit.testing` replaces both ends:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from qbwc_kit.testing import FakeQuickBooks, FakeWebConnector, service_transport
|
|
75
|
+
|
|
76
|
+
connector = FakeWebConnector(transport=service_transport(service),
|
|
77
|
+
username="qbwc", password="s3cret")
|
|
78
|
+
quickbooks = FakeQuickBooks(entities={"Customer": [{"ListID": "1", "Name": "Acme"}]})
|
|
79
|
+
|
|
80
|
+
result = connector.run_update(quickbooks)
|
|
81
|
+
|
|
82
|
+
assert result.progress[-1] == 100
|
|
83
|
+
assert quickbooks.seen[0].count("iterator=\"Start\"") == 1
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`FakeWebConnector` replays the real call sequence — `clientVersion`, `authenticate`, the
|
|
87
|
+
`sendRequestXML`/`receiveResponseXML` loop, `closeConnection` — and raises if the session
|
|
88
|
+
doesn't terminate, which catches runaway iterators in milliseconds instead of in the
|
|
89
|
+
connector's log. `FakeQuickBooks` answers qbXML the way QuickBooks does: paged iterators,
|
|
90
|
+
`MaxReturned`, status 1 for an empty result, status 3100 for an unsupported request.
|
|
91
|
+
|
|
92
|
+
## The parts that bite
|
|
93
|
+
|
|
94
|
+
**Status codes ride on successful envelopes.** A request QuickBooks refused (status 3100,
|
|
95
|
+
"not available in this edition") comes back as a perfectly well-formed response document with
|
|
96
|
+
no records in it. If you parse for rows and ignore the status, an unsupported request is
|
|
97
|
+
indistinguishable from an empty table, and a cache built on top of it degrades into empty
|
|
98
|
+
results without anything logging an error. Every `Response` here carries its status, `ok`
|
|
99
|
+
distinguishes "nothing found" (status 1, genuinely fine) from "never ran", and
|
|
100
|
+
`raise_for_status()` is one call away.
|
|
101
|
+
|
|
102
|
+
**Element order is part of the schema.** qbXML is a sequence, not a bag. `MaxReturned` before
|
|
103
|
+
the filters, `EditSequence` before the fields being changed. The builders emit the right order
|
|
104
|
+
so you pass a dict and stop thinking about it.
|
|
105
|
+
|
|
106
|
+
**Writes use optimistic concurrency.** Every `Mod` request must carry the `EditSequence` from
|
|
107
|
+
the last read, and a stale one is rejected rather than silently clobbering another user's
|
|
108
|
+
edit. `qbxml.mod()` requires it as a keyword argument for that reason.
|
|
109
|
+
|
|
110
|
+
**Iterators only exist on some entities.** Asking for one on an entity that doesn't support it
|
|
111
|
+
is an opaque parse error from QuickBooks, so `qbwc-kit` raises at build time instead.
|
|
112
|
+
|
|
113
|
+
**Returning 100 ends the session.** `receiveResponseXML` returns percent complete, and a
|
|
114
|
+
progress calculation that rounds up too early silently truncates the sync. `Session.progress()`
|
|
115
|
+
caps at 99 until the work is genuinely done.
|
|
116
|
+
|
|
117
|
+
**An unknown ticket is normal.** Restart the server mid-update and the next callback arrives
|
|
118
|
+
with a ticket that no longer exists. Faulting makes the Web Connector retry forever; this
|
|
119
|
+
service tells it the session is over instead.
|
|
120
|
+
|
|
121
|
+
## Layout
|
|
122
|
+
|
|
123
|
+
| Module | What it does |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `qbwc_kit.soap` | The small SOAP 1.1 slice QBWC actually uses — parse a call, build a response or fault |
|
|
126
|
+
| `qbwc_kit.qbxml` | Request builders (`query`, `add`, `mod`) and a status-aware response parser |
|
|
127
|
+
| `qbwc_kit.session` | Generator-based tasks, the request/response loop, ticket store with TTL |
|
|
128
|
+
| `qbwc_kit.service` | The eight callbacks, framework-agnostic |
|
|
129
|
+
| `qbwc_kit.wsdl` | WSDL generation, plus the `.qwc` file users import into the connector |
|
|
130
|
+
| `qbwc_kit.server` | Optional FastAPI adapter |
|
|
131
|
+
| `qbwc_kit.testing` | `FakeWebConnector`, `FakeQuickBooks` |
|
|
132
|
+
|
|
133
|
+
## Example
|
|
134
|
+
|
|
135
|
+
[`examples/sync_to_sqlite.py`](examples/sync_to_sqlite.py) is a complete integration: it
|
|
136
|
+
mirrors customers and invoices into SQLite, syncs incrementally off a stored watermark, and
|
|
137
|
+
generates the `.qwc` file to import into the Web Connector.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
python examples/sync_to_sqlite.py --write-qwc mirror.qwc # generate the connector file
|
|
141
|
+
python examples/sync_to_sqlite.py --url https://books.example.com/qbwc
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Two details in there are worth stealing. The watermark only advances after every page has been
|
|
145
|
+
written, so an interrupted run repeats work instead of skipping it. And it is rewound by a
|
|
146
|
+
minute, because QuickBooks stamps `TimeModified` from the workstation clock — a record saved
|
|
147
|
+
during the sync can otherwise land just behind the watermark and never be picked up again.
|
|
148
|
+
|
|
149
|
+
The example is covered by the test suite, so it can't drift from the library.
|
|
150
|
+
|
|
151
|
+
## Deployment notes
|
|
152
|
+
|
|
153
|
+
- QBWC will not accept an endpoint on plain HTTP unless it is `localhost`. Use TLS.
|
|
154
|
+
- `soap:address` in the WSDL has to be the URL the connector actually posts to. Behind a
|
|
155
|
+
reverse proxy, that is the public URL, not `http://localhost:8000`.
|
|
156
|
+
- `OwnerID` and `FileID` in the `.qwc` file identify your integration to QuickBooks.
|
|
157
|
+
Generate them once and keep them; changing them forces every user to re-authorise.
|
|
158
|
+
- The connector authenticates with a password stored in Windows' credential store. Treat the
|
|
159
|
+
`authenticate` callback as a real auth boundary — `StaticAuthenticator` compares with
|
|
160
|
+
`secrets.compare_digest`, and anything you write should too.
|
|
161
|
+
|
|
162
|
+
## Scope
|
|
163
|
+
|
|
164
|
+
Read and write access to list and transaction entities through qbXML, which is what the Web
|
|
165
|
+
Connector exposes. Not covered: QuickBooks Online (that has a real REST API — use it),
|
|
166
|
+
qbposXML for Point of Sale, and the direct COM `QBFC` interface, which needs code running on
|
|
167
|
+
the same Windows machine as QuickBooks.
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
pip install -e '.[dev]'
|
|
173
|
+
pytest -q
|
|
174
|
+
ruff check . && ruff format --check .
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qbwc-kit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Talk to QuickBooks Desktop over the Web Connector: SOAP callbacks, qbXML, and test doubles."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Eren Altuntas" }]
|
|
13
|
+
keywords = ["quickbooks", "qbxml", "web-connector", "qbwc", "soap", "accounting", "erp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
server = ["fastapi>=0.100"]
|
|
29
|
+
dev = ["pytest>=7", "fastapi>=0.100", "httpx>=0.24", "ruff>=0.4"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/meister5/qbwc-kit"
|
|
33
|
+
Issues = "https://github.com/meister5/qbwc-kit/issues"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py310"
|
|
45
|
+
src = ["src", "tests", "examples"]
|
|
46
|
+
# Code blocks in the README are hand-aligned for reading; recent ruff versions
|
|
47
|
+
# format markdown too, and that alignment is not worth a red CI badge.
|
|
48
|
+
extend-exclude = ["*.md"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
52
|
+
ignore = ["E501", "B008"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint.per-file-ignores]
|
|
55
|
+
"tests/*" = ["E402"]
|
qbwc_kit-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""qbwc-kit: talk to QuickBooks Desktop over the Web Connector.
|
|
2
|
+
|
|
3
|
+
QuickBooks Desktop has no HTTP API. The only supported way in is the Web
|
|
4
|
+
Connector: a Windows service that polls *your* SOAP endpoint on a schedule,
|
|
5
|
+
asks it for qbXML, hands that to QuickBooks over COM, and posts the response
|
|
6
|
+
back. Every integration therefore has to implement the same eight callbacks and
|
|
7
|
+
the same request/response loop before it can read a single invoice.
|
|
8
|
+
|
|
9
|
+
This package is that plumbing:
|
|
10
|
+
|
|
11
|
+
* :mod:`qbwc_kit.soap` - the small SOAP slice QBWC actually uses
|
|
12
|
+
* :mod:`qbwc_kit.qbxml` - qbXML request building and status-aware parsing
|
|
13
|
+
* :mod:`qbwc_kit.session` - generator-based tasks spanning many round trips
|
|
14
|
+
* :mod:`qbwc_kit.service` - the eight callbacks, framework-agnostic
|
|
15
|
+
* :mod:`qbwc_kit.server` - optional FastAPI adapter and WSDL hosting
|
|
16
|
+
* :mod:`qbwc_kit.testing` - a fake Web Connector and a fake QuickBooks
|
|
17
|
+
|
|
18
|
+
The core has no dependencies outside the standard library.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from . import qbxml
|
|
22
|
+
from .qbxml import (
|
|
23
|
+
QBXMLRequest,
|
|
24
|
+
QBXMLStatusError,
|
|
25
|
+
Request,
|
|
26
|
+
Response,
|
|
27
|
+
ResponseSet,
|
|
28
|
+
add,
|
|
29
|
+
mod,
|
|
30
|
+
parse_response,
|
|
31
|
+
query,
|
|
32
|
+
)
|
|
33
|
+
from .service import QBWCService
|
|
34
|
+
from .session import (
|
|
35
|
+
Authenticator,
|
|
36
|
+
Session,
|
|
37
|
+
SessionStore,
|
|
38
|
+
SimpleTask,
|
|
39
|
+
StaticAuthenticator,
|
|
40
|
+
Task,
|
|
41
|
+
TaskContext,
|
|
42
|
+
)
|
|
43
|
+
from .wsdl import build_qwc, build_wsdl
|
|
44
|
+
|
|
45
|
+
__version__ = "0.1.0"
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Authenticator",
|
|
49
|
+
"QBWCService",
|
|
50
|
+
"QBXMLRequest",
|
|
51
|
+
"QBXMLStatusError",
|
|
52
|
+
"Request",
|
|
53
|
+
"Response",
|
|
54
|
+
"ResponseSet",
|
|
55
|
+
"Session",
|
|
56
|
+
"SessionStore",
|
|
57
|
+
"SimpleTask",
|
|
58
|
+
"StaticAuthenticator",
|
|
59
|
+
"Task",
|
|
60
|
+
"TaskContext",
|
|
61
|
+
"__version__",
|
|
62
|
+
"add",
|
|
63
|
+
"build_qwc",
|
|
64
|
+
"build_wsdl",
|
|
65
|
+
"mod",
|
|
66
|
+
"parse_response",
|
|
67
|
+
"qbxml",
|
|
68
|
+
"query",
|
|
69
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""qbXML request building and response parsing."""
|
|
2
|
+
|
|
3
|
+
from .builder import QBXMLRequest, Request, add, element, elements, mod, query, ref
|
|
4
|
+
from .parser import (
|
|
5
|
+
QBXMLParseError,
|
|
6
|
+
QBXMLStatusError,
|
|
7
|
+
Response,
|
|
8
|
+
ResponseSet,
|
|
9
|
+
parse_response,
|
|
10
|
+
)
|
|
11
|
+
from .types import (
|
|
12
|
+
ITERATOR_ENTITIES,
|
|
13
|
+
STATUS_NOTHING_FOUND,
|
|
14
|
+
STATUS_OK,
|
|
15
|
+
STATUS_UNSUPPORTED_REQUEST,
|
|
16
|
+
OnError,
|
|
17
|
+
Severity,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ITERATOR_ENTITIES",
|
|
22
|
+
"OnError",
|
|
23
|
+
"QBXMLParseError",
|
|
24
|
+
"QBXMLRequest",
|
|
25
|
+
"QBXMLStatusError",
|
|
26
|
+
"Request",
|
|
27
|
+
"Response",
|
|
28
|
+
"ResponseSet",
|
|
29
|
+
"STATUS_NOTHING_FOUND",
|
|
30
|
+
"STATUS_OK",
|
|
31
|
+
"STATUS_UNSUPPORTED_REQUEST",
|
|
32
|
+
"Severity",
|
|
33
|
+
"add",
|
|
34
|
+
"element",
|
|
35
|
+
"elements",
|
|
36
|
+
"mod",
|
|
37
|
+
"parse_response",
|
|
38
|
+
"query",
|
|
39
|
+
"ref",
|
|
40
|
+
]
|