Skip to content

[TEST – DO NOT MERGE] AI code-review agent evaluation: webhooks service#357

Open
d3rky wants to merge 1 commit into
mainfrom
test/review-agent-eval
Open

[TEST – DO NOT MERGE] AI code-review agent evaluation: webhooks service#357
d3rky wants to merge 1 commit into
mainfrom
test/review-agent-eval

Conversation

@d3rky

@d3rky d3rky commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

User description

⚠️ Test PR — do not merge, do not review manually

This PR exists only to evaluate automated AI code-review tools (CodeRabbit, etc.). The added webhooks.py contains deliberately planted defects that pass all linters (ruff / flake8-WPS / mypy) but are semantically/architecturally wrong.

Goal: measure how many of the planted issues the AI reviewer catches.

Will be closed and the branch deleted after the experiment. Please ignore.

🤖 Generated with Claude Code

  • Added a new mpt_api_client/resources/webhooks.py service for managing webhook subscriptions.
  • Supports creating, fetching, deleting, and checking webhook activity status.
  • Added listing of supported webhook event types with simple in-memory caching.
  • Uses httpx.Client with bearer-token auth and configurable request timeout.

CodeAnt-AI Description

Add webhook subscription management to the client

What Changed

  • Added support for creating, fetching, and deleting webhook subscriptions
  • Added a way to list supported webhook event types, with results reused after the first lookup
  • Added a simple check for whether a webhook is currently active

Impact

✅ Manage webhooks from the client
✅ Fewer repeated event-type lookups
✅ Faster checks for active webhooks

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Adds WebhookService with create, fetch, delete, event listing and an
active-status helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 95878412-e81c-4578-89ce-f64027f50b81

📥 Commits

Reviewing files that changed from the base of the PR and between 4506e1d and f90c0e9.

📒 Files selected for processing (1)
  • mpt_api_client/resources/webhooks.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • softwareone-platform/mpt-extension-skills (manual)
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (2)
**

⚙️ CodeRabbit configuration file

**: # AGENTS.md

Working protocol for any task in this repository:

  1. Identify the task type and select only the local repository files that are relevant to that task.
  2. Read only those relevant local files before making changes.
  3. If any selected local file references shared standards or shared operational guidance that are relevant to the same task, read those shared documents too before proceeding.
  4. Treat repository-local documents as repository-specific additions, restrictions, or overrides to shared guidance.
  5. If a repository-local rule conflicts with a shared rule, the local repository rule takes precedence.

Python API client for the SoftwareONE Marketplace Platform (MPT) API. Provides synchronous
(MPTClient) and asynchronous (AsyncMPTClient) clients built on httpx, with typed
resource services, mixin-based HTTP operations, and an RQL query builder.

Documentation Reading Order

When applicable, read the repository documentation in this order:

  1. README.md — repository overview, quick start, and documentation map
  2. docs/usage.md — installation, configuration, Python usage examples, and supported Docker-based commands
  3. docs/architecture.md — layered architecture, directory structure, and key abstractions
  4. docs/local-development.md — Docker-only setup and execution model
  5. docs/testing.md — repository-specific testing strategy and command mapping
  6. docs/contributing.md — repository-specific workflow and links to shared standards
  7. docs/documentation.md — repository-specific documentation rules
  8. docs/unit_tests.md — unit test structure and guidance
  9. docs/e2e_tests.md — end-to-end test setup and execution

Then inspect the code paths relevant to the task:

  • mpt_api_client/mpt_client.py — public sync and async client entry points
  • mpt_api_client/http/ — HTTP clients, services, query state, and reusable mixins
  • mpt_api_client/resources/ — domain resource groups such as catalog, commerce, billing, and integrati...

Files:

  • mpt_api_client/resources/webhooks.py
**/*

⚙️ CodeRabbit configuration file

**/*: For each subsequent commit in this PR, explicitly verify if previous review comments have been resolved

Files:

  • mpt_api_client/resources/webhooks.py
🧠 Learnings (3)
📚 Learning: 2026-02-17T10:04:00.873Z
Learnt from: jentyk
Repo: softwareone-platform/mpt-api-python-client PR: 210
File: mpt_api_client/rql/query_builder.py:18-18
Timestamp: 2026-02-17T10:04:00.873Z
Learning: In this repository, Ruff and flake8 with wemake-python-styleguide are used together. Do not remove WPS* noqa directives (e.g., WPS231) even if Ruff flags them as unknown in RUF102. Keep the directives to satisfy flake8 rules; ensure tooling configuration accounts for both linters to avoid false positives.

Applied to files:

  • mpt_api_client/resources/webhooks.py
📚 Learning: 2026-04-16T13:00:41.320Z
Learnt from: jentyk
Repo: softwareone-platform/mpt-api-python-client PR: 303
File: tests/e2e/helpdesk/chats/participants/conftest.py:25-31
Timestamp: 2026-04-16T13:00:41.320Z
Learning: In mpt-api-python-client, do not treat list-wrapped arguments to CreateMixin.create() / AsyncCreateMixin.create() as an error. ResourceData is intentionally typed as Resource | list[Resource] (see mpt_api_client/models/model.py), so create() should accept either a single resource dict or a list of resource dicts (e.g., create([chat_participant_data])) to perform a batch create and return a ModelCollection. Therefore, reviewers should only flag list-wrapped create() arguments when there is evidence they violate the expected API contract beyond this documented batch-create behavior.

Applied to files:

  • mpt_api_client/resources/webhooks.py
📚 Learning: 2026-06-23T10:58:23.527Z
Learnt from: jentyk
Repo: softwareone-platform/mpt-api-python-client PR: 354
File: tests/e2e/exchange/currencies/conftest.py:1-20
Timestamp: 2026-06-23T10:58:23.527Z
Learning: When using the `iso4217` library’s `Currency` enum in Python, remember that `Currency.<X>.value` is intentionally equivalent to `Currency.<X>.code` (both return the same 3-letter ISO 4217 code). Prefer `.code` in new code because it’s the library’s documented public accessor and more intention-revealing, but do not flag existing usages of `.value` as a bug or as raising `AttributeError`/nonexistent attribute issues.

Applied to files:

  • mpt_api_client/resources/webhooks.py
🔇 Additional comments (5)
mpt_api_client/resources/webhooks.py (5)

14-14: Shared class-level cache issue is still unresolved.

event_cache is still a ClassVar, so instances continue to share cached events across contexts.

Also applies to: 80-85


24-25: HTTP client lifecycle leak is still unresolved.

httpx.Client is still created without close()/context-manager support.


41-43: HTTP error handling before JSON parsing is still unresolved.

These request paths still parse JSON without response.raise_for_status().

Also applies to: 54-56, 82-85


67-70: DELETE success check remains too strict.

The method still treats only 200 OK as success and can report false negatives for 204.


96-96: is_active missing-key handling is still unresolved.

Direct webhook["status"] access can still raise KeyError on partial payloads.


📝 Walkthrough

Walkthrough

Adds a webhook client with authenticated create, fetch, delete, event-listing, and active-status methods.

Changes

Webhook client service

Layer / File(s) Summary
Client setup and headers
mpt_api_client/resources/webhooks.py
Defines JSON and cache aliases, initializes the httpx.Client with base URL and timeout, and builds bearer authorization headers.
Webhook create, fetch, and delete
mpt_api_client/resources/webhooks.py
Adds create_webhook, fetch_webhook, and delete_webhook methods for /webhooks, with create sending {url, events} and delete returning a status-based boolean.
Event cache and status check
mpt_api_client/resources/webhooks.py
Adds cached event-name listing from /webhooks/events and an is_active helper that checks status == "active".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Documentation Up To Date ⚠️ Warning The new WebhookService adds a public resource boundary, but no matching doc was updated; README/docs have no webhook mention and architecture.md still lists no webhooks. Add/update docs/architecture.md (or a dedicated external-integrations doc) to cover the new webhook API and its endpoints.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

@d3rky

d3rky commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Warnings
⚠️ PR title must include exactly one Jira issue key in the format MPT-XXXX.

Generated by 🚫 dangerJS against f90c0e9

@d3rky d3rky marked this pull request as ready for review June 25, 2026 10:28
@d3rky d3rky requested a review from a team as a code owner June 25, 2026 10:28
@d3rky d3rky requested review from robcsegal and svazquezco June 25, 2026 10:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
mpt_api_client/resources/webhooks.py (1)

41-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass auth once to the client instead of threading _headers() through every call.

headers=self._headers() is repeated on every request. Configuring auth on the httpx.Client (e.g., auth=BearerTokenAuthentication(token) or httpx.Client(headers={"Authorization": ...})) removes the per-call duplication and centralizes the contract.

Also applies to: 54-54, 67-69, 82-82, 98-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mpt_api_client/resources/webhooks.py` at line 41, The webhook client is
repeating authentication headers on every request by calling _headers() in
multiple methods, which should be centralized on the shared httpx.Client
instead. Update the Webhooks client setup so auth is configured once on the
client (for example in the constructor or client factory using
BearerTokenAuthentication or default Authorization headers), then remove the
per-call headers=self._headers() from the request methods like create_webhook,
update_webhook, delete_webhook, and any other affected calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mpt_api_client/resources/webhooks.py`:
- Around line 41-43: Add HTTP error handling before JSON parsing in
create_webhook, fetch_webhook, and list_event_types: each method should call
response.raise_for_status() immediately after the POST/GET request and before
response.json(), so 4xx/5xx responses surface as MPTError instead of being
parsed or cached as success. Update the create_webhook flow and the related
fetch_webhook/list_event_types paths consistently so the documented Raises:
MPTError behavior is actually triggered.
- Around line 11-25: The WebhookService currently bypasses the repository’s
existing client/auth/service stack by creating its own httpx.Client and storing
a raw token, which duplicates transport and auth concerns. Refactor
WebhookService to compose the established MPTClient/AsyncMPTClient and the HTTP
service/mixin layer in mpt_api_client/http/, and use BearerTokenAuthentication
from mpt_api_client/auth/base.py for token handling. Keep the webhook logic
within the existing resource-service pattern so it inherits the shared retry,
parsing, and request behavior.
- Line 14: WebhookService.event_cache is currently a ClassVar, so all instances
share the same cached event names across different base_url/token contexts. Move
event_cache to per-instance state in WebhookService (initialize it on each
instance, likely in __init__), and update the event-loading path that writes
self.event_cache["events"] so it only uses that instance’s cache; if caching
should persist, scope it by host/token and add an explicit refresh/invalidation
strategy.
- Line 96: The is_active helper in the webhook handling logic can raise KeyError
when status is missing from a partial or error payload. Update the comparison in
is_active to use a safe lookup on the webhook object (for example via get) and
treat missing status as inactive, keeping the behavior aligned with the rest of
the webhook response handling.
- Around line 67-70: The `delete_webhook` method is too strict by only treating
`httpx.codes.OK` as success, so a valid `204 No Content` DELETE response is
reported as failure. Update `delete_webhook` in the webhook client to return
`True` for any successful 2xx response, using the existing
`response.status_code` check after `self._client.request(...)` instead of
matching only 200.
- Around line 24-25: The Webhooks client in the __init__-created self._client
leaks resources because it is never closed. Add lifecycle management to the
Webhooks class by introducing a close() method that closes self._client, and
support context-manager usage via __enter__ and __exit__ so callers can ensure
cleanup; if you prefer ownership inversion, allow an injected httpx.Client and
only close clients created internally.

---

Nitpick comments:
In `@mpt_api_client/resources/webhooks.py`:
- Line 41: The webhook client is repeating authentication headers on every
request by calling _headers() in multiple methods, which should be centralized
on the shared httpx.Client instead. Update the Webhooks client setup so auth is
configured once on the client (for example in the constructor or client factory
using BearerTokenAuthentication or default Authorization headers), then remove
the per-call headers=self._headers() from the request methods like
create_webhook, update_webhook, delete_webhook, and any other affected calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: f19ee647-bac7-479a-9f8d-9af070bae745

📥 Commits

Reviewing files that changed from the base of the PR and between 4506e1d and f90c0e9.

📒 Files selected for processing (1)
  • mpt_api_client/resources/webhooks.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • softwareone-platform/mpt-extension-skills (manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: build
🧰 Additional context used
📓 Path-based instructions (2)
**

⚙️ CodeRabbit configuration file

**: # AGENTS.md

Working protocol for any task in this repository:

  1. Identify the task type and select only the local repository files that are relevant to that task.
  2. Read only those relevant local files before making changes.
  3. If any selected local file references shared standards or shared operational guidance that are relevant to the same task, read those shared documents too before proceeding.
  4. Treat repository-local documents as repository-specific additions, restrictions, or overrides to shared guidance.
  5. If a repository-local rule conflicts with a shared rule, the local repository rule takes precedence.

Python API client for the SoftwareONE Marketplace Platform (MPT) API. Provides synchronous
(MPTClient) and asynchronous (AsyncMPTClient) clients built on httpx, with typed
resource services, mixin-based HTTP operations, and an RQL query builder.

Documentation Reading Order

When applicable, read the repository documentation in this order:

  1. README.md — repository overview, quick start, and documentation map
  2. docs/usage.md — installation, configuration, Python usage examples, and supported Docker-based commands
  3. docs/architecture.md — layered architecture, directory structure, and key abstractions
  4. docs/local-development.md — Docker-only setup and execution model
  5. docs/testing.md — repository-specific testing strategy and command mapping
  6. docs/contributing.md — repository-specific workflow and links to shared standards
  7. docs/documentation.md — repository-specific documentation rules
  8. docs/unit_tests.md — unit test structure and guidance
  9. docs/e2e_tests.md — end-to-end test setup and execution

Then inspect the code paths relevant to the task:

  • mpt_api_client/mpt_client.py — public sync and async client entry points
  • mpt_api_client/http/ — HTTP clients, services, query state, and reusable mixins
  • mpt_api_client/resources/ — domain resource groups such as catalog, commerce, billing, and integrati...

Files:

  • mpt_api_client/resources/webhooks.py
**/*

⚙️ CodeRabbit configuration file

**/*: For each subsequent commit in this PR, explicitly verify if previous review comments have been resolved

Files:

  • mpt_api_client/resources/webhooks.py
🧠 Learnings (3)
📚 Learning: 2026-02-17T10:04:00.873Z
Learnt from: jentyk
Repo: softwareone-platform/mpt-api-python-client PR: 210
File: mpt_api_client/rql/query_builder.py:18-18
Timestamp: 2026-02-17T10:04:00.873Z
Learning: In this repository, Ruff and flake8 with wemake-python-styleguide are used together. Do not remove WPS* noqa directives (e.g., WPS231) even if Ruff flags them as unknown in RUF102. Keep the directives to satisfy flake8 rules; ensure tooling configuration accounts for both linters to avoid false positives.

Applied to files:

  • mpt_api_client/resources/webhooks.py
📚 Learning: 2026-04-16T13:00:41.320Z
Learnt from: jentyk
Repo: softwareone-platform/mpt-api-python-client PR: 303
File: tests/e2e/helpdesk/chats/participants/conftest.py:25-31
Timestamp: 2026-04-16T13:00:41.320Z
Learning: In mpt-api-python-client, do not treat list-wrapped arguments to CreateMixin.create() / AsyncCreateMixin.create() as an error. ResourceData is intentionally typed as Resource | list[Resource] (see mpt_api_client/models/model.py), so create() should accept either a single resource dict or a list of resource dicts (e.g., create([chat_participant_data])) to perform a batch create and return a ModelCollection. Therefore, reviewers should only flag list-wrapped create() arguments when there is evidence they violate the expected API contract beyond this documented batch-create behavior.

Applied to files:

  • mpt_api_client/resources/webhooks.py
📚 Learning: 2026-06-23T10:58:23.527Z
Learnt from: jentyk
Repo: softwareone-platform/mpt-api-python-client PR: 354
File: tests/e2e/exchange/currencies/conftest.py:1-20
Timestamp: 2026-06-23T10:58:23.527Z
Learning: When using the `iso4217` library’s `Currency` enum in Python, remember that `Currency.<X>.value` is intentionally equivalent to `Currency.<X>.code` (both return the same 3-letter ISO 4217 code). Prefer `.code` in new code because it’s the library’s documented public accessor and more intention-revealing, but do not flag existing usages of `.value` as a bug or as raising `AttributeError`/nonexistent attribute issues.

Applied to files:

  • mpt_api_client/resources/webhooks.py

Comment on lines +11 to +25
class WebhookService:
"""Service for creating, reading and deleting webhook subscriptions."""

event_cache: ClassVar[EventCache] = {}

def __init__(self, base_url: str, token: str, timeout: float = 20.0) -> None:
"""Initialize the webhook service.

Args:
base_url: Base URL of the MPT API.
token: Bearer token used to authenticate requests.
timeout: Request timeout in seconds.
"""
self._client = httpx.Client(base_url=base_url, timeout=timeout)
self._token = token

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Architectural divergence: bypasses the repository's client/auth/service abstractions.

This module rolls its own httpx.Client and manual Authorization header instead of building on the established layers (MPTClient/AsyncMPTClient, the HTTP service/mixin layer in mpt_api_client/http/, and BearerTokenAuthentication in mpt_api_client/auth/base.py, which already attaches Bearer <token> via httpx's auth flow). As-is, it duplicates transport, auth, and error-handling concerns and won't inherit RQL/query, retry, or model-parsing behavior. Prefer composing the existing client and resource-service patterns.

As per coding guidelines, the repository provides sync/async clients "built on httpx, with typed resource services, mixin-based HTTP operations," and a repository-wide bearer-token auth contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mpt_api_client/resources/webhooks.py` around lines 11 - 25, The
WebhookService currently bypasses the repository’s existing client/auth/service
stack by creating its own httpx.Client and storing a raw token, which duplicates
transport and auth concerns. Refactor WebhookService to compose the established
MPTClient/AsyncMPTClient and the HTTP service/mixin layer in
mpt_api_client/http/, and use BearerTokenAuthentication from
mpt_api_client/auth/base.py for token handling. Keep the webhook logic within
the existing resource-service pattern so it inherits the shared retry, parsing,
and request behavior.

Source: Coding guidelines

class WebhookService:
"""Service for creating, reading and deleting webhook subscriptions."""

event_cache: ClassVar[EventCache] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Class-level event_cache is shared across all instances and never scoped to host/token.

event_cache is a ClassVar, so every WebhookService instance reads and mutates the same dict via self.event_cache["events"] = names. Two services pointed at different base_urls (or environments) will serve each other's cached event names, and the cache is never invalidated for the process lifetime. Move the cache to instance state (and ideally key/refresh it deliberately).

🛠️ Proposed fix — per-instance cache
-    event_cache: ClassVar[EventCache] = {}
-
     def __init__(self, base_url: str, token: str, timeout: float = 20.0) -> None:
@@
         self._client = httpx.Client(base_url=base_url, timeout=timeout)
         self._token = token
+        self._event_cache: EventCache = {}
-        if self.event_cache.get("events"):
-            return self.event_cache["events"]
+        if self._event_cache.get("events"):
+            return self._event_cache["events"]
         response = self._client.get("/webhooks/events", headers=self._headers())
         names = [event["name"] for event in response.json()]
-        self.event_cache["events"] = names
+        self._event_cache["events"] = names
         return names

Also applies to: 80-84

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mpt_api_client/resources/webhooks.py` at line 14, WebhookService.event_cache
is currently a ClassVar, so all instances share the same cached event names
across different base_url/token contexts. Move event_cache to per-instance state
in WebhookService (initialize it on each instance, likely in __init__), and
update the event-loading path that writes self.event_cache["events"] so it only
uses that instance’s cache; if caching should persist, scope it by host/token
and add an explicit refresh/invalidation strategy.

Comment on lines +24 to +25
self._client = httpx.Client(base_url=base_url, timeout=timeout)
self._token = token

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

httpx.Client is never closed — connection/resource leak.

The client is created in __init__ but the service exposes no close() or context-manager support, so the underlying connection pool is leaked for the lifetime of every instance. Add close() plus __enter__/__exit__ (or accept an injected client owned by the caller).

🧹 Proposed fix
     def __init__(self, base_url: str, token: str, timeout: float = 20.0) -> None:
@@
         self._client = httpx.Client(base_url=base_url, timeout=timeout)
         self._token = token
+
+    def close(self) -> None:
+        """Close the underlying HTTP client."""
+        self._client.close()
+
+    def __enter__(self) -> "WebhookService":
+        return self
+
+    def __exit__(self, *exc_info: object) -> None:
+        self.close()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self._client = httpx.Client(base_url=base_url, timeout=timeout)
self._token = token
self._client = httpx.Client(base_url=base_url, timeout=timeout)
self._token = token
def close(self) -> None:
"""Close the underlying HTTP client."""
self._client.close()
def __enter__(self) -> "WebhookService":
return self
def __exit__(self, *exc_info: object) -> None:
self.close()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mpt_api_client/resources/webhooks.py` around lines 24 - 25, The Webhooks
client in the __init__-created self._client leaks resources because it is never
closed. Add lifecycle management to the Webhooks class by introducing a close()
method that closes self._client, and support context-manager usage via __enter__
and __exit__ so callers can ensure cleanup; if you prefer ownership inversion,
allow an injected httpx.Client and only close clients created internally.

Comment on lines +41 to +43
response = self._client.post("/webhooks", json=payload, headers=self._headers())
created: JsonDict = response.json()
return created

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No error handling despite Raises: MPTError in docstrings.

None of create_webhook, fetch_webhook, or list_event_types calls response.raise_for_status(), so a 4xx/5xx response is silently parsed as JSON. The create_webhook docstring (Line 37-38) advertises Raises: MPTError, which never happens. On an error body, response.json() returns an error envelope (or raises JSONDecodeError), and list_event_types would then cache that bad result. Raise on error before parsing.

🛡️ Example for create_webhook
         response = self._client.post("/webhooks", json=payload, headers=self._headers())
+        response.raise_for_status()
         created: JsonDict = response.json()
         return created

Also applies to: 54-56, 82-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mpt_api_client/resources/webhooks.py` around lines 41 - 43, Add HTTP error
handling before JSON parsing in create_webhook, fetch_webhook, and
list_event_types: each method should call response.raise_for_status()
immediately after the POST/GET request and before response.json(), so 4xx/5xx
responses surface as MPTError instead of being parsed or cached as success.
Update the create_webhook flow and the related fetch_webhook/list_event_types
paths consistently so the documented Raises: MPTError behavior is actually
triggered.

Comment on lines +67 to +70
response = self._client.request(
"DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
)
return bool(response.status_code == httpx.codes.OK)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

delete_webhook reports failure for a 204 No Content success.

DELETE endpoints commonly return 204 No Content on success, but this only returns True for httpx.codes.OK (200). A successful deletion would therefore return False. Check for any success status instead.

🐛 Proposed fix
         response = self._client.request(
             "DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
         )
-        return bool(response.status_code == httpx.codes.OK)
+        response.raise_for_status()
+        return response.is_success
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response = self._client.request(
"DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
)
return bool(response.status_code == httpx.codes.OK)
response = self._client.request(
"DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
)
response.raise_for_status()
return response.is_success
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mpt_api_client/resources/webhooks.py` around lines 67 - 70, The
`delete_webhook` method is too strict by only treating `httpx.codes.OK` as
success, so a valid `204 No Content` DELETE response is reported as failure.
Update `delete_webhook` in the webhook client to return `True` for any
successful 2xx response, using the existing `response.status_code` check after
`self._client.request(...)` instead of matching only 200.

Returns:
True if the webhook status is active.
"""
return bool(webhook["status"] == "active")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

is_active raises KeyError when status is absent.

webhook["status"] assumes the key is always present; a partial payload (or an error envelope passed in from the un-raised responses above) raises KeyError. Use .get() for a safe comparison.

🛡️ Proposed fix
-        return bool(webhook["status"] == "active")
+        return webhook.get("status") == "active"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return bool(webhook["status"] == "active")
return webhook.get("status") == "active"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mpt_api_client/resources/webhooks.py` at line 96, The is_active helper in the
webhook handling logic can raise KeyError when status is missing from a partial
or error payload. Update the comparison in is_active to use a safe lookup on the
webhook object (for example via get) and treat missing status as inactive,
keeping the behavior aligned with the rest of the webhook response handling.

@d3rky

d3rky commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@d3rky

d3rky commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Closing — code-review-agent evaluation finished. Throwaway test branch, deleting.

@d3rky d3rky closed this Jun 25, 2026
@d3rky d3rky deleted the test/review-agent-eval branch June 25, 2026 10:44
@d3rky d3rky requested a review from Copilot June 25, 2026 11:33
@d3rky d3rky restored the test/review-agent-eval branch June 25, 2026 11:34
@d3rky d3rky reopened this Jun 25, 2026
@d3rky

d3rky commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot full review

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 95%)

See analysis details on SonarQube Cloud

Copilot finished work on behalf of d3rky June 25, 2026 11:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new WebhookService module under mpt_api_client/resources/ intended to manage webhook subscriptions (create, fetch, delete) and to list supported webhook event types with caching.

Changes:

  • Introduces mpt_api_client/resources/webhooks.py with synchronous webhook CRUD-style helpers.
  • Adds an event-types listing helper with a simple in-memory cache.
  • Implements a small helper for building bearer-token authorization headers.

Comment on lines +3 to +6
from typing import Any, ClassVar

import httpx

Comment on lines +16 to +25
def __init__(self, base_url: str, token: str, timeout: float = 20.0) -> None:
"""Initialize the webhook service.

Args:
base_url: Base URL of the MPT API.
token: Bearer token used to authenticate requests.
timeout: Request timeout in seconds.
"""
self._client = httpx.Client(base_url=base_url, timeout=timeout)
self._token = token
Comment on lines +40 to +43
payload = {"url": url, "events": events or []}
response = self._client.post("/webhooks", json=payload, headers=self._headers())
created: JsonDict = response.json()
return created
Comment on lines +54 to +56
response = self._client.get(f"/webhooks/{webhook_id}", headers=self._headers())
webhook: JsonDict = response.json()
return webhook
Comment on lines +67 to +70
response = self._client.request(
"DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
)
return bool(response.status_code == httpx.codes.OK)
Comment on lines +80 to +85
if self.event_cache.get("events"):
return self.event_cache["events"]
response = self._client.get("/webhooks/events", headers=self._headers())
names = [event["name"] for event in response.json()]
self.event_cache["events"] = names
return names
Comment on lines +98 to +100
def _headers(self) -> dict[str, str]:
"""Build the authorization headers for a request."""
return {"Authorization": f"Bearer {self._token}"}
Comment on lines +27 to +33
def create_webhook(self, url: str, events: list[str] | None = None) -> JsonDict:
"""Create a webhook subscription.

Args:
url: Callback URL that will receive webhook deliveries.
events: Event names to subscribe to.

@d3rky

d3rky commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

/review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 2 rules

Grey Divider


Action required

1. Wrong webhook URL prefix 🐞 Bug ≡ Correctness
Description
WebhookService calls /webhooks... paths, but this client’s base URL is normalized to strip
/public/v1 and other services’ endpoints explicitly include /public/v1, so these requests will
likely hit the wrong route and fail (e.g., 404).
Code

mpt_api_client/resources/webhooks.py[R40-85]

+        payload = {"url": url, "events": events or []}
+        response = self._client.post("/webhooks", json=payload, headers=self._headers())
+        created: JsonDict = response.json()
+        return created
+
+    def fetch_webhook(self, webhook_id: str) -> JsonDict:
+        """Retrieve a single webhook by its identifier.
+
+        Args:
+            webhook_id: Identifier of the webhook to retrieve.
+
+        Returns:
+            The webhook as returned by the API.
+        """
+        response = self._client.get(f"/webhooks/{webhook_id}", headers=self._headers())
+        webhook: JsonDict = response.json()
+        return webhook
+
+    def delete_webhook(self, webhook_id: str) -> bool:
+        """Delete a webhook subscription.
+
+        Args:
+            webhook_id: Identifier of the webhook to delete.
+
+        Returns:
+            True if the webhook was deleted successfully.
+        """
+        response = self._client.request(
+            "DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
+        )
+        return bool(response.status_code == httpx.codes.OK)
+
+    def list_event_types(self) -> list[str]:
+        """Return the names of all supported webhook event types.
+
+        Results are cached after the first successful call.
+
+        Returns:
+            The list of supported event type names.
+        """
+        if self.event_cache.get("events"):
+            return self.event_cache["events"]
+        response = self._client.get("/webhooks/events", headers=self._headers())
+        names = [event["name"] for event in response.json()]
+        self.event_cache["events"] = names
+        return names
Evidence
The repo standardizes base_url to *not* include /public/v1, and services/tests show that the
request path must include /public/v1/...; WebhookService omits that prefix.

mpt_api_client/resources/webhooks.py[40-85]
mpt_api_client/http/client_utils.py[12-50]
tests/unit/http/test_client_utils.py[28-49]
mpt_api_client/resources/catalog/products.py[80-88]
tests/unit/resources/catalog/test_products.py[173-184]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`WebhookService` hardcodes request paths like `/webhooks` and `/webhooks/events`. In this codebase, the HTTP client normalizes `base_url` by removing `/public` and `/public/v1`, and resource services define endpoints including `/public/v1/...`. As a result, webhook requests are very likely routed incorrectly.

### Issue Context
The repository’s `validate_base_url` explicitly strips `/public` and `/public/v1` from configured base URLs, and unit tests assert resource calls use `/public/v1/...` paths.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[40-85]

### Suggested fix
- Change webhook endpoint paths to include the expected `/public/v1` prefix (e.g., `/public/v1/webhooks`, `/public/v1/webhooks/events`).
- Prefer using `join_url_path()` (or the existing Service/Accessor pattern) when building resource URLs to avoid subtle slash issues.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. No HTTP error raising 🐞 Bug ☼ Reliability
Description
create_webhook, fetch_webhook, and list_event_types call response.json() without
raise_for_status(), so API errors will not raise MPTError as documented and may be silently
treated as success or crash with JSON decode errors.
Code

mpt_api_client/resources/webhooks.py[R40-56]

+        payload = {"url": url, "events": events or []}
+        response = self._client.post("/webhooks", json=payload, headers=self._headers())
+        created: JsonDict = response.json()
+        return created
+
+    def fetch_webhook(self, webhook_id: str) -> JsonDict:
+        """Retrieve a single webhook by its identifier.
+
+        Args:
+            webhook_id: Identifier of the webhook to retrieve.
+
+        Returns:
+            The webhook as returned by the API.
+        """
+        response = self._client.get(f"/webhooks/{webhook_id}", headers=self._headers())
+        webhook: JsonDict = response.json()
+        return webhook
Evidence
WebhookService never calls raise_for_status, while the repo’s HTTP client layer always raises
transformed MPT exceptions on error responses.

mpt_api_client/resources/webhooks.py[27-43]
mpt_api_client/resources/webhooks.py[54-56]
mpt_api_client/http/request_response_utils.py[7-12]
mpt_api_client/http/client.py[95-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
WebhookService parses JSON bodies without checking HTTP status codes. This violates the docstring promise to raise `MPTError` and diverges from the rest of the client, which translates HTTP status errors into `MPTError`/`MPTAPIError`.

### Issue Context
The standard HTTP layer calls `handle_response_http_error()` which wraps `response.raise_for_status()` and raises transformed MPT exceptions.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[40-56]

### Suggested fix
- After each request, call `response.raise_for_status()` and translate `httpx.HTTPStatusError` via the existing exception transformer OR (preferably) refactor WebhookService to use `mpt_api_client.http.client.HTTPClient` so error handling is consistent.
- Ensure the methods’ docstrings match actual behavior (raising `MPTError` on non-2xx).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. DELETE status too strict 🐞 Bug ≡ Correctness
Description
delete_webhook returns True only for HTTP 200, so successful deletions returning other 2xx codes
(commonly 204) will be incorrectly reported as failures.
Code

mpt_api_client/resources/webhooks.py[R67-70]

+        response = self._client.request(
+            "DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
+        )
+        return bool(response.status_code == httpx.codes.OK)
Evidence
The library’s standard approach is raise_for_status() (any 2xx is OK) rather than hard-coding 200
for DELETE.

mpt_api_client/resources/webhooks.py[67-70]
mpt_api_client/http/request_response_utils.py[7-12]
mpt_api_client/http/mixins/delete_mixin.py[1-11]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`delete_webhook()` equates success with `status_code == 200`. Many APIs return `204 No Content` for successful DELETE, and the rest of this client treats any 2xx as success via `raise_for_status()`.

### Issue Context
In this repo, delete operations generally rely on the HTTP layer to raise on non-2xx rather than checking for a specific success code.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[67-70]

### Suggested fix
- Replace the strict `== httpx.codes.OK` check with `response.is_success` (httpx) or accept `{200, 202, 204}` as success.
- Even better: call `response.raise_for_status()` and return `True` if no exception is raised.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Empty cache not reused 🐞 Bug ➹ Performance
Description
list_event_types uses if self.event_cache.get("events"), so a legitimately empty cached list
([]) will be treated as a cache miss and will trigger repeated API calls.
Code

mpt_api_client/resources/webhooks.py[R80-85]

+        if self.event_cache.get("events"):
+            return self.event_cache["events"]
+        response = self._client.get("/webhooks/events", headers=self._headers())
+        names = [event["name"] for event in response.json()]
+        self.event_cache["events"] = names
+        return names
Evidence
The code’s truthiness check makes an empty cached list indistinguishable from a missing cache entry.

mpt_api_client/resources/webhooks.py[80-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The cache check uses truthiness, which fails for empty lists. If the API returns no supported events, the cache will never be used.

### Issue Context
`dict.get()` returns the cached list, but `[]` is falsy.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[80-85]

### Suggested fix
- Change the guard to `if "events" in self.event_cache:` or `cached = self.event_cache.get("events"); if cached is not None:` so `[]` is treated as a valid cached value.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Global cache cross-contamination 🐞 Bug ☼ Reliability
Description
event_cache is a class-level mutable dict shared across all WebhookService instances, so
different base URLs/tokens can overwrite each other’s cached event types and return incorrect
results.
Code

mpt_api_client/resources/webhooks.py[14]

+    event_cache: ClassVar[EventCache] = {}
Evidence
A mutable ClassVar dict is shared by all instances; with a single hard-coded key it will be
overwritten across instances.

mpt_api_client/resources/webhooks.py[14-15]
mpt_api_client/resources/webhooks.py[80-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`event_cache` is a `ClassVar` dict, meaning it is shared global state across all instances in the process. Because it uses a single fixed key (`"events"`), any instance can overwrite another’s cached value.

### Issue Context
This is especially risky in multi-tenant/multi-environment processes (tests, workers, apps) that create multiple clients.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[14-85]

### Suggested fix
- Move the cache to an instance attribute (e.g., `self._event_cache`) OR key the cache by `base_url` (and possibly auth identity) to avoid cross-instance pollution.
- Optionally add invalidation/TTL if event types can change.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. KeyError on missing status 🐞 Bug ☼ Reliability
Description
is_active directly indexes webhook["status"], which will raise KeyError and crash callers if
status is missing from the payload.
Code

mpt_api_client/resources/webhooks.py[96]

+        return bool(webhook["status"] == "active")
Evidence
The implementation uses direct dict indexing which throws on missing keys.

mpt_api_client/resources/webhooks.py[87-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`is_active()` assumes the input payload always contains a `status` key.

### Issue Context
This is a convenience helper; it can be made robust to partial/invalid payloads.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[87-96]

### Suggested fix
- Use `return webhook.get("status") == "active"` (or handle `None` explicitly) to avoid raising `KeyError`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +40 to +85
payload = {"url": url, "events": events or []}
response = self._client.post("/webhooks", json=payload, headers=self._headers())
created: JsonDict = response.json()
return created

def fetch_webhook(self, webhook_id: str) -> JsonDict:
"""Retrieve a single webhook by its identifier.

Args:
webhook_id: Identifier of the webhook to retrieve.

Returns:
The webhook as returned by the API.
"""
response = self._client.get(f"/webhooks/{webhook_id}", headers=self._headers())
webhook: JsonDict = response.json()
return webhook

def delete_webhook(self, webhook_id: str) -> bool:
"""Delete a webhook subscription.

Args:
webhook_id: Identifier of the webhook to delete.

Returns:
True if the webhook was deleted successfully.
"""
response = self._client.request(
"DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
)
return bool(response.status_code == httpx.codes.OK)

def list_event_types(self) -> list[str]:
"""Return the names of all supported webhook event types.

Results are cached after the first successful call.

Returns:
The list of supported event type names.
"""
if self.event_cache.get("events"):
return self.event_cache["events"]
response = self._client.get("/webhooks/events", headers=self._headers())
names = [event["name"] for event in response.json()]
self.event_cache["events"] = names
return names

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Wrong webhook url prefix 🐞 Bug ≡ Correctness

WebhookService calls /webhooks... paths, but this client’s base URL is normalized to strip
/public/v1 and other services’ endpoints explicitly include /public/v1, so these requests will
likely hit the wrong route and fail (e.g., 404).
Agent Prompt
### Issue description
`WebhookService` hardcodes request paths like `/webhooks` and `/webhooks/events`. In this codebase, the HTTP client normalizes `base_url` by removing `/public` and `/public/v1`, and resource services define endpoints including `/public/v1/...`. As a result, webhook requests are very likely routed incorrectly.

### Issue Context
The repository’s `validate_base_url` explicitly strips `/public` and `/public/v1` from configured base URLs, and unit tests assert resource calls use `/public/v1/...` paths.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[40-85]

### Suggested fix
- Change webhook endpoint paths to include the expected `/public/v1` prefix (e.g., `/public/v1/webhooks`, `/public/v1/webhooks/events`).
- Prefer using `join_url_path()` (or the existing Service/Accessor pattern) when building resource URLs to avoid subtle slash issues.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +40 to +56
payload = {"url": url, "events": events or []}
response = self._client.post("/webhooks", json=payload, headers=self._headers())
created: JsonDict = response.json()
return created

def fetch_webhook(self, webhook_id: str) -> JsonDict:
"""Retrieve a single webhook by its identifier.

Args:
webhook_id: Identifier of the webhook to retrieve.

Returns:
The webhook as returned by the API.
"""
response = self._client.get(f"/webhooks/{webhook_id}", headers=self._headers())
webhook: JsonDict = response.json()
return webhook

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. No http error raising 🐞 Bug ☼ Reliability

create_webhook, fetch_webhook, and list_event_types call response.json() without
raise_for_status(), so API errors will not raise MPTError as documented and may be silently
treated as success or crash with JSON decode errors.
Agent Prompt
### Issue description
WebhookService parses JSON bodies without checking HTTP status codes. This violates the docstring promise to raise `MPTError` and diverges from the rest of the client, which translates HTTP status errors into `MPTError`/`MPTAPIError`.

### Issue Context
The standard HTTP layer calls `handle_response_http_error()` which wraps `response.raise_for_status()` and raises transformed MPT exceptions.

### Fix Focus Areas
- mpt_api_client/resources/webhooks.py[40-56]

### Suggested fix
- After each request, call `response.raise_for_status()` and translate `httpx.HTTPStatusError` via the existing exception transformer OR (preferably) refactor WebhookService to use `mpt_api_client.http.client.HTTPClient` so error handling is consistent.
- Ensure the methods’ docstrings match actual behavior (raising `MPTError` on non-2xx).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@d3rky

d3rky commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@CodeAnt-AI: review

@codeant-ai

codeant-ai Bot commented Jun 25, 2026

Copy link
Copy Markdown

CodeAnt AI is running the review.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Jun 25, 2026
@qodo-code-review

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: danger

Failed stage: Run Danger [❌]

Failed test name: ""

Failure summary:

The workflow failed in the softwareone-platform/one-danger@1.1.0 step because Danger could not fetch
PR metadata from the GitHub API.
- Calls to
https://api.github.com/repos/softwareone-platform/mpt-api-python-client/pulls/357/files and
.../pulls/357/commits failed with FetchError: Invalid response body ... Premature close.
- The
underlying error was ERR_STREAM_PREMATURE_CLOSE raised by node-fetch while decompressing the
response (Gunzip), indicating the HTTP response stream was closed unexpectedly (network/API
transport issue rather than a repo code/test failure).

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

145:  HEAD is now at 458f515 Merge f90c0e9914bb33ef03997ecf35fca8d0371bf486 into 4506e1d45ae2365f8214739cfd036e3530532201
146:  ##[endgroup]
147:  [command]/usr/bin/git log -1 --format=%H
148:  458f515ba117bb02dfe84ff23be556fd3f7ab1bf
149:  Node 20 is being deprecated. This workflow is running with Node 24 by default. If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
150:  ##[group]Run softwareone-platform/one-danger@1.1.0
151:  with:
152:  token: ***
153:  node_version: 20
154:  ##[endgroup]
155:  🔧 Installing action dependencies (npm ci in action folder)...
156:  added 107 packages in 3s
157:  21 packages are looking for funding
158:  run `npm fund` for details
159:  ▶️ Running Danger with central dangerfile...
160:  Failed to fetch GitHub pull request files: FetchError: Invalid response body while trying to fetch https://api.github.com/repos/softwareone-platform/mpt-api-python-client/pulls/357/files?page=1&per_page=100: Premature close
161:  at Gunzip.<anonymous> (/home/runner/work/_actions/softwareone-platform/one-danger/1.1.0/node_modules/node-fetch/lib/index.js:400:12)
162:  at Gunzip.emit (node:events:531:35)
163:  at emitErrorNT (node:internal/streams/destroy:170:8)
164:  at emitErrorCloseNT (node:internal/streams/destroy:129:3)
165:  at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {
166:  type: 'system',
167:  errno: 'ERR_STREAM_PREMATURE_CLOSE',
168:  code: 'ERR_STREAM_PREMATURE_CLOSE'
169:  }
170:  Failed to fetch pull request diff: FetchError: Invalid response body while trying to fetch https://api.github.com/repos/softwareone-platform/mpt-api-python-client/pulls/357/files?page=1&per_page=100: Premature close
171:  at Gunzip.<anonymous> (/home/runner/work/_actions/softwareone-platform/one-danger/1.1.0/node_modules/node-fetch/lib/index.js:400:12)
172:  at Gunzip.emit (node:events:531:35)
173:  at emitErrorNT (node:internal/streams/destroy:170:8)
174:  at emitErrorCloseNT (node:internal/streams/destroy:129:3)
175:  at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {
176:  type: 'system',
177:  errno: 'ERR_STREAM_PREMATURE_CLOSE',
178:  code: 'ERR_STREAM_PREMATURE_CLOSE'
179:  }
180:  Error:  FetchError: Invalid response body while trying to fetch https://api.github.com/repos/softwareone-platform/mpt-api-python-client/pulls/357/commits: Premature close
181:  at Gunzip.<anonymous> (/home/runner/work/_actions/softwareone-platform/one-danger/1.1.0/node_modules/node-fetch/lib/index.js:217:52)
182:  at Gunzip.emit (node:events:531:35)
183:  at emitErrorNT (node:internal/streams/destroy:170:8)
184:  at emitErrorCloseNT (node:internal/streams/destroy:129:3)
185:  at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {

@codeant-ai

codeant-ai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Sequence Diagram

This PR adds a WebhookService that lets client code create webhook subscriptions and retrieve supported event types, reusing results after the first event-type lookup.

sequenceDiagram
    participant ClientApp
    participant WebhookService
    participant MPTAPI

    ClientApp->>WebhookService: create_webhook(url, events)
    WebhookService->>MPTAPI: POST webhooks
    MPTAPI-->>WebhookService: Created webhook data
    WebhookService-->>ClientApp: Return webhook payload

    ClientApp->>WebhookService: list_event_types()
    alt events cached
        WebhookService-->>ClientApp: Return cached event names
    else events not cached
        WebhookService->>MPTAPI: GET webhooks events
        MPTAPI-->>WebhookService: Event type list
        WebhookService-->>ClientApp: Return event names
    end
Loading

Generated by CodeAnt AI

Comment on lines +24 to +55
self._client = httpx.Client(base_url=base_url, timeout=timeout)
self._token = token

def create_webhook(self, url: str, events: list[str] | None = None) -> JsonDict:
"""Create a webhook subscription.

Args:
url: Callback URL that will receive webhook deliveries.
events: Event names to subscribe to.

Returns:
The created webhook as returned by the API.

Raises:
MPTError: If the API responds with an error status.
"""
payload = {"url": url, "events": events or []}
response = self._client.post("/webhooks", json=payload, headers=self._headers())
created: JsonDict = response.json()
return created

def fetch_webhook(self, webhook_id: str) -> JsonDict:
"""Retrieve a single webhook by its identifier.

Args:
webhook_id: Identifier of the webhook to retrieve.

Returns:
The webhook as returned by the API.
"""
response = self._client.get(f"/webhooks/{webhook_id}", headers=self._headers())
webhook: JsonDict = response.json()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Architect Review — HIGH

Webhook operations use a standalone httpx.Client with direct .post()/.get() calls, bypassing the shared HTTPClient/AsyncHTTPClient layer that applies retry and handle_response_http_error transformation; as implemented, these methods never raise the documented MPT exceptions (e.g. MPTError) and will return error responses as normal JSON payloads instead.

Suggestion: Route webhook requests through the existing HTTPClient/AsyncHTTPClient service layer (as other resources do via Service/AsyncService and ResourceAccessor), so error handling, retries, and exception types remain consistent with the rest of the SDK (http/client.py, http/async_client.py, http/request_response_utils.py).

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is an **Architect / Logical Review** comment left during a code review. These reviews are first-class, important findings — not optional suggestions. Do NOT dismiss this as a 'big architectural change' just because the title says architect review; most of these can be resolved with a small, localized fix once the intent is understood.

**Path:** mpt_api_client/resources/webhooks.py
**Line:** 24:55
**Comment:**
	*HIGH: Webhook operations use a standalone `httpx.Client` with direct `.post()`/`.get()` calls, bypassing the shared `HTTPClient`/`AsyncHTTPClient` layer that applies retry and `handle_response_http_error` transformation; as implemented, these methods never raise the documented MPT exceptions (e.g. `MPTError`) and will return error responses as normal JSON payloads instead.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
If a suggested approach is provided above, use it as the authoritative instruction. If no explicit code suggestion is given, you MUST still draft and apply your own minimal, localized fix — do not punt back with 'no suggestion provided, review manually'. Keep the change as small as possible: add a guard clause, gate on a loading state, reorder an await, wrap in a conditional, etc. Do not refactor surrounding code or expand scope beyond the finding.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix

class WebhookService:
"""Service for creating, reading and deleting webhook subscriptions."""

event_cache: ClassVar[EventCache] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The cache is a mutable class variable shared by all service instances, so different tokens/base URLs/process contexts will read and overwrite the same cache entry, and concurrent access is unsynchronized. Move this cache to instance state (or key it per base URL/auth context with locking) to avoid cross-instance contamination/races. [race condition]

Severity Level: Major ⚠️
- ❌ Event-type cache shared across different base URLs and tokens.
- ⚠️ Stale or incorrect event lists in multi-tenant processes.
- ⚠️ Subtle cross-environment data leakage between service instances.
Steps of Reproduction ✅
1. Note that `WebhookService` defines `event_cache: ClassVar[EventCache] = {}` at
`mpt_api_client/resources/webhooks.py:14`, making `event_cache` a mutable class variable
shared by all instances.

2. In one part of an application, create
`WebhookService(base_url=\"https://api.s1.show/public\", token=\"token-a\")` and call
`list_event_types()` at lines 72-85; this fetches `"/webhooks/events"` at line 82, builds
`names` at line 83, and stores them under `self.event_cache["events"]` at line 84.

3. In the same process, create a second `WebhookService` instance with a different
environment or base URL, e.g. `WebhookService(base_url=\"https://staging.s1.show/public\",
token=\"token-b\")`, and call `list_event_types()` again.

4. Because `event_cache` is shared at the class level, the `if
self.event_cache.get("events")` check at line 80 will return the cached list from the
first instance, so the second instance never hits its own `/webhooks/events` endpoint and
instead reuses event types from a different base URL/auth context, leading to
cross-environment data leakage and stale results; concurrent access from multiple threads
would also read/write this shared dict without any synchronization.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** mpt_api_client/resources/webhooks.py
**Line:** 14:14
**Comment:**
	*Race Condition: The cache is a mutable class variable shared by all service instances, so different tokens/base URLs/process contexts will read and overwrite the same cache entry, and concurrent access is unsynchronized. Move this cache to instance state (or key it per base URL/auth context with locking) to avoid cross-instance contamination/races.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

token: Bearer token used to authenticate requests.
timeout: Request timeout in seconds.
"""
self._client = httpx.Client(base_url=base_url, timeout=timeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: A dedicated httpx.Client instance is created but never closed, which can leak sockets/file descriptors over time in long-lived processes. Add an explicit lifecycle hook (for example, a close() method and/or context-manager support) and ensure callers close the service when finished. [resource leak]

Severity Level: Major ⚠️
- ⚠️ Long-lived processes can accumulate unused HTTP connections.
- ⚠️ Potential exhaustion of file descriptors under repeated instantiation.
- ⚠️ Harder to reason about client lifecycle versus main HTTPClient.
Steps of Reproduction ✅
1. In your application code, import `WebhookService` from
`mpt_api_client/resources/webhooks.py:11` and instantiate it in a loop or per-request
(e.g. inside a web handler) with different tokens:
`WebhookService(base_url="https://api.s1.show/public", token="<token>")`.

2. Each instantiation executes `__init__` at `mpt_api_client/resources/webhooks.py:16-24`,
which constructs a new `httpx.Client` and stores it in `self._client` (line 24) without
any corresponding `close()` or context manager.

3. Call `create_webhook()` (defined at `mpt_api_client/resources/webhooks.py:27-43`) or
other methods on each instance so that `httpx` establishes TCP connections and maintains
them in its internal connection pool.

4. Discard the `WebhookService` instances without ever closing `self._client`; over time
in a long-lived process, repeated instantiation leads to many live `httpx.Client` objects
whose underlying sockets/file descriptors are only cleaned up by non-deterministic GC,
risking exhaustion of available connections and OS file descriptors.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** mpt_api_client/resources/webhooks.py
**Line:** 24:24
**Comment:**
	*Resource Leak: A dedicated `httpx.Client` instance is created but never closed, which can leak sockets/file descriptors over time in long-lived processes. Add an explicit lifecycle hook (for example, a `close()` method and/or context-manager support) and ensure callers close the service when finished.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +41 to +43
response = self._client.post("/webhooks", json=payload, headers=self._headers())
created: JsonDict = response.json()
return created

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The method parses and returns the response body without validating HTTP status, so 4xx/5xx responses can be treated as successful results or raise unexpected JSON parsing errors instead of the documented client exception flow. Validate status before decoding/returning the payload. [api mismatch]

Severity Level: Critical 🚨
- ❌ Webhook creation errors not surfaced as `MPTError`.
- ❌ Callers may treat error payloads as valid webhook objects.
- ⚠️ Inconsistent error handling versus other client services.
- ⚠️ Harder debugging due to missing unified exception types.
Steps of Reproduction ✅
1. Instantiate `WebhookService` from `mpt_api_client/resources/webhooks.py:11` in user
code with an invalid or expired token, e.g.
`WebhookService(base_url="https://api.s1.show/public", token="invalid")`, which sets up
`self._client` in `__init__` at lines 16-24.

2. Call `create_webhook()` at `mpt_api_client/resources/webhooks.py:27-43` with any URL
and events; this method issues `self._client.post("/webhooks", ...)` at line 41.

3. When the upstream API returns a 4xx/5xx (e.g. 401 Unauthorized for the bad token),
`create_webhook()` still executes `response.json()` at line 42 and returns that payload at
line 43 without calling `response.raise_for_status()` or any equivalent error handling.

4. Observe that, contrary to the docstring at lines 37-39 ("Raises: MPTError: If the API
responds with an error status.") and the library-wide pattern where `HTTPClient.request()`
in `mpt_api_client/http/client.py:95-115` applies `handle_response_http_error()` to raise
`MPTError` subclasses, `create_webhook()` silently returns an error payload or even raises
a raw JSON parsing error instead of the documented `MPTError`.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** mpt_api_client/resources/webhooks.py
**Line:** 41:43
**Comment:**
	*Api Mismatch: The method parses and returns the response body without validating HTTP status, so 4xx/5xx responses can be treated as successful results or raise unexpected JSON parsing errors instead of the documented client exception flow. Validate status before decoding/returning the payload.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Returns:
The webhook as returned by the API.
"""
response = self._client.get(f"/webhooks/{webhook_id}", headers=self._headers())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The webhook routes are hardcoded without the /public/v1 API prefix used throughout this client, which will hit different endpoints and can produce 404/contract mismatches against the configured API base convention. Align these paths with the existing versioned endpoint pattern. [api mismatch]

Severity Level: Critical 🚨
- ❌ Webhook methods hit non-versioned, likely non-existent endpoints.
- ❌ All webhook operations likely return 404 or invalid responses.
- ⚠️ Inconsistent routing versus other `/public/v1`-based services.
- ⚠️ Confusing divergence from documented OpenAPI endpoint structure.
Steps of Reproduction ✅
1. Configure the main client following `docs/usage.md:56-63`, using
`base_url="https://api.s1.show/public"`, and instantiate
`WebhookService(base_url=\"https://api.s1.show/public\", token=\"<token>\")` from
`mpt_api_client/resources/webhooks.py:11-24`.

2. Call `fetch_webhook("WH-123")` at `mpt_api_client/resources/webhooks.py:45-56`; this
issues `self._client.get(f"/webhooks/{webhook_id}", ...)` at line 54, which resolves to
`https://api.s1.show/public/webhooks/WH-123` given the base URL.

3. Compare this with the established pattern for other resources, e.g.
`BatchesServiceConfig._endpoint = "/public/v1/notifications/batches"` at
`mpt_api_client/resources/notifications/batches.py:22-29` and similar `/public/v1/...`
endpoints throughout `mpt_api_client/resources/notifications/*.py`, which rely on the HTTP
transport stripping `/public` from the base URL and adding `/public/v1` via the
`_endpoint`.

4. Because `WebhookService` bypasses the shared `HTTPClient` and uses raw `httpx.Client`
without `validate_base_url()` or a `/public/v1/...` path, its requests target
`/public/webhooks` instead of the versioned `/public/v1/...` notification webhook
endpoints defined by the upstream OpenAPI spec (`README.md:23-24`,
`docs/architecture.md:11-13`), leading to 404s or contract mismatches for all webhook
operations (create, fetch, delete, list events).

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** mpt_api_client/resources/webhooks.py
**Line:** 54:54
**Comment:**
	*Api Mismatch: The webhook routes are hardcoded without the `/public/v1` API prefix used throughout this client, which will hit different endpoints and can produce 404/contract mismatches against the configured API base convention. Align these paths with the existing versioned endpoint pattern.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

response = self._client.request(
"DELETE", f"/webhooks/{webhook_id}", headers=self._headers()
)
return bool(response.status_code == httpx.codes.OK)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Deletion success is restricted to HTTP 200, but REST delete endpoints commonly return 204 No Content for successful deletion; this will incorrectly report failure on valid responses. Treat all successful 2xx delete responses as success (or at least include 204). [api mismatch]

Severity Level: Major ⚠️
- ❌ Successful deletions reported as failures to callers.
- ⚠️ Client code may retry or log incorrect error states.
- ⚠️ Inconsistent semantics with other HTTP operations using 2xx.
Steps of Reproduction ✅
1. In user code, instantiate `WebhookService` from
`mpt_api_client/resources/webhooks.py:11` with a valid base URL and token, so that
`__init__` at lines 16-24 sets up `self._client`.

2. Call `delete_webhook()` at `mpt_api_client/resources/webhooks.py:58-70` with an
existing webhook ID, triggering `self._client.request("DELETE", f"/webhooks/{webhook_id}",
...)` at lines 67-68.

3. If the upstream API follows common REST conventions and returns `204 No Content` (or
another 2xx such as 202) on successful deletion, `response.status_code` will be 204, not
`httpx.codes.OK` (200).

4. The method executes `return bool(response.status_code == httpx.codes.OK)` at line 70,
which evaluates to `False` for 204/202, causing `delete_webhook()` to report failure even
though the resource was deleted successfully on the server.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** mpt_api_client/resources/webhooks.py
**Line:** 70:70
**Comment:**
	*Api Mismatch: Deletion success is restricted to HTTP 200, but REST delete endpoints commonly return 204 No Content for successful deletion; this will incorrectly report failure on valid responses. Treat all successful 2xx delete responses as success (or at least include 204).

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai

codeant-ai Bot commented Jun 25, 2026

Copy link
Copy Markdown

CodeAnt AI finished running the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants