Skip to content

Commit 925cd87

Browse files
authored
HGI-10886: add shared qbwc-common library for tap and target (#1)
* HGI-10886: add shared qbwc-common library for tap and target * Bugbot fixes and tests improvements * Add qbxml schema load log
1 parent d19ccee commit 925cd87

24 files changed

Lines changed: 28186 additions & 1 deletion

.github/workflows/lint.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Lint
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- '**.py'
7+
- 'tests/fixtures/**'
8+
- 'qbwc_common/qbd_xml_schemas/**'
9+
- 'ruff.toml'
10+
- 'tox.ini'
11+
- 'pyproject.toml'
12+
13+
jobs:
14+
lint:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v3
22+
with:
23+
persist-credentials: false
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v3
27+
with:
28+
python-version: '3.10'
29+
30+
- name: Install tox
31+
run: pip install tox
32+
33+
- name: Run lint
34+
run: tox

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Secrets and internal config files
2+
.secrets/*
3+
4+
# Byte-compiled / optimized / DLL files
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
9+
# Distribution / packaging
10+
build/
11+
dist/
12+
*.egg-info/
13+
.eggs/
14+
15+
# Unit test / coverage reports
16+
.tox/
17+
.pytest_cache/
18+
.coverage
19+
htmlcov/
20+
21+
# Environments
22+
.env
23+
.venv
24+
venv/
25+
26+
.vscode/
27+
.DS_Store

README.md

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,144 @@
11
# qbwc-common
2-
QBWC logic shared between tap and target
2+
3+
Shared Python library for QuickBooks Desktop Web Connector (QBWC) used by [`tap-qbwc`](https://github.com/hotgluexyz/tap-qbwc) and [`target-qbwc`](https://github.com/hotgluexyz/target-qbwc).
4+
5+
Repository: [github.com/hotgluexyz/qbwc-common](https://github.com/hotgluexyz/qbwc-common)
6+
7+
It centralises transport to the QBWC SOAP service, the bundled Intuit qbXML 13.0 XSD set, and the encode/decode helpers both connectors build on.
8+
9+
## What's included
10+
11+
| Module | Responsibility |
12+
|---|---|
13+
| `client.py` | `QBWCClient`: authenticate, enqueue qbXML, long-poll for responses |
14+
| `qbxml.py` | Encode requests and decode responses against the XSD; `merge_request_elements`, `normalize_rs_list`, `parse_rs_element` |
15+
| `mod_merge.py` | XSD-driven filtering for `*Mod` payloads (`get_mod_element_names`, `filter_dict_for_mod`, `get_ret_only_field_names`) |
16+
| `qbxml_encode_errors.py` | User-facing messages for XSD encode validation failures (`format_encode_validation_error`) |
17+
| `config.py` | Shared config contract, defaults, and sandbox/production base URLs |
18+
| `exceptions.py` | `QBWC*` transport errors and `QBXML*` encode/decode errors |
19+
| `qbd_xml_schemas/` | Bundled `qbxmlops130.xsd` and dependencies (qbXML 13.0) |
20+
21+
Public exports live in `qbwc_common.__init__`. Import from `qbwc_common` in connector code.
22+
23+
## Installation
24+
25+
```bash
26+
git clone https://github.com/hotgluexyz/qbwc-common.git
27+
cd qbwc-common
28+
python -m venv .venv
29+
source .venv/bin/activate
30+
pip install -e ".[dev]"
31+
```
32+
33+
Connectors declare a dependency on this package. For local work on both repos, clone `qbwc-common` as a sibling directory and install it before the tap or target.
34+
35+
## Configuration
36+
37+
Shared keys consumed by `QBWCClient` (connectors may add their own settings on top):
38+
39+
| Key | Required | Default | Description |
40+
|---|---|---|---|
41+
| `token` | yes | | Base64 connector token for the QBWC SOAP service |
42+
| `request_timeout` | no | `1200` | Seconds a QBWC request may take before timeout |
43+
| `is_sandbox` | no | `false` | Use `qbwc-qa.hotglue.xyz` instead of `qbwc.hotglue.com` |
44+
| `qbwc_is_alive_timeout` | no | `3600` | Timeout for the optional `HostQueryRq` liveness check |
45+
46+
Example:
47+
48+
```json
49+
{
50+
"token": "your_base64_connector_token",
51+
"is_sandbox": true,
52+
"request_timeout": 1200
53+
}
54+
```
55+
56+
## Usage
57+
58+
### Client and qbXML round-trip
59+
60+
```python
61+
from qbwc_common import QBWCClient, load_qbd_xml_schemas
62+
63+
schemas = load_qbd_xml_schemas()
64+
client = QBWCClient(config, schemas, logger=my_logger)
65+
client.create_session()
66+
client.check_qbwc_is_alive() # optional HostQueryRq ping
67+
response = client.make_request({"HostQueryRq": {}})
68+
```
69+
70+
`make_request` encodes dict-shaped request elements, sends them through QBWC, and returns the decoded `QBXMLMsgsRs` body. Use `send_qbxml` when you already have a qbXML string.
71+
72+
### Batched writes
73+
74+
Pass a list of single-key request dicts. Set `on_error="continueOnError"` so one failed record does not abort the batch. Give each record a `@requestID` attribute; QuickBooks echoes it on the matching `*Rs`.
75+
76+
For write responses with partial `*Ret` payloads, pass `decode_validation="skip"` (the tap defaults to strict decode on reads).
77+
78+
```python
79+
from qbwc_common import ON_ERROR_CONTINUE
80+
81+
requests = [
82+
{
83+
"CustomerAddRq": {
84+
"@requestID": "0",
85+
"CustomerAdd": {"Name": "Customer A", "CompanyName": "A Co"},
86+
}
87+
},
88+
{
89+
"CustomerAddRq": {
90+
"@requestID": "1",
91+
"CustomerAdd": {"Name": "Customer B", "CompanyName": "B Co"},
92+
}
93+
},
94+
]
95+
response = client.make_request(
96+
requests,
97+
on_error=ON_ERROR_CONTINUE,
98+
decode_validation="skip",
99+
)
100+
```
101+
102+
### Mod payload merge
103+
104+
After a query returns a `*Ret` record, strip read-only fields before overlaying incoming changes onto a `*Mod` payload:
105+
106+
```python
107+
from qbwc_common import filter_dict_for_mod, get_mod_element_names
108+
109+
allowed = get_mod_element_names(schemas, "CustomerMod")
110+
incoming = filter_dict_for_mod(payload, allowed)
111+
merged = {**filter_dict_for_mod(existing_ret, allowed), **incoming}
112+
merged["ListID"] = existing_ret["ListID"]
113+
merged["EditSequence"] = existing_ret["EditSequence"]
114+
```
115+
116+
`get_ret_only_field_names` returns `*Ret` fields that are not valid on the matching `*Mod` element.
117+
118+
### Response parsing
119+
120+
```python
121+
from qbwc_common import normalize_rs_list, parse_rs_element
122+
123+
for rs in normalize_rs_list(response, "CustomerAddRs"):
124+
parsed = parse_rs_element(rs)
125+
# parsed: request_id, status_code, status_message, entity_key, entity
126+
```
127+
128+
## Development
129+
130+
```bash
131+
tox
132+
```
133+
134+
Or run tools directly:
135+
136+
```bash
137+
.venv/bin/ruff check .
138+
.venv/bin/pytest tests/
139+
```
140+
141+
Related repos:
142+
143+
- [`tap-qbwc`](https://github.com/hotgluexyz/tap-qbwc): Singer tap for reading from QuickBooks Desktop via QBWC
144+
- [`target-qbwc`](https://github.com/hotgluexyz/target-qbwc): Singer target for writing QuickBooks-shaped records via QBWC

pyproject.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[build-system]
2+
requires = ["setuptools>=61"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "qbwc-common"
7+
version = "0.1.0"
8+
description = "Shared QBWC transport, QBXML helpers and Intuit XSD bundle for tap-qbwc and target-qbwc."
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = { text = "Apache-2.0" }
12+
authors = [{ name = "Hotglue" }]
13+
dependencies = [
14+
"backoff>=1.8.0,<2.0",
15+
"requests>=2.28.0",
16+
"xmlschema>=4.3.2",
17+
]
18+
19+
[project.optional-dependencies]
20+
dev = [
21+
"pytest>=7.0",
22+
"ruff>=0.15.0,<0.16.0",
23+
"tox>=4.0",
24+
]
25+
26+
[tool.setuptools.packages.find]
27+
where = ["."]
28+
include = ["qbwc_common*"]
29+
30+
[tool.setuptools.package-data]
31+
qbwc_common = ["qbd_xml_schemas/*.xsd"]

qbwc_common/__init__.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Shared QBWC transport and QBXML helpers for tap-qbwc and target-qbwc."""
2+
3+
from qbwc_common.client import QBWCClient
4+
from qbwc_common.config import (
5+
DEFAULT_IS_SANDBOX,
6+
DEFAULT_QBWC_IS_ALIVE_TIMEOUT,
7+
DEFAULT_REQUEST_TIMEOUT,
8+
OPTIONAL_CONFIG_KEYS,
9+
PRODUCTION_BASE_URL,
10+
REQUIRED_CONFIG_KEYS,
11+
SANDBOX_BASE_URL,
12+
normalize_config,
13+
resolve_base_url,
14+
)
15+
from qbwc_common.exceptions import (
16+
QBWCAuthenticationError,
17+
QBWCEnqueueError,
18+
QBWCError,
19+
QBWCNotAuthenticatedError,
20+
QBWCQueueFullError,
21+
QBWCRequestError,
22+
QBWCRequestTimeoutError,
23+
QBWCUnknownPollStatusError,
24+
QBXMLDecodeError,
25+
QBXMLStatusError,
26+
QBXMLEncodeError,
27+
)
28+
from qbwc_common.qbxml_encode_errors import format_encode_validation_error
29+
from qbwc_common.mod_merge import (
30+
filter_dict_for_mod,
31+
get_mod_element_names,
32+
get_ret_only_field_names,
33+
)
34+
from qbwc_common.qbxml import (
35+
DEFAULT_DECODE_VALIDATION,
36+
ON_ERROR_CONTINUE,
37+
ON_ERROR_STOP,
38+
QBXML_HEADER,
39+
decode_response,
40+
encode_requests,
41+
load_qbd_xml_schemas,
42+
merge_request_elements,
43+
normalize_rs_list,
44+
parse_rs_element,
45+
)
46+
47+
__all__ = [
48+
"DEFAULT_DECODE_VALIDATION",
49+
"DEFAULT_IS_SANDBOX",
50+
"DEFAULT_QBWC_IS_ALIVE_TIMEOUT",
51+
"DEFAULT_REQUEST_TIMEOUT",
52+
"ON_ERROR_CONTINUE",
53+
"ON_ERROR_STOP",
54+
"OPTIONAL_CONFIG_KEYS",
55+
"PRODUCTION_BASE_URL",
56+
"QBWCClient",
57+
"QBWCAuthenticationError",
58+
"QBWCEnqueueError",
59+
"QBWCError",
60+
"QBWCNotAuthenticatedError",
61+
"QBWCQueueFullError",
62+
"QBWCRequestError",
63+
"QBWCRequestTimeoutError",
64+
"QBWCUnknownPollStatusError",
65+
"QBXMLDecodeError",
66+
"QBXML_HEADER",
67+
"QBXMLStatusError",
68+
"QBXMLEncodeError",
69+
"REQUIRED_CONFIG_KEYS",
70+
"SANDBOX_BASE_URL",
71+
"decode_response",
72+
"encode_requests",
73+
"filter_dict_for_mod",
74+
"format_encode_validation_error",
75+
"get_mod_element_names",
76+
"get_ret_only_field_names",
77+
"load_qbd_xml_schemas",
78+
"merge_request_elements",
79+
"normalize_config",
80+
"normalize_rs_list",
81+
"parse_rs_element",
82+
"resolve_base_url",
83+
]

0 commit comments

Comments
 (0)