annexops-sdk (Python)
The customer-side clients for AnnexOps — AILogger (EU AI Act Article 12
runtime log, including agent tool calls and approvals), ConsentLogger (GDPR Article 7 consent receipts), and
SchemaScanner (private-database schema discovery for data mapping). The
stdlib-only Python mirror of the TypeScript @annexops/sdk (npm), with the same
hashes-only / schema-only wire contracts.
Engineering documentation, not legal advice. AnnexOps does not certify conformity. Keeping these records supports (but does not by itself discharge) your EU AI Act Article 12 record-keeping and GDPR Article 7 consent-recording obligations — whether they satisfy Article 12/7 for your system is a determination for you and your counsel.
Zero third-party dependencies: hashlib, uuid, urllib, json, threading
and friends only.
Install
pip install annexops-sdk
Requires Python ≥ 3.9. Zero third-party runtime dependencies.
Quickstart — AILogger (runtime inference log, Article 12)
import os
from annexops_sdk import AILogger
logger = AILogger(api_key=os.environ["ANNEXOPS_API_KEY"], system_key="my-model-v1")
logger.log_inference(input=prompt, output=response)
logger.close() # on shutdown — flushes the remainder
Agent tool calls and approvals
If your AI system is an agent that calls tools, record each tool call and each human approval on the same logger. The tool arguments and result are hashed at source and discarded; only the tool name, the outcome, the approval reference and SHA-256 hashes are sent.
# a human approved the call (approval_ref is your own id: never a name or e-mail)
logger.log_approval(tool_name="send_email", approval_ref="appr-1042",
decision="approved", arguments=args_json)
# the call that relied on that approval
logger.log_tool_call(
tool_name="send_email",
arguments=args_json, # hashed at source, never sent
result=result_json, # hashed at source, never sent
outcome="succeeded", # "succeeded" | "failed" | "blocked" (the call did not run)
approval_ref="appr-1042",
)
tool_name is canonicalised before it is sent (trimmed, lower-case, - and
spaces become _). tool_name and approval_ref are identifiers (letters,
digits, _ . : / -, at most 128 characters), never free text. Pass
arguments_hash / result_hash instead of the raw values if you hash
yourself. Tool events go into the same system_key hash chain as your
inference events; in the portal, the AI system page shows them in the
Agent tool calls card.
Quickstart — ConsentLogger (GDPR consent receipts, Article 7)
import os
from annexops_sdk import ConsentLogger
consent_logger = ConsentLogger(
api_key=os.environ["ANNEXOPS_API_KEY"], # the same key as AILogger
purpose_key="marketing-emails",
)
consent_logger.log_consent(
subject_id=user_email,
action="grant",
consent_text=cookie_banner_notice_text,
consent_version="v3",
)
consent_logger.log_consent(subject_id=user_email, action="withdraw") # no consent_text needed
consent_logger.close() # on shutdown — flushes the remainder
subject_id is hashed at source with SHA-256, then run through a second
server-side HMAC step with a secret pepper AnnexOps holds — a low-entropy
identifier (an email, a user id) can't be reversed from the stored value the
way a bare hash could. consent_text (high-entropy notice copy) is a bare
SHA-256, the same treatment AILogger gives your prompts/responses; your
code never needs to know the difference. Pass subject_hash/
consent_text_hash instead of the raw values if you'd rather hash yourself
(same mutual-exclusivity rule as AILogger's input/input_hash).
Quickstart — SchemaScanner (data-map schema push)
SchemaScanner reads the column metadata of one of your databases — schema,
table, and column names plus their declared types — and pushes it to
AnnexOps, where it is classified into your data map. Only names and types are
read; no row is ever queried, and no data value ever leaves your process. The
query is a fixed information_schema read the SDK owns — you never pass SQL.
You bring your own driver (psycopg or pymysql); the SDK bundles none. Give
it a runner that executes exactly the SQL it hands you and returns the rows
(dicts keyed table_schema/table_name/column_name/data_type):
import os
import psycopg
from psycopg.rows import dict_row
from annexops_sdk import SchemaScanner
conn = psycopg.connect(os.environ["DATABASE_URL"])
def runner(sql):
with conn.cursor(row_factory=dict_row) as cur:
cur.execute(sql) # runs exactly the SQL passed — the SDK never interpolates
return cur.fetchall()
scanner = SchemaScanner(
api_key=os.environ["ANNEXOPS_API_KEY"], # the same key as AILogger
store_key="prod-postgres", # a stable name you choose per database
source="postgres", # "postgres" | "mysql"
store_name="Production Postgres", # optional label
)
result = scanner.scan_and_push(runner)
conn.close()
print(f"Pushed {result['elements_ingested']} columns; {result['classifications']} classified.")
For MySQL, pass source="mysql" and a pymysql runner (a DictCursor).
scan(runner) and push() are also available separately. A very large schema
is truncated at 50 000 columns and reported as completeness: "partial".
This is a single-shot push — no batching, no interval timer, no close();
just the shared retry loop (network / 5xx / 429 retried with full-jitter
backoff, 401/400 never retried). Grant the scanning credential a read-only
role that can read information_schema and nothing more.
What arrives in AnnexOps
AILogger POSTs to POST /v1/runtime-logs; ConsentLogger POSTs to
POST /v1/consent-receipts; SchemaScanner POSTs to
POST /v1/data-stores/schema-push. Each is the only endpoint its client calls.
Runtime-log events land under Runtime Logs, appended to the per-(org, system_key) hash chain; consent receipts land under Consent, appended to
the per-(org, purpose_key) hash chain; pushed schema lands under Data
Stores, classified into your data map (feeding RoPA and DSAR). Viewing
events/receipts/stores, verifying chain integrity, and exporting records all
happen in the AnnexOps portal UI, signed in — they are portal features, not SDK
or API-key endpoints.
Key handling
The full ak_live_<prefix8>_<secret32> key is shown exactly once at mint
(Settings → API keys); both loggers require the full 49-char key — the same
key works for both, nothing extra to mint. NEVER commit the key to source —
it matches common secret-scanner patterns. Rotate by minting a
new key and revoking the old. Neither logger ever logs the key or places it
in an exception message.
Hashes only, never content
log_inference(input=..., output=...) computes input_hash/output_hash at
source via sha256_hex(...) (exported, for callers who want to hash
themselves — "you hash what you pass": strings are UTF-8-encoded, bytes pass
through). log_consent(subject_id=..., consent_text=...) computes
subject_hash/consent_text_hash the same way. Raw content never leaves
your process — the wire types have no field that could carry it, and the
server-side schema is strict.
Retries + idempotency
Same batch, same event_ids on every retry: event_id is
generated once at log time and the retry loop re-POSTs the byte-identical
body. Full jitter, 3 retries, 30 s cap, 10 s request timeout; retries
network errors / 5xx / 429, never other 4xx. Configure via max_retries,
backoff_base_ms, backoff_cap_ms, request_timeout_ms (identical
constructor options on both loggers).
Flush + close
Each logger buffers up to flush_at: 100 events (hard-clamped ≤ 500, the
wire batch max) and flushes on interval (flush_interval_ms: 5000, a
daemon-thread timer that never keeps the interpreter alive) or
on close(). Bodies are split to ≤ 500 events and ≤ 1 MiB each. Always call
.close() on shutdown to avoid losing events; per-event server-side
rejections surface in the returned {"accepted": ..., "rejected": [...]} and
via the optional on_rejected callback. close() is bounded: its TOTAL wait
is capped by close_timeout_s (default: the retry policy's worst case plus
slack — 135 s with default options). Within that one budget it first joins
any in-flight flushes, then runs its own final flush on a bounded path (a
daemon worker joined with the remaining deadline) — so a wedged network stack
can never hang your process shutdown, even when the wedge happens inside the
final flush itself; a timeout surfaces via on_error with a static message.
close() does not lock the instance (mirror of the TS SDK): log_inference()
/log_consent() after close() still buffers and still auto-flushes at
flush_at; a subsequent explicit flush() sends the rest.
License
MIT © AnnexOps
Metadata
Release files for annexops-sdk 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| annexops_sdk-0.4.0.tar.gz | 45.1 kB | Without provenance | Details for annexops_sdk-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| annexops_sdk-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for annexops_sdk-0.4.0-py3-none-any.whl |
Total release size: 81.0 kB
Release files / annexops_sdk-0.4.0.tar.gz
| Download URL | annexops_sdk-0.4.0.tar.gz |
|---|---|
| Size | 45.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cf536e06b85cbd6c57b7142a3f2007b44f8f86ef8bfcf70f9103dc64b4bcc4af
|
|
BLAKE2b-256 checksum How to use checksums |
6b99e6b3714d5aad8a20b68fee95e27d4e078528bbf7b2613db3af9a5f2bcc5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Release files / annexops_sdk-0.4.0-py3-none-any.whl
| Download URL | annexops_sdk-0.4.0-py3-none-any.whl |
|---|---|
| Size | 36.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7992c231b5ad6aad2902d82b852ee0c4678458eaad3b97171259acecc7ef11f1
|
|
BLAKE2b-256 checksum How to use checksums |
032b6777d651986c47fae9165f093e145a24b0dbfdaa4b30af0ca4e380368da3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Security for annexops-sdk 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.