Files
parsedmarc/parsedmarc/webhook.py
T
0a95a0ceb2 Upgrade ruff to 0.16.0 and pyright to 1.1.411; convert to f-strings (#847)
* Upgrade ruff to 0.16.0 and pyright to 1.1.411; convert to f-strings

ruff 0.16.0 expanded the default lint rule selection well beyond the
long-standing E4/E7/E9/F, so [tool.ruff.lint] now selects the rule set
explicitly: the pre-0.16 defaults plus the modern-type-hint UP rules and
the two f-string rules (UP030/UP032). All 352 UP030/UP032 findings were
auto-fixed; conversions requiring Python 3.12 f-string quote reuse were
conservatively left as .format() by ruff (verified: the whole package and
test suite byte-compile under CPython 3.10.20, the oldest CI version).
Adopting the other newly-default rule families (BLE, SIM, C4, DTZ, I, ...)
is deferred as a deliberate per-family decision.

ruff format with 0.16.0 also now formats Python code fences in Markdown,
which reformatted one block in parsedmarc/resources/maps/AGENTS.md.

ruff check, ruff format --check, pyright (0 errors/warnings), and the
full test suite (775 passed) are green on the new versions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address Copilot review findings on the f-string conversion

- Rewrite messages that used backslash line continuations inside string
  literals, which embedded the source indentation as literal whitespace
  in the logged/raised text: the duplicate search-error messages in the
  Elasticsearch and OpenSearch outputs, the 'since'-option warning (which
  also implicitly concatenated "24hrs" and "SMTP" with no separator) and
  the IMAP 'since' debug line, and the missing-org_name KeyError message.
- Build the Splunk HEC newline-delimited payloads by appending to a list
  and joining once instead of quadratic string concatenation in a loop.

Declined: switching the webhook output's logger.error to
logger.exception — the single-line ERROR without a traceback is the
deliberate house pattern for batch-resilient sinks, and changing log
verbosity is out of scope for this refactor PR.

ruff check/format, pyright (0 errors/warnings), 775 tests, and a
CPython 3.10 compileall all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 13:46:36 -04:00

74 lines
2.5 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Any
import httpx
from parsedmarc import logger
from parsedmarc.constants import USER_AGENT
class WebhookClient(object):
"""A client for webhooks"""
def __init__(
self,
aggregate_url: str,
failure_url: str,
smtp_tls_url: str,
timeout: int | None = 60,
):
"""
Initializes the WebhookClient
Args:
aggregate_url (str): The aggregate report webhook url
failure_url (str): The failure report webhook url
smtp_tls_url (str): The smtp_tls report webhook url
timeout (int): The timeout to use when calling the webhooks
"""
self.aggregate_url = aggregate_url
self.failure_url = failure_url
self.smtp_tls_url = smtp_tls_url
self.timeout = timeout
self.session = httpx.Client(
headers={
"User-Agent": USER_AGENT,
"Content-Type": "application/json",
},
follow_redirects=True,
)
def save_failure_report_to_webhook(self, report: str):
self._send_to_webhook(self.failure_url, report)
def save_smtp_tls_report_to_webhook(self, report: str):
self._send_to_webhook(self.smtp_tls_url, report)
def save_aggregate_report_to_webhook(self, report: str):
self._send_to_webhook(self.aggregate_url, report)
def _send_to_webhook(self, webhook_url: str, payload: bytes | str | dict[str, Any]):
# All HTTP / network errors are swallowed and logged: a failing
# webhook should never abort the surrounding parse-and-output
# batch. The outer save_* methods previously wrapped this in a
# redundant try/except — removed because _send_to_webhook
# already catches every Exception itself.
try:
if isinstance(payload, dict):
# requests form-encoded dict payloads via data=; httpx does
# the same only via data=
self.session.post(webhook_url, data=payload, timeout=self.timeout)
else:
self.session.post(webhook_url, content=payload, timeout=self.timeout)
except Exception as error_:
logger.error(f"Webhook Error: {error_.__str__()}")
def close(self):
"""Close the underlying HTTP session."""
self.session.close()
# Backward-compatible alias
save_forensic_report_to_webhook = save_failure_report_to_webhook