# Native appraisal sync for Python

Copy your dealership's saved native appraisals into a **private** SQLite database. This receiver reads inspection facts, seller answers and linked-stock detail; it does not create an appraisal, execute a payment or download files.

## Start

1. Use Python **3.10 or newer**. Save `signal_trade_stock_sync.py` and `signal_trade_native_appraisal_sqlite_example.py` together. Only the standard library is used.
2. In Business → Connections, choose Appraisals → **Manage inspections and purchase decisions**. Keep `SIGNAL_TRADE_API_KEY` in the server environment. Ordinary `appraisal:workflow` permission is enough; file transfer remains separately opt-in.
3. Run `python signal_trade_native_appraisal_sqlite_example.py`.

The result reports the committed record count and opaque checkpoint, without printing seller facts or the bearer token. The default database is `signal-native-appraisals.db`. Set `SIGNAL_TRADE_SQLITE_PATH` for another location and `SIGNAL_TRADE_BASE_URL` only when deliberately using another approved server.

The old `appraisal:read` and `appraisal:manage` scopes are for safe figures, calculations and figure imports. They cannot discover or read private native inspections. An `appraisal:workflow_evidence` key can also run this mirror, but **the mirror never calls file endpoints**. Protect the database and backups; current detail includes private seller contacts and notes.

## Keep it current

Run the command again for changes. Each invocation runs once; your receiving system chooses a schedule within the API's hourly limits. The first run performs a complete inventory reconciliation, repeated at least daily **when invoked**. No scheduler is installed.

| Option | Use |
| --- | --- |
| `--status` | Read local counts, binding and checkpoint without a key or API request. |
| `--reconcile` | Replace from the complete current inventory and catch up changes now. |
| `--rebind` | Deliberately accept a changed key/scope for the same dealership and fully rebuild. |

For example: `python signal_trade_native_appraisal_sqlite_example.py --reconcile`.

## Recovery and storage

The database binds to the normalized API origin, business ID, key ID, non-secret prefix and exact scope. Another dealership or server needs a separate database; `--rebind` cannot change either. Existing unbound native tables need deliberate `--rebind`. The bearer key is never stored in SQLite or status output.

One `BEGIN IMMEDIATE` transaction with WAL and full synchronization covers replacement, hydration, removals, binding and checkpoint. A concurrent writer fails; readers retain the previous committed copy. Failed requests, malformed responses, callback errors, denied access and interrupted runs roll back. The final account check immediately precedes commit. Retry after resolving the error.

Node's native appraisal example shares the same table and state format. Stock mirrors have separate tables and a different default database. Rebuilding the mirror does not erase your backups, exports or other copies.

## Client helpers

| Python helper | Job |
| --- | --- |
| `get_native_appraisal(api_key, appraisal_id, base_url)` | Read exact current private detail, with no file bytes. |
| `get_native_appraisals(api_key, ids, base_url)` | Read one to ten complete current envelopes in requested order, all or nothing. |
| `load_all_native_appraisals(api_key, on_appraisal, base_url)` | Discover all compact pages, hydrate detail and return `{syncCheckpoint, recordsUpdated}`. |
| `sync_native_appraisal_records(api_key, checkpoint, on_record, on_delete, save_checkpoint, base_url)` | Drain a fixed sequence window, hydrate touched IDs and save checkpoint last. |

`base_url` defaults to `https://signalautomotive.uk`. The inventory helper never saves its own checkpoint: use its `syncCheckpoint` to catch up the feed before committing. The runnable SQLite receiver implements this. Custom callbacks must durably and idempotently apply `detail.appraisal.id`; change callbacks receive `(detail, latest_event)` and removal callbacks `(appraisal_id, latest_event)`. Commit all records and checkpoint together in your own transaction. If callbacks commit separately, a failed later callback can replay earlier ones.

The complete current-ID batch is validated before callbacks. Every touched ID is resolved, including `delete` notices. A present current record wins after deletion and recreation. An absent ID is removed only with explicit deletion evidence in that completed window. Unexplained absence, changed detail, duplicate/unexpected IDs, repeated cursors or a nonterminal exact-ID batch stop without checkpointing. A delete notice may represent physical removal or current source visibility loss; it does not authorize deleting anything in Signal.

Each page contains at most 100 records. The reference helper caps discovery/touched IDs at 100,000 and notices at 1,000,000 per run. Exceeding a bound fails without a new checkpoint; larger receivers need bounded durable staging. Checkpoints are opaque business/key/scope-bound sequences. Do not use `pollStartedAt`, your clock, or an old figure-import timestamp checkpoint. Inventory and catch-up provide a recoverable current copy, not a historical point-in-time snapshot. Periodic full reconciliation prunes missing rows only after the whole run succeeds.

## Batches and API allowance

Presence/discovery pages contain at most 100 compact summaries. Hydration uses `/api/v1/appraisals/native/batch?ids=...`, up to ten full envelopes in requested order. Any missing, imported, invalid, unavailable or raced member fails the complete batch. Every member is validated before callbacks. No error triggers a silent fallback to individual GETs. The cumulative data cap is 1 MiB; after 413 deliberately set `SIGNAL_TRADE_NATIVE_BATCH_SIZE` to 1–9 and retry from the same checkpoint. Custom helpers accept the keyword `detail_batch_size` (default 10). A single oversized record still needs review.

The mirror shares `create_native_appraisal_sync_transport(api_key, base_url)` across account, discovery, presence, batch, events and retry attempts. Its monotonic-clock pacing starts GETs at least 600 ms apart, at most 100/minute for one receiver. Custom integrations should share it via the helpers' `transport` keyword, including `get_trade_api_account`. An injectable `clock`/`sleep` or explicit `request_spacing_ms` supports isolated fixtures. CLI spacing below 600 ms is allowed only for loopback fixtures; ordinary production is paced. No bearer token is durably stored or logged.

The network quota remains 120 requests/minute, and the **shared key** remains limited to 600 GETs/hour. Pacing does not increase either. A 1,000-record full inventory at batch size 10 needs at least 113 reads: 10 compact pages, 100 private batches, one catch-up page and two account checks. Extra event pages, changed-ID presence/hydration and retries add requests. Plan allowance for daily reconciliation and other readers. The 100,000-record/1,000,000-event safeguards are **memory ceilings, not throughput guarantees**. A history beyond the available hourly allowance needs a custom durable staging receiver across quota windows; the atomic reference mirror preserves its previous copy on rate failure and does not resume a partly committed full inventory.

The reference mirror does not prove an external system is integrated, a valuation is accurate, or a seller statement establishes legal acceptance. Review destination mapping and local privacy/retention needs before relying on it.
