Skip to content

bug/category to viper role#170

Open
xnorkl wants to merge 3 commits into
developfrom
bug/category-to-viper-role
Open

bug/category to viper role#170
xnorkl wants to merge 3 commits into
developfrom
bug/category-to-viper-role

Conversation

@xnorkl
Copy link
Copy Markdown
Contributor

@xnorkl xnorkl commented May 22, 2026

  • test(viper): add regression for TapirXL device_class -> Viper role pipeline
  • fix(upsert): accept TapirXL device_class and app_sw_version on asset upsert
  • fix(viper): emit camelCase integrationUpload payload

Summary by CodeRabbit

  • New Features

    • Asset upsert API now accepts app_sw_version and device_class fields.
    • Device class automatically normalized to category when provided.
    • Viper webhook responses updated to use camelCase JSON format.
  • Tests

    • Added regression test coverage for telemetry integration with Viper pipeline.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

This PR refactors Viper integration to standardize JSON payloads to camelCase, changes vendor ID sourcing to use asset manufacturer, extends asset upsert to accept device classification fields, and validates the complete TapirXL telemetry pipeline with new regression tests.

Changes

Viper camelCase and device classification pipeline

Layer / File(s) Summary
AssetUpsertSerializer device classification extension
blueflow/views/asset.py
AssetUpsertSerializer adds optional app_sw_version and device_class fields, and a validate() method that copies device_class into category when category is absent.
Viper model camelCase and optional field refactoring
blueflow/models/viper.py
ViperAsset.to_dict() rewrites to explicitly build camelCase keys (upstreamApi, vendorId, macAddress) with conditional inclusion of optional fields and sources vendor_id from asset.manufacturer. ViperWebhookResponse.to_dict() constructs explicit response payload with camelCase pagination keys (pageSize, totalCount, totalPages).
Celery and model unit tests for camelCase validation
blueflow/celery/tasks.py, blueflow/celery/tests/test_viper.py, blueflow/tests/test_viper_models.py
Type annotation in tasks.py updated to ViperWebhookRequest; celery webhook tests assert camelCase keys (pageSize, totalCount, totalPages, upstreamApi, vendorId) in POST payload; comprehensive model unit tests validate required camelCase keys, conditional field omission when unset, vendor ID mapping from manufacturer, and absence of snake_case alternatives.
TapirXL telemetry fixture and end-to-end regression tests
blueflow/tests/fixtures/tapirxl_telemetry.jsonl, blueflow/tests/test_tapirxl_viper_regression.py
Adds 8-record JSONL telemetry fixture and regression test module: unit tests for VRL field-mapping mirror (device_class→category, conditional vendor/product/version mapping), persistence tests validating device_class and app_sw_version survive upsert into stored Asset fields, and full pipeline regression asserting each golden record's Viper role matches its device_class.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • virtalabs/blueflow#169: Updates BlueFlow's Viper integration payload schema with camelCase keys and pagination field changes.
  • virtalabs/blueflow#83: Modifies AssetUpsertSerializer validation logic and serializer fields.

Suggested labels

integrations, api

Poem

🐰 A rabbit hops through camelCase fields,
Where vendors map to manufacturers' yields,
Device classes flow from TapirXL's sight,
Through upsert and Viper—all schemas align right,
The payload now speaks in a cleaner tone! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'bug/category to viper role' describes a core aspect of the changes—the propagation of device_class/category data to Viper role in the webhook payload—which is central to the PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.
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.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bug/category-to-viper-role

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
blueflow/celery/tasks.py (1)

31-41: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout to the Viper callback POST
requests.post(...) at blueflow/celery/tasks.py:36 is a blocking network call with no timeout, so a slow/stuck callback can pin Celery workers.

Suggested fix
         response = requests.post(
             viper_data.callback,
             json=as_dict,
             headers={"Content-Type": "application/json"},
+            timeout=30,
         )
🤖 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 `@blueflow/celery/tasks.py` around lines 31 - 41, The POST to the Viper
callback in the loop uses requests.post(...) without a timeout which can block
Celery workers; update the call in the loop (where
ViperWebhookResponseList.from_request is iterated and response.to_dict() is
used) to pass a reasonable timeout parameter (e.g., timeout=5) to requests.post
and handle requests.exceptions.Timeout/RequestException around the call so the
task can log the failure and continue/raise appropriately rather than blocking
the worker.
🤖 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 `@blueflow/tests/test_tapirxl_viper_regression.py`:
- Around line 164-214: Move all local imports (eg. "from blueflow import models"
found inside test_device_class_alias_persisted_through_upsert,
test_device_class_persisted_through_upsert,
test_version_persisted_through_upsert and other tests) to the top of the test
module; replace any identity comparisons against numeric HTTP status codes (e.g.
"is 201" or "is 200") with equality checks or named constants (e.g.
resp.status_code == 201 or use HTTPStatus.CREATED/OK) to remove magic-status
lint warnings; and shorten or rewrap the overlong docstring in
test_version_persisted_through_upsert so it fits the line length limit. Ensure
you update all occurrences (also around the other failing ranges mentioned) so
the module passes lint.

In `@blueflow/views/asset.py`:
- Around line 119-123: The current validate method overwrites category when
attrs contains a falsey value; change the condition to only map device_class if
the category key is absent. In the validate function, replace the truthiness
check (attrs.get("category")) with a key-presence check (e.g., "'category' not
in attrs") so attrs["category"] is set from device_class only when the category
key isn't provided. Ensure you keep the existing device_class extraction
(device_class = attrs.pop("device_class", None)) and the rest of the method
intact.

---

Outside diff comments:
In `@blueflow/celery/tasks.py`:
- Around line 31-41: The POST to the Viper callback in the loop uses
requests.post(...) without a timeout which can block Celery workers; update the
call in the loop (where ViperWebhookResponseList.from_request is iterated and
response.to_dict() is used) to pass a reasonable timeout parameter (e.g.,
timeout=5) to requests.post and handle
requests.exceptions.Timeout/RequestException around the call so the task can log
the failure and continue/raise appropriately rather than blocking the worker.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85a9dd16-3e17-4073-8006-823d4c1db71b

📥 Commits

Reviewing files that changed from the base of the PR and between 7c01696 and 187883f.

📒 Files selected for processing (7)
  • blueflow/celery/tasks.py
  • blueflow/celery/tests/test_viper.py
  • blueflow/models/viper.py
  • blueflow/tests/fixtures/tapirxl_telemetry.jsonl
  • blueflow/tests/test_tapirxl_viper_regression.py
  • blueflow/tests/test_viper_models.py
  • blueflow/views/asset.py

Comment on lines +164 to +214
def test_device_class_alias_persisted_through_upsert(asset_edit_client) -> None:
"""Raw TapirXL device_class field maps to category when Vector is bypassed."""
from blueflow import models

resp = asset_edit_client.put(
"/api/assets/upsert/",
json.dumps(
{
"mac_address": "DE:AD:BE:EF:00:01",
"ip_address": "10.10.10.99",
"device_class": "patient_monitor",
}
),
content_type="application/json",
)
assert resp.status_code == 201
asset = models.Asset.objects.get(mac_address="de:ad:be:ef:00:01")
assert asset.category == "patient_monitor"


def test_device_class_persisted_through_upsert(asset_edit_client) -> None:
"""category from VRL transform survives PUT /api/assets/upsert/ → Asset.category."""
from blueflow import models

payload = _vrl_transform(
{
"mac_address": "00:09:FB:BD:75:6D",
"ip_address": "10.10.10.21",
"hostname": "MX700-bed12",
"vendor": "philips",
"product": "intellivue_mx700",
"version": None,
"device_class": "patient_monitor",
"open_ports": [3702],
"confidence": "LOW",
}
)

resp = asset_edit_client.put(
"/api/assets/upsert/",
json.dumps(payload),
content_type="application/json",
)
assert resp.status_code == 201
asset = models.Asset.objects.get(mac_address="00:09:fb:bd:75:6d")
assert asset.category == "patient_monitor"


def test_version_persisted_through_upsert(asset_edit_client) -> None:
"""app_sw_version from VRL transform survives PUT /api/assets/upsert/ → Asset.app_sw_version."""
from blueflow import models
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix current lint blockers in this test module before merge.

The new file still fails lint on: imports inside functions (Line 166, Line 186, Line 214, Line 275), magic status code comparisons (Line 207, Line 236), and one overlong docstring line (Line 213).

Suggested fix
 import json
 import uuid
+from http import HTTPStatus
 from pathlib import Path
 from unittest.mock import patch
 
 import pytest
 
+from blueflow import models
+from blueflow.celery.tasks import viper_webhook
 from blueflow.models.viper import ViperWebhookRequest, ViperWebhookResponseList
@@
 def test_device_class_alias_persisted_through_upsert(asset_edit_client) -> None:
     """Raw TapirXL device_class field maps to category when Vector is bypassed."""
-    from blueflow import models
@@
-    assert resp.status_code == 201
+    assert resp.status_code == HTTPStatus.CREATED
@@
 def test_device_class_persisted_through_upsert(asset_edit_client) -> None:
     """category from VRL transform survives PUT /api/assets/upsert/ → Asset.category."""
-    from blueflow import models
@@
-    assert resp.status_code == 201
+    assert resp.status_code == HTTPStatus.CREATED
@@
 def test_version_persisted_through_upsert(asset_edit_client) -> None:
-    """app_sw_version from VRL transform survives PUT /api/assets/upsert/ → Asset.app_sw_version."""
-    from blueflow import models
+    """app_sw_version from VRL transform survives
+    PUT /api/assets/upsert/ → Asset.app_sw_version.
+    """
@@
-    assert resp.status_code == 201
+    assert resp.status_code == HTTPStatus.CREATED
@@
     with patch("blueflow.celery.tasks.requests.post") as mock_post:
-        from blueflow.celery.tasks import viper_webhook
-
         viper_webhook.apply(

Also applies to: 231-236, 275-287

🧰 Tools
🪛 GitHub Actions: Lint / 0_lint.txt

[error] 186-186: PLC0415: import should be at the top-level of a file


[error] 207-207: PLR2004: Magic value used in comparison; consider replacing 201 with a constant variable


[error] 213-213: E501: Line too long (100 > 88)


[error] 214-214: PLC0415: import should be at the top-level of a file

🪛 GitHub Actions: Lint / lint

[error] 186-186: PLC0415 import should be at the top-level of a file (from blueflow import models)


[error] 207-207: PLR2004 Magic value used in comparison, consider replacing 201 with a constant variable (assert resp.status_code == 201)


[error] 213-213: E501 Line too long (100 > 88) in test_version_persisted_through_upsert docstring


[error] 214-214: PLC0415 import should be at the top-level of a file (from blueflow import models)

🤖 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 `@blueflow/tests/test_tapirxl_viper_regression.py` around lines 164 - 214, Move
all local imports (eg. "from blueflow import models" found inside
test_device_class_alias_persisted_through_upsert,
test_device_class_persisted_through_upsert,
test_version_persisted_through_upsert and other tests) to the top of the test
module; replace any identity comparisons against numeric HTTP status codes (e.g.
"is 201" or "is 200") with equality checks or named constants (e.g.
resp.status_code == 201 or use HTTPStatus.CREATED/OK) to remove magic-status
lint warnings; and shorten or rewrap the overlong docstring in
test_version_persisted_through_upsert so it fits the line length limit. Ensure
you update all occurrences (also around the other failing ranges mentioned) so
the module passes lint.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

further to the rabbit's point django has a preferred pattern for lazy imports: https://docs.djangoproject.com/en/6.0/ref/applications/#django.apps.apps.get_model

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@t-a-y-l-o-r — great callout on apps.get_model(). That's the right Django idiom for production code where lazy lookup is needed to break circular import cycles — e.g. inside model methods, signal handlers, or anything that may execute before the full app registry is ready:

# Good in production code (models, signals, middleware)
from django.apps import apps

def some_signal_handler(sender, instance, **kwargs):
    Asset = apps.get_model("blueflow", "Asset")
    Asset.objects.filter(...)

In test files though, pytest-django fully populates the app registry before any test runs, so circular imports aren't a concern. The standard pattern — and the one that satisfies the PLC0415 lint rule — is a plain top-level import:

# Idiomatic for test files
from blueflow import models  # ← top of file

def test_device_class_alias_persisted_through_upsert(asset_edit_client) -> None:
    ...
    asset = models.Asset.objects.get(mac_address="de:ad:be:ef:00:01")

So for this PR the lint fix is just hoisting the from blueflow import models (and from blueflow.celery.tasks import viper_webhook) to the module top, which is both simpler and what the linter expects.


✏️ Learnings added
Learnt from: t-a-y-l-o-r
Repo: virtalabs/blueflow PR: 170
File: blueflow/tests/test_tapirxl_viper_regression.py:164-214
Timestamp: 2026-05-25T16:10:21.212Z
Learning: In the blueflow project (virtalabs/blueflow), `apps.get_model()` is the Django-preferred pattern for lazy model lookups in production code (model methods, signals, middleware) to avoid circular import cycles. In test files, however, pytest-django fully populates the app registry before tests run, so plain top-level imports (`from blueflow import models`) are both correct and idiomatic — and are required by the PLC0415 lint rule. Do not flag top-level imports in test files as needing `apps.get_model()` replacement.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: t-a-y-l-o-r
Repo: virtalabs/blueflow PR: 78
File: blueflow/views/asset.py:1058-1058
Timestamp: 2026-04-07T16:44:13.557Z
Learning: In blueflow’s Django `Asset` model (`blueflow/models`), `open_ports_tcp` is a list field declared with `default=list`, so it will not be `None` at the model/database level. When reviewing code, avoid flagging (or adding) unnecessary guards such as `asset.open_ports_tcp or []`; you can treat `asset.open_ports_tcp` as an always-present list.

Comment thread blueflow/views/asset.py
Comment on lines +119 to +123
def validate(self, attrs: dict) -> dict:
"""Map TapirXL ``device_class`` to ``category`` when Vector is bypassed."""
device_class = attrs.pop("device_class", None)
if device_class and not attrs.get("category"):
attrs["category"] = device_class
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Map device_class only when category is truly absent.

Line 122 currently treats falsey values ("", None) as missing and will overwrite an explicit category input. The contract here is “when category is not present”.

Suggested fix
-        if device_class and not attrs.get("category"):
+        if device_class and "category" not in attrs:
             attrs["category"] = device_class
🤖 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 `@blueflow/views/asset.py` around lines 119 - 123, The current validate method
overwrites category when attrs contains a falsey value; change the condition to
only map device_class if the category key is absent. In the validate function,
replace the truthiness check (attrs.get("category")) with a key-presence check
(e.g., "'category' not in attrs") so attrs["category"] is set from device_class
only when the category key isn't provided. Ensure you keep the existing
device_class extraction (device_class = attrs.pop("device_class", None)) and the
rest of the method intact.

Copy link
Copy Markdown
Collaborator

@t-a-y-l-o-r t-a-y-l-o-r left a comment

Choose a reason for hiding this comment

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

looks workable for hackathon

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I've been running with the assumption we dont want to normalize having network data in the repo just to be extra safe. I'm sure this is synthetic but maybe that's worth chatting about post-hackathon

Comment on lines +164 to +214
def test_device_class_alias_persisted_through_upsert(asset_edit_client) -> None:
"""Raw TapirXL device_class field maps to category when Vector is bypassed."""
from blueflow import models

resp = asset_edit_client.put(
"/api/assets/upsert/",
json.dumps(
{
"mac_address": "DE:AD:BE:EF:00:01",
"ip_address": "10.10.10.99",
"device_class": "patient_monitor",
}
),
content_type="application/json",
)
assert resp.status_code == 201
asset = models.Asset.objects.get(mac_address="de:ad:be:ef:00:01")
assert asset.category == "patient_monitor"


def test_device_class_persisted_through_upsert(asset_edit_client) -> None:
"""category from VRL transform survives PUT /api/assets/upsert/ → Asset.category."""
from blueflow import models

payload = _vrl_transform(
{
"mac_address": "00:09:FB:BD:75:6D",
"ip_address": "10.10.10.21",
"hostname": "MX700-bed12",
"vendor": "philips",
"product": "intellivue_mx700",
"version": None,
"device_class": "patient_monitor",
"open_ports": [3702],
"confidence": "LOW",
}
)

resp = asset_edit_client.put(
"/api/assets/upsert/",
json.dumps(payload),
content_type="application/json",
)
assert resp.status_code == 201
asset = models.Asset.objects.get(mac_address="00:09:fb:bd:75:6d")
assert asset.category == "patient_monitor"


def test_version_persisted_through_upsert(asset_edit_client) -> None:
"""app_sw_version from VRL transform survives PUT /api/assets/upsert/ → Asset.app_sw_version."""
from blueflow import models
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

further to the rabbit's point django has a preferred pattern for lazy imports: https://docs.djangoproject.com/en/6.0/ref/applications/#django.apps.apps.get_model

Comment thread blueflow/views/asset.py
required=False, allow_blank=True, allow_null=True
)
os = serializers.CharField(required=False, allow_blank=True, allow_null=True)
app_sw_version = serializers.CharField(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

all_blank and all_null in Django is generally considered an anti-pattern in Django. we should prefer either blank or null as our empty state; more post-hackathon stuff

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as a quick note, I generally make blank-able false and null-able true. But there are arguments for either

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants