Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/iot app add header and xml support #335

Merged
merged 9 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
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
11 changes: 7 additions & 4 deletions iot/README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
## About
The purpose of the IoT app is to store temporarly data from various IoT-data sources, that do not allow frequent fetching of their data.
The data is stored as it is in JSON to a JSONField and served as JSON. The app uses caching to cache all its queries and serialized data. The Cache is cleared for the source when importing the data source or when a data source is added. The cache is populated if empty when serving data.
The data is stored as it in JSON format to a JSONField and served as JSON. The app uses caching to cache all its queries and serialized data. The Cache is cleared for the source when importing the data or when a data source is added. The cache is populated if empty when serving data.

## Adding IoT-data source from the Admin
* Give a tree letter long source name, this name will be the name for the source. Used for example when requesting the data.
* Give a tree letter long identifier, this will be used to identify the data
to be imported in the Celery task and when requesting data in the API.
* Add the full name of the source
* Add the Url to the JSON data.
* Set is_xml to True if the data is in XML format, the data will be converted to JSON.
* Add the optional headers for the request.

## Setting periodic importing using Celery from the Admin
* Create a periodic task, give a descrpitive name.
* Select *iot.tasks.import_iot_data* as the Task (registered)
* Choose the *Interval Schedule*
* Set the Start DateTime
* Add the source name as *Positional Arguments*, e.g. ["R24"] would import the source_name R24.
* Add the identifier as *Positional Arguments*, e.g. ["R24"] would import the identifier R24.

## Manual import
To manually import source:
`./manage.py import_iot_data source_name`
`./manage.py import_iot_data identifier`
Or by running the perioc task from the admin.

## Retriving data
Expand Down
24 changes: 17 additions & 7 deletions iot/management/commands/import_iot_data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
import logging
from xml.parsers.expat import ExpatError

import requests
import xmltodict
from django.core.cache import cache
from django.core.management.base import BaseCommand

Expand All @@ -13,17 +15,25 @@

def save_data_to_db(source):
IoTData.objects.filter(data_source=source).delete()

try:
response = requests.get(source.url)
response = requests.get(source.url, headers=source.headers)
except requests.exceptions.ConnectionError:
logger.error(f"Could not fetch data from: {source.url}")
return
try:
json_data = response.json()
except json.decoder.JSONDecodeError:
logger.error(f"Could not decode data to json from: {source.url}")
return
if source.is_xml:
try:
json_data = xmltodict.parse(response.text)
except ExpatError as err:
logger.error(
f"Could not parse XML data from the give url {source.url}. {err}"
)
return
else:
try:
json_data = response.json()
except json.decoder.JSONDecodeError as err:
logger.error(f"Could not decode data to JSON from: {source.url}. {err}")
return

IoTData.objects.create(data_source=source, data=json_data)

Expand Down
20 changes: 20 additions & 0 deletions iot/migrations/0003_iotdatasource_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.1.13 on 2024-03-13 06:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("iot", "0002_add_ordering"),
]

operations = [
migrations.AddField(
model_name="iotdatasource",
name="headers",
field=models.JSONField(
blank=True, null=True, verbose_name="request headers"
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 4.1.13 on 2024-03-13 08:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("iot", "0003_iotdatasource_headers"),
]

operations = [
migrations.AddField(
model_name="iotdatasource",
name="is_xml",
field=models.BooleanField(
default=False,
verbose_name="If True, XML data will be converted to JSON.",
),
),
migrations.AlterField(
model_name="iotdatasource",
name="headers",
field=models.JSONField(
blank=True,
null=True,
verbose_name='request headers in JSON format, e.g., {"key1": "value1", "key2": "value2"}',
),
),
migrations.AlterField(
model_name="iotdatasource",
name="source_name",
field=models.CharField(
max_length=3,
unique=True,
verbose_name="Three letter long identifier for the source. Set the identifier as an argument to the Celery task that fetches the data.",
),
),
]
41 changes: 32 additions & 9 deletions iot/models.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
import json
from xml.parsers.expat import ExpatError

import requests
import xmltodict
from django.core.exceptions import ValidationError
from django.db import models


class IoTDataSource(models.Model):
source_name = models.CharField(
max_length=3, unique=True, verbose_name="Three letter long name for the source"
max_length=3,
unique=True,
verbose_name="Three letter long identifier for the source. "
"Set the identifier as an argument to the Celery task that fetches the data.",
)
source_full_name = models.CharField(max_length=64, null=True)
is_xml = models.BooleanField(
default=False, verbose_name="If True, XML data will be converted to JSON."
)
url = models.URLField()
headers = models.JSONField(
null=True,
blank=True,
verbose_name='request headers in JSON format, e.g., {"key1": "value1", "key2": "value2"}',
)

def __str__(self):
return self.source_name

def clean(self):
# Test if url exists
try:
response = requests.get(self.url)
response = requests.get(self.url, headers=self.headers)

Check warning on line 34 in iot/models.py

View check run for this annotation

Codecov / codecov/patch

iot/models.py#L34

Added line #L34 was not covered by tests
except requests.exceptions.ConnectionError:
raise ValidationError(f"The given url {self.url} does not exist.")
# Test if valid json
try:
response.json()
except json.decoder.JSONDecodeError:
raise ValidationError(
f"Could not parse the JSON data for the given url {self.url}"
)

# Test if XML data can be parsed into JSON
if self.is_xml:
try:
xmltodict.parse(response.text)
except ExpatError as err:
raise ExpatError(

Check warning on line 43 in iot/models.py

View check run for this annotation

Codecov / codecov/patch

iot/models.py#L40-L43

Added lines #L40 - L43 were not covered by tests
f"Could not parse XML data from the give url {self.url}. {err}"
)
else:
# Test if valid JSON
try:
response.json()
except json.decoder.JSONDecodeError as err:
raise ValidationError(

Check warning on line 51 in iot/models.py

View check run for this annotation

Codecov / codecov/patch

iot/models.py#L48-L51

Added lines #L48 - L51 were not covered by tests
f"Could not parse the JSON data from the given url {self.url}. {err}"
)


class IoTData(models.Model):
Expand Down
Loading