Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions backend/app/integrations/snipeit/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from __future__ import annotations

from typing import Any
from typing import TypedDict

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Importieren Sie NotRequired aus typing, um optionale Felder in der TypedDict explizit zu kennzeichnen, anstatt total=False zu verwenden.

Suggested change
from typing import TypedDict
from typing import NotRequired, TypedDict

from urllib.parse import quote

import httpx
Expand All @@ -17,6 +17,22 @@
from app.services.errors import AppLookupNotFoundError


class _SnipeITAssetResponse(TypedDict, total=False):
"""Subset of Snipe-IT `/api/v1/hardware/bytag/{tag}` response we actually
read. Strikt-typisiert statt `dict[str, Any]` — beschränkt sich auf die
Felder die `_payload_to_label` konsumiert. `total=False` weil alle Felder
außer `id` optional sind (User-Mapping kann Snipe-IT-fields ausblenden).

Audit #67 / PR #51 Finding: das alte `dict[str, Any]` hat das Hub-mypy-
strict-Verbot von `Any` in Plugin-Responses verletzt.
"""

id: int
asset_tag: str | None
name: str | None
serial: str | None
Comment on lines +20 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Mit total=False werden alle Felder der TypedDict als optional markiert, einschließlich id. Dies widerspricht dem Docstring, der besagt, dass alle Felder außer id optional sind.

Da das Projekt Python 3.12+ verwendet, ist es sauberer, total=False zu entfernen und stattdessen die optionalen Felder explizit mit NotRequired zu typisieren. Dadurch bleibt id als einziges erforderliches Feld im Typ-System bestehen.

Suggested change
class _SnipeITAssetResponse(TypedDict, total=False):
"""Subset of Snipe-IT `/api/v1/hardware/bytag/{tag}` response we actually
read. Strikt-typisiert statt `dict[str, Any]`beschränkt sich auf die
Felder die `_payload_to_label` konsumiert. `total=False` weil alle Felder
außer `id` optional sind (User-Mapping kann Snipe-IT-fields ausblenden).
Audit #67 / PR #51 Finding: das alte `dict[str, Any]` hat das Hub-mypy-
strict-Verbot von `Any` in Plugin-Responses verletzt.
"""
id: int
asset_tag: str | None
name: str | None
serial: str | None
class _SnipeITAssetResponse(TypedDict):
"""Subset of Snipe-IT /api/v1/hardware/bytag/{tag} response we actually
read. Strikt-typisiert statt dict[str, Any] — beschränkt sich auf die
Felder die _payload_to_label konsumiert.
Audit #67 / PR #51 Finding: das alte dict[str, Any] hat das Hub-mypy-
strict-Verbot von Any in Plugin-Responses verletzt.
"""
id: int
asset_tag: NotRequired[str | None]
name: NotRequired[str | None]
serial: NotRequired[str | None]



class SnipeITNotFoundError(AppLookupNotFoundError):
"""Raised when no Snipe-IT asset matches the given tag."""

Expand Down Expand Up @@ -72,10 +88,10 @@ async def lookup(self, asset_tag: str) -> LabelData:
# decide whether to treat them as configuration errors vs transient failures.
response.raise_for_status()

payload: dict[str, Any] = response.json()
payload: _SnipeITAssetResponse = response.json()
return self._payload_to_label(payload, asset_tag)

def _payload_to_label(self, payload: dict[str, Any], asset_tag: str) -> LabelData:
def _payload_to_label(self, payload: _SnipeITAssetResponse, asset_tag: str) -> LabelData:
asset_id = payload.get("id")
if asset_id is None:
raise ValueError(f"Snipe-IT response for {asset_tag!r} is missing required field 'id'")
Expand Down
Loading