diff --git a/Makefile b/Makefile index 90478cb..9be3297 100644 --- a/Makefile +++ b/Makefile @@ -94,3 +94,14 @@ docs-serve: ## serve documentation .PHONY: ci-test-docs ci-test-docs: docs ## run CI test for documentation + +# --- +# Project +# --- + +.PHONY: server +server: ## run server + poetry run uvicorn workshop_azure_iot.core:app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload diff --git a/iot_hub.env.template b/iot_hub.env.template new file mode 100644 index 0000000..2c2a89c --- /dev/null +++ b/iot_hub.env.template @@ -0,0 +1 @@ +IOT_HUB_DEVICE_CONNECTION_STRING="CHANGE_ME" diff --git a/poetry.lock b/poetry.lock index 959be5e..0e0d311 100644 --- a/poetry.lock +++ b/poetry.lock @@ -47,6 +47,27 @@ files = [ [package.extras] dev = ["azure-functions-durable", "coverage", "flake8 (>=4.0.1,<4.1.0)", "flake8-logging-format", "mypy", "pytest", "pytest-cov", "requests (==2.*)"] +[[package]] +name = "azure-iot-device" +version = "2.14.0" +description = "Microsoft Azure IoT Device Library" +optional = false +python-versions = "<4,>=3.8" +files = [ + {file = "azure_iot_device-2.14.0-py3-none-any.whl", hash = "sha256:db5e401132cdf98d56a758382e3c332384d445b22f574ffb4ea684c7f63345fb"}, + {file = "azure_iot_device-2.14.0.tar.gz", hash = "sha256:b6d48d4932c240025736ace544c4e71bc49a1576ac998ea1de778af82496ffce"}, +] + +[package.dependencies] +deprecation = ">=2.1.0,<3.0.0" +janus = "*" +paho-mqtt = ">=1.6.1,<2.0.0" +PySocks = "*" +requests = ">=2.32.3,<3.0.0" +requests-unixsocket2 = ">=0.4.1" +typing-extensions = "*" +urllib3 = ">=2.2.2,<3.0.0" + [[package]] name = "babel" version = "2.16.0" @@ -299,6 +320,20 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "deprecation" +version = "2.1.0" +description = "A library to handle automated deprecations" +optional = false +python-versions = "*" +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[package.dependencies] +packaging = "*" + [[package]] name = "distlib" version = "0.3.8" @@ -587,6 +622,20 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "janus" +version = "1.0.0" +description = "Mixed sync-async queue to interoperate between asyncio tasks and classic threads" +optional = false +python-versions = ">=3.7" +files = [ + {file = "janus-1.0.0-py3-none-any.whl", hash = "sha256:2596ea5482711c1ee3ef2df6c290aaf370a13c55a007826e8f7c32d696d1d00a"}, + {file = "janus-1.0.0.tar.gz", hash = "sha256:df976f2cdcfb034b147a2d51edfc34ff6bfb12d4e2643d3ad0e10de058cb1612"}, +] + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + [[package]] name = "jinja2" version = "3.1.4" @@ -858,6 +907,19 @@ files = [ dev = ["pytest", "tox"] lint = ["black"] +[[package]] +name = "paho-mqtt" +version = "1.6.1" +description = "MQTT version 5.0/3.1.1 client class" +optional = false +python-versions = "*" +files = [ + {file = "paho-mqtt-1.6.1.tar.gz", hash = "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f"}, +] + +[package.extras] +proxy = ["PySocks"] + [[package]] name = "pathspec" version = "0.12.1" @@ -1094,6 +1156,18 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "8.3.3" @@ -1373,6 +1447,21 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-unixsocket2" +version = "0.4.2" +description = "Use requests to talk HTTP via a UNIX domain socket" +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "requests_unixsocket2-0.4.2-py3-none-any.whl", hash = "sha256:701fcd49d74bc0f759bbe45c4dfda0045fd89652948c2b473b1a312214c3770b"}, + {file = "requests_unixsocket2-0.4.2.tar.gz", hash = "sha256:929c58ecc5981f3d127661ceb9ec8c76e0f08d31c52e44ab1462ac0dcd55b5f5"}, +] + +[package.dependencies] +requests = ">=2.32.3,<3.0.0" +urllib3 = ">=2.2.2,<3.0" + [[package]] name = "rich" version = "13.9.2" @@ -1857,4 +1946,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "229143f678f64febf2916f20c084f571afbb5d258b8d820e4e7af00e691d2e33" +content-hash = "35578d5fe2520ab5010113714345380c08f22fcf4af8b50643559c9361e3a330" diff --git a/pyproject.toml b/pyproject.toml index 833c9b5..251767c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ typer = "^0.12.5" fastapi = {extras = ["standard"], version = "^0.115.2"} azure-functions = "^1.21.3" pydantic-settings = "^2.5.2" +azure-iot-device = "^2.14.0" [tool.poetry.group.dev.dependencies] pre-commit = "^4.0.1" @@ -33,6 +34,8 @@ build-backend = "poetry.core.masonry.api" [tool.ruff] line-length = 120 target-version = "py310" + +[tool.ruff.lint] select = ["E", "F", "I", "UP"] ignore = ["D203"] diff --git a/tests/test_iot.py b/tests/test_iot.py new file mode 100644 index 0000000..a2de572 --- /dev/null +++ b/tests/test_iot.py @@ -0,0 +1,14 @@ +from logging import getLogger + +from tests.utilities import client + +logger = getLogger(__name__) + + +def test_iot(): + path_format = "/iot_hub/{0}" + response = client.get( + url=path_format.format("device_twin"), + ) + assert response.status_code == 200 + logger.info(f"response: {response.json()}") diff --git a/workshop_azure_iot/core.py b/workshop_azure_iot/core.py index 913dd98..a737448 100644 --- a/workshop_azure_iot/core.py +++ b/workshop_azure_iot/core.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from workshop_azure_iot.routers.core import router as core_router +from workshop_azure_iot.routers.iot_hub import router as iot_hub_router app = FastAPI( docs_url="/", @@ -8,6 +9,7 @@ for router in [ core_router, + iot_hub_router, # Add routers here ]: app.include_router(router) diff --git a/workshop_azure_iot/internals/iot_hub.py b/workshop_azure_iot/internals/iot_hub.py new file mode 100644 index 0000000..5be16d0 --- /dev/null +++ b/workshop_azure_iot/internals/iot_hub.py @@ -0,0 +1,20 @@ +from logging import getLogger + +from azure.iot.device.aio import IoTHubDeviceClient + +from workshop_azure_iot.settings.iot_hub import Settings + +logger = getLogger(__name__) + + +class Client: + def __init__(self, settings: Settings) -> None: + self.settings = settings + + async def get_device_twin(self) -> dict: + client = IoTHubDeviceClient.create_from_connection_string(self.settings.iot_hub_device_connection_string) + # FIXME: to make it faster, connection should be established once and reused + await client.connect() + twin = await client.get_twin() + await client.shutdown() + return twin diff --git a/workshop_azure_iot/routers/iot_hub.py b/workshop_azure_iot/routers/iot_hub.py new file mode 100644 index 0000000..4443998 --- /dev/null +++ b/workshop_azure_iot/routers/iot_hub.py @@ -0,0 +1,26 @@ +from logging import getLogger + +from fastapi import APIRouter, status +from fastapi.responses import JSONResponse + +from workshop_azure_iot.internals.iot_hub import Client +from workshop_azure_iot.settings.iot_hub import Settings + +logger = getLogger(__name__) + +client = Client( + settings=Settings(), +) + +router = APIRouter( + prefix="/iot_hub", + tags=["iot_hub"], +) + + +@router.get("/device_twin") +async def get_device_twin(): + return JSONResponse( + status_code=status.HTTP_200_OK, + content=await client.get_device_twin(), + ) diff --git a/workshop_azure_iot/settings/iot_hub.py b/workshop_azure_iot/settings/iot_hub.py new file mode 100644 index 0000000..58f7d3a --- /dev/null +++ b/workshop_azure_iot/settings/iot_hub.py @@ -0,0 +1,6 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + iot_hub_device_connection_string: str + model_config = SettingsConfigDict(env_file="iot_hub.env")