Skip to content

Commit

Permalink
Clean up asset classes (ohcnetwork#2494)
Browse files Browse the repository at this point in the history
Clean up asset classes (ohcnetwork#2494)
---------

Co-authored-by: Aakash Singh <[email protected]>
  • Loading branch information
DraKen0009 and sainak authored Nov 14, 2024
1 parent 05cf1cf commit 5b2b304
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 30 deletions.
4 changes: 2 additions & 2 deletions care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from care.facility.models.bed import AssetBed, ConsultationBed
from care.users.models import User
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.cache.cache_allowed_facilities import get_accessible_facilities
from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices
from care.utils.queryset.asset_bed import get_asset_queryset
Expand Down Expand Up @@ -389,7 +390,6 @@ def operate_assets(self, request, *args, **kwargs):
This API is used to operate assets. API accepts the asset_id and action as parameters.
"""
try:
action = request.data["action"]
asset: Asset = self.get_object()
middleware_hostname = (
asset.meta.get(
Expand All @@ -405,7 +405,7 @@ def operate_assets(self, request, *args, **kwargs):
"middleware_hostname": middleware_hostname,
}
)
result = asset_class.handle_action(action)
result = asset_class.handle_action(**request.data["action"])
return Response({"result": result}, status=status.HTTP_200_OK)

except ValidationError as e:
Expand Down
141 changes: 141 additions & 0 deletions care/facility/tests/test_asset_api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from django.utils.timezone import now, timedelta
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.test import APITestCase

from care.facility.models import Asset, Bed
from care.users.models import User
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.hl7monitor import HL7MonitorAsset
from care.utils.assetintegration.onvif import OnvifAsset
from care.utils.assetintegration.ventilator import VentilatorAsset
from care.utils.tests.test_utils import TestUtils


Expand All @@ -31,6 +35,143 @@ def setUp(self) -> None:
super().setUp()
self.asset = self.create_asset(self.asset_location)

def validate_invalid_meta(self, asset_class, meta):
with self.assertRaises(ValidationError):
asset_class(meta)

def test_meta_validations_for_onvif_asset(self):
valid_meta = {
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
}
onvif_asset = OnvifAsset(valid_meta)
self.assertEqual(onvif_asset.middleware_hostname, "middleware.local")
self.assertEqual(onvif_asset.host, "192.168.0.1")
self.assertEqual(onvif_asset.username, "username")
self.assertEqual(onvif_asset.password, "password")
self.assertEqual(onvif_asset.access_key, "access_key")
self.assertTrue(onvif_asset.insecure_connection)

invalid_meta_cases = [
# Invalid format for camera_access_key
{
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"camera_access_key": "invalid_format",
},
# Missing username/password in camera_access_key
{
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"camera_access_key": "invalid_format",
},
# Missing middleware_hostname
{
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
},
# Missing local_ip_address
{
"middleware_hostname": "middleware.local",
"camera_access_key": "username:password:access_key",
},
# Invalid value for insecure_connection
{
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": "invalid_value",
},
]
for meta in invalid_meta_cases:
self.validate_invalid_meta(OnvifAsset, meta)

def test_meta_validations_for_ventilator_asset(self):
valid_meta = {
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
}
ventilator_asset = VentilatorAsset(valid_meta)
self.assertEqual(ventilator_asset.middleware_hostname, "middleware.local")
self.assertEqual(ventilator_asset.host, "192.168.0.1")
self.assertTrue(ventilator_asset.insecure_connection)

invalid_meta_cases = [
# Missing id
{
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
},
# Missing middleware_hostname
{"id": "123", "local_ip_address": "192.168.0.1"},
# Missing local_ip_address
{"id": "123", "middleware_hostname": "middleware.local"},
# Invalid insecure_connection
{
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": "invalid_value",
},
# Camera access key not required for ventilator, invalid meta
{
"id": "21",
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
},
]
for meta in invalid_meta_cases:
self.validate_invalid_meta(VentilatorAsset, meta)

def test_meta_validations_for_hl7monitor_asset(self):
valid_meta = {
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
}
hl7monitor_asset = HL7MonitorAsset(valid_meta)
self.assertEqual(hl7monitor_asset.middleware_hostname, "middleware.local")
self.assertEqual(hl7monitor_asset.host, "192.168.0.1")
self.assertEqual(hl7monitor_asset.id, "123")
self.assertTrue(hl7monitor_asset.insecure_connection)

invalid_meta_cases = [
# Missing id
{
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
},
# Missing middleware_hostname
{"id": "123", "local_ip_address": "192.168.0.1"},
# Missing local_ip_address
{"id": "123", "middleware_hostname": "middleware.local"},
# Invalid insecure_connection
{
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": "invalid_value",
},
# Camera access key not required for HL7Monitor, invalid meta
{
"id": "123",
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
},
]
for meta in invalid_meta_cases:
self.validate_invalid_meta(HL7MonitorAsset, meta)

def test_list_assets(self):
response = self.client.get("/api/v1/asset/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
Expand Down
38 changes: 27 additions & 11 deletions care/utils/assetintegration/base.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import json
from typing import TypedDict

import jsonschema
import requests
from django.conf import settings
from jsonschema import ValidationError as JSONValidationError
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.exceptions import APIException, ValidationError

from care.utils.jwks.token_generator import generate_jwt

from .schema import meta_object_schema


class ActionParams(TypedDict, total=False):
type: str
data: dict | None
timeout: int | None


class BaseAssetIntegration:
auth_header_type = "Care_Bearer "

def __init__(self, meta):
try:
meta["_name"] = self._name
jsonschema.validate(instance=meta, schema=meta_object_schema)
except JSONValidationError as e:
error_message = f"Invalid metadata: {e.message}"
raise ValidationError(error_message) from e

self.meta = meta
self.id = self.meta.get("id", "")
self.host = self.meta["local_ip_address"]
self.middleware_hostname = self.meta["middleware_hostname"]
self.insecure_connection = self.meta.get("insecure_connection", False)
self.timeout = settings.MIDDLEWARE_REQUEST_TIMEOUT

def handle_action(self, action):
pass
def handle_action(self, **kwargs):
"""Handle actions using kwargs instead of dict."""

def get_url(self, endpoint):
protocol = "http"
Expand Down Expand Up @@ -48,16 +66,14 @@ def _validate_response(self, response: requests.Response):
{"error": "Invalid Response"}, response.status_code
) from e

def api_post(self, url, data=None):
def api_post(self, url, data=None, timeout=None):
timeout = timeout or self.timeout
return self._validate_response(
requests.post(
url, json=data, headers=self.get_headers(), timeout=self.timeout
)
requests.post(url, json=data, headers=self.get_headers(), timeout=timeout)
)

def api_get(self, url, data=None):
def api_get(self, url, data=None, timeout=None):
timeout = timeout or self.timeout
return self._validate_response(
requests.get(
url, params=data, headers=self.get_headers(), timeout=self.timeout
)
requests.get(url, params=data, headers=self.get_headers(), timeout=timeout)
)
10 changes: 6 additions & 4 deletions care/utils/assetintegration/hl7monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from rest_framework.exceptions import ValidationError

from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration


class HL7MonitorAsset(BaseAssetIntegration):
Expand All @@ -20,12 +20,13 @@ def __init__(self, meta):
{key: f"{key} not found in asset metadata" for key in e.args}
) from e

def handle_action(self, action):
action_type = action["type"]
def handle_action(self, **kwargs: ActionParams):
action_type = kwargs["type"]
timeout = kwargs.get("timeout")

if action_type == self.HL7MonitorActions.GET_VITALS.value:
request_params = {"device_id": self.host}
return self.api_get(self.get_url("vitals"), request_params)
return self.api_get(self.get_url("vitals"), request_params, timeout)

if action_type == self.HL7MonitorActions.GET_STREAM_TOKEN.value:
return self.api_post(
Expand All @@ -34,6 +35,7 @@ def handle_action(self, action):
"asset_id": self.id,
"ip": self.host,
},
timeout,
)

raise ValidationError({"action": "invalid action type"})
20 changes: 11 additions & 9 deletions care/utils/assetintegration/onvif.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from rest_framework.exceptions import ValidationError

from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration


class OnvifAsset(BaseAssetIntegration):
Expand All @@ -27,9 +27,10 @@ def __init__(self, meta):
{key: f"{key} not found in asset metadata" for key in e.args}
) from e

def handle_action(self, action):
action_type = action["type"]
action_data = action.get("data", {})
def handle_action(self, **kwargs: ActionParams):
action_type = kwargs["type"]
action_data = kwargs.get("data", {})
timeout = kwargs.get("timeout")

request_body = {
"hostname": self.host,
Expand All @@ -41,26 +42,27 @@ def handle_action(self, action):
}

if action_type == self.OnvifActions.GET_CAMERA_STATUS.value:
return self.api_get(self.get_url("status"), request_body)
return self.api_get(self.get_url("status"), request_body, timeout)

if action_type == self.OnvifActions.GET_PRESETS.value:
return self.api_get(self.get_url("presets"), request_body)
return self.api_get(self.get_url("presets"), request_body, timeout)

if action_type == self.OnvifActions.GOTO_PRESET.value:
return self.api_post(self.get_url("gotoPreset"), request_body)
return self.api_post(self.get_url("gotoPreset"), request_body, timeout)

if action_type == self.OnvifActions.ABSOLUTE_MOVE.value:
return self.api_post(self.get_url("absoluteMove"), request_body)
return self.api_post(self.get_url("absoluteMove"), request_body, timeout)

if action_type == self.OnvifActions.RELATIVE_MOVE.value:
return self.api_post(self.get_url("relativeMove"), request_body)
return self.api_post(self.get_url("relativeMove"), request_body, timeout)

if action_type == self.OnvifActions.GET_STREAM_TOKEN.value:
return self.api_post(
self.get_url("api/stream/getToken/videoFeed"),
{
"stream_id": self.access_key,
},
timeout,
)

raise ValidationError({"action": "invalid action type"})
34 changes: 34 additions & 0 deletions care/utils/assetintegration/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
meta_object_schema = {
"type": "object",
"properties": {
"id": {"type": "string"},
"local_ip_address": {"type": "string", "format": "ipv4"},
"middleware_hostname": {"type": "string"},
"insecure_connection": {"type": "boolean", "default": False},
"camera_access_key": {
"type": "string",
"pattern": "^[^:]+:[^:]+:[^:]+$", # valid pattern for "abc:def:ghi" , "123:456:789"
},
},
"required": ["local_ip_address", "middleware_hostname"],
"allOf": [
{
"if": {"properties": {"_name": {"const": "onvif"}}},
"then": {
"properties": {"camera_access_key": {"type": "string"}},
"required": [
"camera_access_key"
], # Require camera_access_key for Onvif
},
"else": {
"properties": {"id": {"type": "string"}},
"required": ["id"], # Require id for non-Onvif assets
"not": {
"required": [
"camera_access_key"
] # Make camera_access_key not required for non-Onvif
},
},
}
],
}
Loading

0 comments on commit 5b2b304

Please sign in to comment.