auditx-connector (Python)
Python implementation of AuditX connector with canonical audit envelope and pluggable publishers.
Supports:
- ASYNC_DB publisher (
PostgresAuditPublisher) - Kafka publisher (
KafkaAuditPublisher) - Java API publisher (
JavaApiAuditPublisher) for remote publish to Spring Boot AuditX endpoint
Install
pip install auditx-connector
Core package does not force DB/Kafka drivers. Consumers install only needed drivers.
PostgreSQL publisher supports both drivers:
psycopg(v3, preferred)psycopg2/psycopg2-binary(fallback)
Optional extras:
pip install "auditx-connector[postgres]"
pip install "auditx-connector[kafka]"
pip install "auditx-connector[all]"
Publisher modes (PublisherType):
ASYNC_DBKAFKAJAVA_API
Consumer Configuration
Consumers set config in code while constructing publisher instances.
Common connector flags
from auditx_connector import AuditConnectorConfig
connector_config = AuditConnectorConfig(
enabled=True,
enforce_idempotency=True,
raise_on_dedup=False, # True => raise when DB insert is skipped by ON CONFLICT
verbose_logging=True, # True => print execution logs per publish
)
ASYNC_DB (PostgresConfig)
from auditx_connector import PostgresConfig
# Option 1: DSN
pg_config = PostgresConfig(
dsn="postgresql://audit_user:audit_pass@localhost:5432/auditdb",
table="AUDITX_EVENT",
)
# Option 2: host/port/database/username/password
pg_config = PostgresConfig(
host="localhost",
port=5432,
database="auditdb",
username="audit_user",
password="audit_pass",
table="AUDITX_EVENT",
)
KAFKA (KafkaConfig)
from auditx_connector import KafkaConfig, KafkaMessageKeyType
kafka_config = KafkaConfig(
bootstrap_servers="localhost:9092",
topic="auditx.events",
message_key_type=KafkaMessageKeyType.IDEMPOTENCY_KEY,
)
JAVA_API (JavaApiConfig)
from auditx_connector import JavaApiConfig
java_api_config = JavaApiConfig(
base_url="http://localhost:8080",
publish_path="/auditx/v1/events/publish",
timeout_seconds=10.0,
verbose_logging=True,
)
Optional env-based loading in consumer app
The connector does not auto-read env vars; consumer app can map env vars to config objects:
import os
from auditx_connector import AuditConnectorConfig, PostgresConfig
connector_config = AuditConnectorConfig(
enabled=os.getenv("AUDITX_ENABLED", "true").lower() == "true",
enforce_idempotency=os.getenv("AUDITX_ENFORCE_IDEMPOTENCY", "true").lower() == "true",
raise_on_dedup=os.getenv("AUDITX_RAISE_ON_DEDUP", "false").lower() == "true",
verbose_logging=os.getenv("AUDITX_VERBOSE_LOGGING", "false").lower() == "true",
)
pg_config = PostgresConfig(
host=os.getenv("AUDITX_DB_HOST"),
port=int(os.getenv("AUDITX_DB_PORT", "5432")),
database=os.getenv("AUDITX_DB_NAME"),
username=os.getenv("AUDITX_DB_USER"),
password=os.getenv("AUDITX_DB_PASSWORD"),
table=os.getenv("AUDITX_DB_TABLE", "AUDITX_EVENT"),
)
Quick Start
1. Publish via AuditWriteRequest
from auditx_connector import (
AuditConnectorConfig,
AuditService,
AuditWriteRequest,
AuditSource,
DefaultIdempotencyKeyFactory,
PostgresAuditPublisher,
PostgresConfig,
)
publisher = PostgresAuditPublisher(
postgres_config=PostgresConfig(
dsn="postgresql://user:pass@localhost:5432/auditdb",
table="AUDITX_EVENT",
),
connector_config=AuditConnectorConfig(
enabled=True,
enforce_idempotency=True,
raise_on_dedup=False, # set True to raise when ON CONFLICT skips insert
verbose_logging=True, # prints runtime publish logs to console
),
idempotency_key_factory=DefaultIdempotencyKeyFactory(),
)
service = AuditService(publisher)
service.publish(
AuditWriteRequest(
event_type="DISCONNECT_REQUEST_RECEIVED",
source=AuditSource.CRON,
conversation_id="550e8400-e29b-41d4-a716-446655440000",
group_id="grp-1001",
interaction_id="int-2001",
extra_map={"zapperCustId": "ZP-10091", "plan": "PREMIUM"},
)
)
You can also connect without DSN:
postgres_config=PostgresConfig(
host="localhost",
port=5432,
database="auditdb",
username="audit_user",
password="audit_pass",
table="AUDITX_EVENT",
)
2. Publish via CanonicalAuditEnvelope
from auditx_connector import CanonicalAuditEnvelope, AuditSource, AuditSeverity
envelope = CanonicalAuditEnvelope(
event_type="DISCONNECT_API_TRIGGERED",
source=AuditSource.API,
severity=AuditSeverity.INFO,
service_name="zapper-disconnect-service",
environment="prod",
conversation_id="550e8400-e29b-41d4-a716-446655440000",
group_id="grp-1001",
interaction_id="int-2001",
trace_id="trace-9f8d2",
business_keys={"zapperCustId": "ZP-10091"},
extra_map={"decision": "AUTO_DISCONNECT_ELIGIBLE"},
)
service.publish_envelope(envelope)
3. Publish from Python to Java AuditX API (JAVA_API)
Java endpoint used: POST /auditx/v1/events/publish
from auditx_connector import (
JavaApiAuditPublisher,
JavaApiConfig,
AuditWriteRequest,
CanonicalAuditEnvelope,
)
java_api = JavaApiAuditPublisher(
JavaApiConfig(
base_url="http://localhost:8080",
verbose_logging=True, # prints runtime publish logs to console
)
)
# mode 1: stage + metadata map
java_api.publish_stage_map(
stage="DISCONNECT_REQUEST_RECEIVED",
conversation_id="550e8400-e29b-41d4-a716-446655440000",
trace_id="trace-2001",
metadata={"zapperCustId": "ZP-10091", "plan": "PREMIUM"},
)
# mode 2: AuditWriteRequest
java_api.publish_write_request(
AuditWriteRequest(
event_type="BILLING_VALIDATION_FAILED",
conversation_id="550e8400-e29b-41d4-a716-446655440000",
)
)
# mode 3: CanonicalAuditEnvelope
java_api.publish_envelope(
CanonicalAuditEnvelope(
event_type="DISCONNECT_API_TRIGGERED",
conversation_id="550e8400-e29b-41d4-a716-446655440000",
)
)
Stage Enum Utility Pattern
Define stage enum with static stage metadata (stage_name, source, severity).
from enum import Enum
from auditx_connector import AuditSeverity, AuditSource, AuditStage
class DisconnectStage(Enum):
DISCONNECT_REQUEST_RECEIVED = (
"DISCONNECT_REQUEST_RECEIVED",
AuditSource.EMAIL_POSTFIX,
AuditSeverity.INFO,
)
BILLING_VALIDATION_FAILED = (
"BILLING_VALIDATION_FAILED",
AuditSource.API,
AuditSeverity.ERROR,
)
def stage_name(self) -> str:
return self.value[0]
def source(self) -> AuditSource:
return self.value[1]
def severity(self) -> AuditSeverity:
return self.value[2]
Publish with stage + trace + metadata:
metadata = {
"zapperCustId": "ZP-10091",
"address": "ABCD, XX",
"plan": "PREMIUM",
}
service.publish_by_stage(
stage=DisconnectStage.DISCONNECT_REQUEST_RECEIVED,
conversation_id="550e8400-e29b-41d4-a716-446655440000",
trace_id="trace-11",
metadata=metadata,
)
Publish with stage + base AuditWriteRequest:
base_request = AuditWriteRequest(
event_type="PLACEHOLDER",
source=AuditSource.CRON,
conversation_id="550e8400-e29b-41d4-a716-446655440000",
group_id="grp-1001",
interaction_id="int-2001",
extra_map={"existing": "value"},
)
service.publish_by_stage(
stage=DisconnectStage.BILLING_VALIDATION_FAILED,
conversation_id="550e8400-e29b-41d4-a716-446655440000",
trace_id="trace-12",
metadata={"errorCode": "ADDRESS_MISMATCH"},
base_request=base_request,
)
Publish with stage + base CanonicalAuditEnvelope:
base_envelope = CanonicalAuditEnvelope(
event_type="PLACEHOLDER",
source=AuditSource.SYSTEM,
conversation_id="550e8400-e29b-41d4-a716-446655440000",
group_id="grp-1001",
interaction_id="int-2001",
service_name="zapper-disconnect-service",
extra_map={"existing": "value"},
)
service.publish_by_stage(
stage=DisconnectStage.DISCONNECT_REQUEST_RECEIVED,
conversation_id="550e8400-e29b-41d4-a716-446655440000",
trace_id="trace-13",
metadata={"sourceSystem": "CCTEAM_EXCEL"},
base_envelope=base_envelope,
)
Utility Class Example
from __future__ import annotations
from enum import Enum
from typing import Any
from auditx_connector import (
AuditService,
AuditSeverity,
AuditSource,
AuditStage,
AuditWriteRequest,
CanonicalAuditEnvelope,
)
class DisconnectStage(Enum):
DISCONNECT_REQUEST_RECEIVED = (
"DISCONNECT_REQUEST_RECEIVED",
AuditSource.EMAIL_POSTFIX,
AuditSeverity.INFO,
)
INVENTORY_ENRICHMENT_STARTED = (
"INVENTORY_ENRICHMENT_STARTED",
AuditSource.CRON,
AuditSeverity.INFO,
)
BILLING_VALIDATION_FAILED = (
"BILLING_VALIDATION_FAILED",
AuditSource.API,
AuditSeverity.ERROR,
)
def stage_name(self) -> str:
return self.value[0]
def source(self) -> AuditSource:
return self.value[1]
def severity(self) -> AuditSeverity:
return self.value[2]
class AuditEventUtil:
@staticmethod
def base_metadata(zapper_cust_id: str, address: str, plan: str) -> dict[str, Any]:
return {
"zapperCustId": zapper_cust_id,
"address": address,
"plan": plan,
"domain": "ELECTRICITY_DISCONNECT",
}
@staticmethod
def build_write_request(
stage: str,
conversation_id: str,
group_id: str,
interaction_id: str,
metadata: dict[str, Any] | None,
) -> AuditWriteRequest:
return AuditWriteRequest(
event_type=stage,
source=AuditSource.CRON,
severity=AuditSeverity.INFO,
conversation_id=conversation_id,
group_id=group_id,
interaction_id=interaction_id,
extra_map=dict(metadata or {}),
)
@staticmethod
def build_canonical_envelope(
stage: str,
conversation_id: str,
group_id: str,
interaction_id: str,
metadata: dict[str, Any] | None,
) -> CanonicalAuditEnvelope:
return CanonicalAuditEnvelope(
event_type=stage,
source=AuditSource.SYSTEM,
severity=AuditSeverity.INFO,
service_name="zapper-disconnect-service",
environment="prod",
conversation_id=conversation_id,
group_id=group_id,
interaction_id=interaction_id,
business_keys={"entityType": "disconnect-request"},
extra_map=dict(metadata or {}),
)
@staticmethod
def publish_by_stage(
service: AuditService,
stage: DisconnectStage,
conversation_id: str,
trace_id: str,
metadata: dict[str, Any] | None,
) -> None:
service.publish_by_stage(stage, conversation_id, trace_id, metadata)
Validation Rules
conversation_idis required and must be UUID.- If
source == AuditSource.UI,session_idis required.
Idempotency
Default key input:
event_type | source | conversation_id | interaction_id | group_id
SHA-256 hash is used as idempotency key when key is missing.
If you are debugging "publish executed but no row inserted":
- turn on Python logging for
auditx_connector.publishers.postgres - set
AuditConnectorConfig(raise_on_dedup=True)to fail fast when insert is skipped by idempotency conflict - provide
interaction_id/group_id(or explicitidempotency_key) so repeated calls are not treated as duplicates unintentionally - set
AuditConnectorConfig(verbose_logging=True)to print execution-level messages (start, rows_affected, dedupe, error)
Local install without publishing
Use this when you want to test on another system without uploading to PyPI.
Build wheel
python -m pip install --upgrade build
python -m build
Copy generated dist/*.whl to your local package folder, for example:
/Users/salilvnair/local-pypi
Install directly from local folder (one-time command)
pip install --no-index --find-links=/Users/salilvnair/local-pypi auditx-connector
Persistent pip config (local folder only, no internet)
~/.pip/pip.conf
[global]
no-index = true
find-links = /Users/salilvnair/local-pypi
Persistent pip config with fallback to indexes
[global]
find-links =
/Users/salilvnair/local-pypi
/Users/salilvnair/team-pypi
index-url = https://your-primary/simple
extra-index-url =
https://repo1/simple
https://repo2/simple
Notes:
- If
no-index = true, pip never queries any online index. - For fallback to indexes, do not set
no-index.
Build and Publish to PyPI
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
python -m twine upload dist/*
Notes
- Package name:
auditx-connector - Import path:
auditx_connector
Publishing the package
1) bump version (example: 1.0.0 -> 1.0.1)
sed -i '' 's/^version = "1.0.0"/version = "1.0.1"/' pyproject.toml
2) clean old artifacts
rm -rf dist build *.egg-info
3) build
python -m pip install --upgrade build twine python -m build
4) validate package
python -m twine check dist/*
5) publish to PyPI
python -m twine upload dist/*
Metadata
Release files for auditx-connector 1.0.2
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 | |
|---|---|---|---|---|
| auditx_connector-1.0.2.tar.gz | 15.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| auditx_connector-1.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.8 kB
Release files / auditx_connector-1.0.2.tar.gz
| Download URL | auditx_connector-1.0.2.tar.gz |
|---|---|
| Size | 15.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
126d2fff53d397f88258d5abdc23ea061acd2d49081022e50e25294b89a1f597
|
|
BLAKE2b-256 checksum How to use checksums |
84e002fbd0a2163752562df208919ef2402abee7e6c7c63dacdd9c2fda89a783
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|
Release files / auditx_connector-1.0.2-py3-none-any.whl
| Download URL | auditx_connector-1.0.2-py3-none-any.whl |
|---|---|
| Size | 15.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
71ead114432098de0793ff995fbdac117c5c5619729b4a8ccb30a9ad5d315cba
|
|
BLAKE2b-256 checksum How to use checksums |
f7258fe2b82216401bbda2b9b60037db4e28cd01565b8ffa39d9cf79259530cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|
Security for auditx-connector 1.0.2
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.