diff --git a/.gitignore b/.gitignore index 0aaa3742..0bc025a0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ __pycache__ !.github/issue-branch.yml *.yaml +!.pre-commit-config.yaml # pip install # ##################### diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..eba7173f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + # Using black mirror since it's 2x faster https://black.readthedocs.io/en/stable/integrations/source_version_control.html + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.10.0 + hooks: + - id: black + # Specifying the latest version of Python supported by Filip + language_version: python diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bdc2aa04..82115e2c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,29 @@ We use PEP8 as a coding style guide. Some IDEs (like PyCharm) automatically show For committing style guide please use Conventional Commits 1.0.0. For more details how to structure your commits please visit this [page](https://www.conventionalcommits.org/en/v1.0.0/). +### Pre-commit Hooks + +In order to make the development easy and uniform, use of pre-commit is highly recommended. The pre-commit hooks run before every commit to ensure the code is compliant with the project style. + +Check if pre-commit is installed: +```bash +pre-commit --version +``` +Install pre-commit via pip if it's not already installed. +```bash +pip install pre-commit~=4.0.1 +``` +Install the git hook scripts: +```bash +pre-commit install +``` +This will run pre-commit automatically on every git commit. Checkout [pre-commit-config.yaml](.pre-commit-config.yaml) file to find out which hooks are currently configured. + +> **Note:** Currently we are using the pre-commit to perform black formatter. You can perform a formatting to all files by running the following command: +> ```bash +> pre-commit run black --all-files +> ``` + ## Documentation All created or modified functions should be documented properly. diff --git a/README.md b/README.md index 239ec78e..d42f76c8 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,11 @@ If you want to benefit from the latest changes, use the following command pip install -U git+git://github.com/RWTH-EBC/filip ``` -> **Note**: For local development, you can install the library in editable mode with the following command: -> ```` -> pip install -e . +> **Note**: For development, you should install FiLiP in editable mode with the following command: +> ````bash +> pip install -e .[development] > ```` +> The `development` option will install extra libraries required for contribution. Please check the [CONTRIBUTING.md](CONTRIBUTING.md) for more information. #### Install semantics module (optional) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2af0cd72..42cd4c80 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,31 +11,38 @@ import sys import datetime from pathlib import Path + # pylint: disable-all -sys.path.insert(0, os.path.abspath('../..')) +sys.path.insert(0, os.path.abspath("../..")) sys.setrecursionlimit(1500) # -- Project information ----------------------------------------------------- -project = 'FiLiP' -copyright = f'2021-{datetime.datetime.now().year}, RWTH Aachen University, ' \ - f'E.ON Energy Research Center, ' \ - f'Institute for Energy Efficient Buildings and Indoor Climate' -author = 'E.ON ERC - EBC' +project = "FiLiP" +copyright = ( + f"2021-{datetime.datetime.now().year}, RWTH Aachen University, " + f"E.ON Energy Research Center, " + f"Institute for Energy Efficient Buildings and Indoor Climate" +) +author = "E.ON ERC - EBC" # The full version, including alpha/beta/rc tags # Get the version from FiliP/filip/__init__.py: with open(Path(__file__).parents[2].joinpath("filip", "__init__.py"), "r") as file: for line in file.readlines(): if line.startswith("__version__"): - release = line.replace("__version__", "").split("=")[1].strip().replace( - "'", "").replace( - '"', '') + release = ( + line.replace("__version__", "") + .split("=")[1] + .strip() + .replace("'", "") + .replace('"', "") + ) release = release # The short X.Y version. -version = '.'.join(release.split('.')[0:2]) +version = ".".join(release.split(".")[0:2]) # -- General configuration ------------------------------------------------ @@ -47,28 +54,29 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.inheritance_diagram', - 'sphinx.ext.intersphinx', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx.ext.coverage', - 'm2r2', # Enable .md files - 'sphinx.ext.napoleon', # Enable google docstrings - 'sphinxcontrib.autodoc_pydantic' # add support for pydantic - ] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.intersphinx", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.coverage", + "m2r2", # Enable .md files + "sphinx.ext.napoleon", # Enable google docstrings + "sphinxcontrib.autodoc_pydantic", # add support for pydantic +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] # The master toctree document. -master_doc = 'contents' +master_doc = "contents" # The language for content autogenerated by Sphinx. Refer to documentation @@ -84,7 +92,7 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'friendly' +pygments_style = "friendly" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -104,7 +112,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -162,10 +170,7 @@ # This is required for the material theme # Refs: https://bashtage.github.io/sphinx-material/index.html html_sidebars = { - "**": ["logo-text.html", - "globaltoc.html", - "localtoc.html", - "searchbox.html"] + "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"] } # This is required for the alabaster theme @@ -181,7 +186,7 @@ # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'FiLiPdoc' +htmlhelp_basename = "FiLiPdoc" # -- Options for LaTeX output --------------------------------------------- @@ -190,15 +195,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -208,8 +210,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'FiLiP.tex', 'FiLiP Documentation', - 'EON EBC', 'manual'), + (master_doc, "FiLiP.tex", "FiLiP Documentation", "EON EBC", "manual"), ] @@ -217,10 +218,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'FiLiP', 'FiLiP Documentation', - [author], 1) -] +man_pages = [(master_doc, "FiLiP", "FiLiP Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -229,9 +227,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'FiLiP', 'FiLiP Documentation', - author, 'FiLiP', 'FIWARE Library for Python', - 'Miscellaneous'), + ( + master_doc, + "FiLiP", + "FiLiP Documentation", + author, + "FiLiP", + "FIWARE Library for Python", + "Miscellaneous", + ), ] # -- Options for Epub output ---------------------------------------------- @@ -252,8 +256,8 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {"https://docs.python.org/": None} diff --git a/examples/basics/e01_http_clients.py b/examples/basics/e01_http_clients.py index a5134930..8ed7b3dd 100644 --- a/examples/basics/e01_http_clients.py +++ b/examples/basics/e01_http_clients.py @@ -6,12 +6,10 @@ # ## Import packages import requests -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient, QuantumLeapClient from filip.models.base import FiwareHeader from filip.config import settings + # ## Parameters # # Note: This example also reads parameters from the '.env.filip'-file @@ -25,12 +23,12 @@ # # Here you can also change the used Fiware service # FIWARE-Service -service = 'filip' +service = "filip" # FIWARE-Servicepath -service_path = '/example' +service_path = "/example" -if __name__ == '__main__': +if __name__ == "__main__": # # 1 FiwareHeader # @@ -41,8 +39,7 @@ # In short, a fiware header specifies a location in Fiware where the # created entities will be saved and requests are executed. # It can be thought of as a separate subdirectory where you work in. - fiware_header = FiwareHeader(service='filip', - service_path='/example') + fiware_header = FiwareHeader(service="filip", service_path="/example") # # 2 Client modes # You can run the clients in different modes: @@ -50,8 +47,7 @@ # ## 2.1 Run it as a pure python object. # # This will open and close a connection each time you use a function. - cb_client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) + cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) print(f"OCB Version: {cb_client.get_version()}") # ## 2.2 Run the client via python's context protocol. @@ -103,8 +99,7 @@ # additional keyword arguments a requests.request would also take, # e.g. headers, params etc. - cb_client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) + cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # # 5 Combined Client # diff --git a/examples/basics/e02_baerer_token.py b/examples/basics/e02_baerer_token.py index b0d0d574..dbcee723 100644 --- a/examples/basics/e02_baerer_token.py +++ b/examples/basics/e02_baerer_token.py @@ -12,20 +12,21 @@ # Host address of Context Broker CB_URL = "https://localhost:1026" # FIWARE-Service -fiware_service = 'filip' +fiware_service = "filip" # FIWARE-Servicepath -fiware_service_path = '/example' +fiware_service_path = "/example" # FIWARE-Bearer token # TODO it has to be replaced with the token of your protected endpoint -fiware_baerer_token = 'BAERER_TOKEN' +fiware_baerer_token = "BAERER_TOKEN" -if __name__ == '__main__': - fiware_header = FiwareHeaderSecure(service=fiware_service, - service_path=fiware_service_path, - authorization=f"""Bearer { - fiware_baerer_token}""") - cb_client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) +if __name__ == "__main__": + fiware_header = FiwareHeaderSecure( + service=fiware_service, + service_path=fiware_service_path, + authorization=f"""Bearer { + fiware_baerer_token}""", + ) + cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # query entities from protected orion endpoint entity_list = cb_client.get_entity_list() print(entity_list) diff --git a/examples/basics/e11_logging.py b/examples/basics/e11_logging.py index 2cf36c20..badb36e0 100644 --- a/examples/basics/e11_logging.py +++ b/examples/basics/e11_logging.py @@ -8,8 +8,10 @@ # import python's built-in logging implementation import logging + # import an api client from filip as example from filip.clients.ngsi_v2 import ContextBrokerClient + # setting up the basic configuration of the logging system. Please check the # official documentation and the functions docstrings for more details. # Handling for 'handlers' in the logging system is not trivial. @@ -20,9 +22,9 @@ # In this example we will simply change the log level and the log format. logging.basicConfig( - level='DEBUG', - format='%(asctime)s %(name)s %(levelname)s: %(message)s') + level="DEBUG", format="%(asctime)s %(name)s %(levelname)s: %(message)s" +) # You need to change the output -ocb = ContextBrokerClient(url='http://') +ocb = ContextBrokerClient(url="http://") ocb.get_version() diff --git a/examples/basics/e12_settings.py b/examples/basics/e12_settings.py index 7aa796cf..865f794e 100644 --- a/examples/basics/e12_settings.py +++ b/examples/basics/e12_settings.py @@ -10,10 +10,11 @@ # Note: Although URLs are also guessed, the safest option is to set the service url # directly """ + import os -if __name__ == '__main__': +if __name__ == "__main__": # # 1 Example using environment variables @@ -22,4 +23,5 @@ os.environ["IOTA_URL"] = "http://localhost:4041" from filip import settings - print(settings.model_dump_json(indent=2)) \ No newline at end of file + + print(settings.model_dump_json(indent=2)) diff --git a/examples/ngsi_v2/e01_ngsi_v2_context_basics.py b/examples/ngsi_v2/e01_ngsi_v2_context_basics.py index 612fdcd4..1bdcc896 100644 --- a/examples/ngsi_v2/e01_ngsi_v2_context_basics.py +++ b/examples/ngsi_v2/e01_ngsi_v2_context_basics.py @@ -7,8 +7,11 @@ from filip.clients.ngsi_v2 import ContextBrokerClient from filip.models.base import FiwareHeader, DataType -from filip.models.ngsi_v2.context import ContextEntity, ContextAttribute, \ - NamedContextAttribute +from filip.models.ngsi_v2.context import ( + ContextEntity, + ContextAttribute, + NamedContextAttribute, +) from filip.utils.simple_ql import QueryString from filip.config import settings @@ -22,15 +25,16 @@ # You can also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) if __name__ == "__main__": @@ -38,14 +42,14 @@ # # 1 Setup Client # # create the client, for more details view the example: e01_http_clients.py - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) - cb_client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) + cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # View version for key, value in cb_client.get_version().items(): - logger.info(f"Context broker version: {value['version']} at url: " - f"{cb_client.base_url}") + logger.info( + f"Context broker version: {value['version']} at url: " + f"{cb_client.base_url}" + ) # # 2 Create Entities # @@ -55,54 +59,61 @@ # # ### 2.1.1 Passing a dict: # - room1_dictionary = {"id": "urn:ngsi-ld:Room:001", - "type": "Room", - "temperature": {"value": 11, - "type": "Float"}, - "pressure": {"value": 111, - "type": "Integer"} - } + room1_dictionary = { + "id": "urn:ngsi-ld:Room:001", + "type": "Room", + "temperature": {"value": 11, "type": "Float"}, + "pressure": {"value": 111, "type": "Integer"}, + } room1_entity = ContextEntity(**room1_dictionary) # ### 2.1.2 Using the constructor and interfaces # room2_entity = ContextEntity(id="urn:ngsi-ld:Room:002", type="Room") - temp_attr = NamedContextAttribute(name="temperature", value=22, - type=DataType.FLOAT) - pressure_attr = NamedContextAttribute(name="pressure", value=222, - type="Integer") + temp_attr = NamedContextAttribute(name="temperature", value=22, type=DataType.FLOAT) + pressure_attr = NamedContextAttribute(name="pressure", value=222, type="Integer") room2_entity.add_attributes([temp_attr, pressure_attr]) # ## 2.2 Post Entities # - logger.info(f'Entity list before posting to CB: {cb_client.get_entity_list()}') + logger.info(f"Entity list before posting to CB: {cb_client.get_entity_list()}") cb_client.post_entity(entity=room1_entity) cb_client.post_entity(entity=room2_entity) # # 3 Access entities in Fiware # # Get all entities from context broker - logger.info(f'Entity list after posting to CB: {cb_client.get_entity_list()}') + logger.info(f"Entity list after posting to CB: {cb_client.get_entity_list()}") # Get entities by id - logger.info(f'Entities with ID "urn:ngsi-ld:Room:001": ' - f'{cb_client.get_entity_list(entity_ids=["urn:ngsi-ld:Room:001"])}') + logger.info( + f'Entities with ID "urn:ngsi-ld:Room:001": ' + f'{cb_client.get_entity_list(entity_ids=["urn:ngsi-ld:Room:001"])}' + ) # Get entities by type - logger.info(f'Entities by type "Room": {cb_client.get_entity_list(entity_types=["Room"])}') + logger.info( + f'Entities by type "Room": {cb_client.get_entity_list(entity_types=["Room"])}' + ) # Get entities by id pattern # The regular expression filters the rooms that have the id number 2 through 5 # with the prefix 'urn:ngsi-ld:Room:' - logger.info(f'Entities with id pattern "^urn:ngsi-ld:Room:00[2-5]": ' - f'{cb_client.get_entity_list(id_pattern="^urn:ngsi-ld:Room:00[2-5]")}') + logger.info( + f'Entities with id pattern "^urn:ngsi-ld:Room:00[2-5]": ' + f'{cb_client.get_entity_list(id_pattern="^urn:ngsi-ld:Room:00[2-5]")}' + ) # Get entities by query expression - query = QueryString(qs=[('temperature', '>=', 22)]) - logger.info(f'Entities with temperature >= 22: {cb_client.get_entity_list(q=query)}') + query = QueryString(qs=[("temperature", ">=", 22)]) + logger.info( + f"Entities with temperature >= 22: {cb_client.get_entity_list(q=query)}" + ) # Get attributes of entities - logger.info(f'Attributes of entities: {cb_client.get_entity_attributes(entity_id="urn:ngsi-ld:Room:001")}') + logger.info( + f'Attributes of entities: {cb_client.get_entity_attributes(entity_id="urn:ngsi-ld:Room:001")}' + ) # Trying to access non-existing ids or attributes will always throw # a request error @@ -112,18 +123,19 @@ # ## 4.1 Updating entity = room2_entity - entity.add_attributes({'Space': ContextAttribute(type='Number', - value=111)}) + entity.add_attributes({"Space": ContextAttribute(type="Number", value=111)}) # ### 4.1.1 Updating directly # # Using the Filip interface, we can update different properties of our - # entity directly in the live version in FIWARE. A few examples of what + # entity directly in the live version in FIWARE. A few examples of what # is possible are listed here: # Updating value of an attribute of an entity - logger.info(cb_client.update_attribute_value(entity_id=room1_entity.id, - attr_name="temperature", - value=12)) + logger.info( + cb_client.update_attribute_value( + entity_id=room1_entity.id, attr_name="temperature", value=12 + ) + ) # Deleting attributes # logger.info(cb_client.delete_entity_attribute(entity_id=room1_entity.id, # attr_name="temperature")) @@ -149,7 +161,5 @@ # ## 4.2 Deleting # # To delete an entry in Fiware, we can call: - cb_client.delete_entity(entity_id=room2_entity.id, - entity_type=room2_entity.type) - cb_client.delete_entity(entity_id=room1_entity.id, - entity_type=room1_entity.type) + cb_client.delete_entity(entity_id=room2_entity.id, entity_type=room2_entity.type) + cb_client.delete_entity(entity_id=room1_entity.id, entity_type=room1_entity.type) diff --git a/examples/ngsi_v2/e02_ngsi_v2_context_relationships.py b/examples/ngsi_v2/e02_ngsi_v2_context_relationships.py index f40b78a4..835443b3 100644 --- a/examples/ngsi_v2/e02_ngsi_v2_context_relationships.py +++ b/examples/ngsi_v2/e02_ngsi_v2_context_relationships.py @@ -1,6 +1,7 @@ """ # Examples for relationships in FIWARE ContextBroker """ + # ## Import packages import logging from filip.config import settings @@ -19,60 +20,45 @@ # You can also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Servicepath -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) if __name__ == "__main__": # # 1 Setup client and models # - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # ## 1.1 Store entities # - with ContextBrokerClient(fiware_header=fiware_header, - url=CB_URL) as cb_client: + with ContextBrokerClient(fiware_header=fiware_header, url=CB_URL) as cb_client: # make sure that the server is clean cb_client.delete_entities(cb_client.get_entity_list()) - store_dict = [{"type": "Store", - "id": "urn:ngsi-ld:Store:001", - "address": { - "type": "Text", - "value": "Bornholmer Straße 65" - }, - "location": { - "type": "Text", - "value": "[13.3986, 52.5547]" - }, - "name": { - "type": "Text", - "value": "Bösebrücke Einkauf" - }}, - { - "type": "Store", - "id": "urn:ngsi-ld:Store:002", - "address": { - "type": "Text", - "value": "Friedrichstraße 44" - }, - "location": { - "type": "Text", - "value": "[13.3903, 52.5075]" - }, - "name": { - "type": "Text", - "value": "Checkpoint Markt" - } - }] + store_dict = [ + { + "type": "Store", + "id": "urn:ngsi-ld:Store:001", + "address": {"type": "Text", "value": "Bornholmer Straße 65"}, + "location": {"type": "Text", "value": "[13.3986, 52.5547]"}, + "name": {"type": "Text", "value": "Bösebrücke Einkauf"}, + }, + { + "type": "Store", + "id": "urn:ngsi-ld:Store:002", + "address": {"type": "Text", "value": "Friedrichstraße 44"}, + "location": {"type": "Text", "value": "[13.3903, 52.5075]"}, + "name": {"type": "Text", "value": "Checkpoint Markt"}, + }, + ] store_entities = [ContextEntity(**store) for store in store_dict] for entity in store_entities: cb_client.post_entity(entity) @@ -82,53 +68,33 @@ with ContextBrokerClient(fiware_header=fiware_header) as cb_client: product_dict = [ { - "id": "urn:ngsi-ld:Product:001", "type": "Product", - "name": { - "type": "Text", "value": "Beer" - }, - "size": { - "type": "Text", "value": "S" - }, - "price": { - "type": "Integer", "value": 99 - } + "id": "urn:ngsi-ld:Product:001", + "type": "Product", + "name": {"type": "Text", "value": "Beer"}, + "size": {"type": "Text", "value": "S"}, + "price": {"type": "Integer", "value": 99}, }, { - "id": "urn:ngsi-ld:Product:002", "type": "Product", - "name": { - "type": "Text", "value": "Red Wine" - }, - "size": { - "type": "Text", "value": "M" - }, - "price": { - "type": "Integer", "value": 1099 - } + "id": "urn:ngsi-ld:Product:002", + "type": "Product", + "name": {"type": "Text", "value": "Red Wine"}, + "size": {"type": "Text", "value": "M"}, + "price": {"type": "Integer", "value": 1099}, }, { - "id": "urn:ngsi-ld:Product:003", "type": "Product", - "name": { - "type": "Text", "value": "White Wine" - }, - "size": { - "type": "Text", "value": "M" - }, - "price": { - "type": "Integer", "value": 1499 - } + "id": "urn:ngsi-ld:Product:003", + "type": "Product", + "name": {"type": "Text", "value": "White Wine"}, + "size": {"type": "Text", "value": "M"}, + "price": {"type": "Integer", "value": 1499}, }, { - "id": "urn:ngsi-ld:Product:004", "type": "Product", - "name": { - "type": "Text", "value": "Vodka" - }, - "size": { - "type": "Text", "value": "XL" - }, - "price": { - "type": "Integer", "value": 5000 - } - } + "id": "urn:ngsi-ld:Product:004", + "type": "Product", + "name": {"type": "Text", "value": "Vodka"}, + "size": {"type": "Text", "value": "XL"}, + "price": {"type": "Integer", "value": 5000}, + }, ] product_entities = [] for product_entity in product_dict: @@ -139,18 +105,12 @@ # with ContextBrokerClient(fiware_header=fiware_header) as cb_client: inventory_dict = { - "id": "urn:ngsi-ld:InventoryItem:001", "type": "InventoryItem", - "refStore": { - "type": "Relationship", - "value": "urn:ngsi-ld:Store:001" - }, - "refProduct": { - "type": "Relationship", - "value": "urn:ngsi-ld:Product:001" - }, - "stockCount": { - "type": "Integer", "value": 10000 - }} + "id": "urn:ngsi-ld:InventoryItem:001", + "type": "InventoryItem", + "refStore": {"type": "Relationship", "value": "urn:ngsi-ld:Store:001"}, + "refProduct": {"type": "Relationship", "value": "urn:ngsi-ld:Product:001"}, + "stockCount": {"type": "Integer", "value": 10000}, + } inventory_entity = ContextEntity(**inventory_dict) cb_client.post_entity(inventory_entity) @@ -162,15 +122,15 @@ # ## 2.2 Get entities # with ContextBrokerClient(fiware_header=fiware_header) as cb_client: - # It should return the inventory item according to the relationship - query = QueryString(qs=[('refProduct', '==', 'urn:ngsi-ld:Product:001')]) + # It should return the inventory item according to the relationship + query = QueryString(qs=[("refProduct", "==", "urn:ngsi-ld:Product:001")]) logger.info(cb_client.get_entity_list(q=query)) - - query = QueryString(qs=[('refStore', '==', 'urn:ngsi-ld:Store:001')]) + + query = QueryString(qs=[("refStore", "==", "urn:ngsi-ld:Store:001")]) logger.info(cb_client.get_entity_list(q=query)) # It should not return the inventory item according to the relationship - query = QueryString(qs=[('refStore', '==', 'urn:ngsi-ld:Store:002')]) + query = QueryString(qs=[("refStore", "==", "urn:ngsi-ld:Store:002")]) logger.info(cb_client.get_entity_list(q=query)) # # 3 Delete test entities @@ -178,5 +138,6 @@ with ContextBrokerClient(fiware_header=fiware_header) as cb_client: cb_client.delete_entities(store_entities) cb_client.delete_entities(product_entities) - cb_client.delete_entity(entity_id=inventory_entity.id, - entity_type=inventory_entity.type) + cb_client.delete_entity( + entity_id=inventory_entity.id, entity_type=inventory_entity.type + ) diff --git a/examples/ngsi_v2/e03_ngsi_v2_context_subscriptions_http.py b/examples/ngsi_v2/e03_ngsi_v2_context_subscriptions_http.py index b29bd02d..2c938b41 100644 --- a/examples/ngsi_v2/e03_ngsi_v2_context_subscriptions_http.py +++ b/examples/ngsi_v2/e03_ngsi_v2_context_subscriptions_http.py @@ -4,6 +4,7 @@ # create new subscriptions following the API Walkthrough example: # https://fiware-orion.readthedocs.io/en/master/user/walkthrough_apiv2.html#subscriptions """ + # ## Import packages import logging import datetime @@ -23,23 +24,24 @@ # You can also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Servicepath -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # Web server URL for receiving notifications # It has to be accessible from the context broker! SERVER_URL = "http://example.com" # You can replace SERVER_URL with the URL of the web server, where you'd like to receive notifications -# e.g. "http://host.docker.internal:8080/notify/", or if you're not sure how to set up the -# server, create a dummy version via +# e.g. "http://host.docker.internal:8080/notify/", or if you're not sure how to set up the +# server, create a dummy version via # https://fiware-orion.rtfd.io/en/master/user/walkthrough_apiv2.html#starting-accumulator-server # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) @@ -47,10 +49,8 @@ # # 1 Client setup # # Create the context broker client, for more details view the example: e01_http_clients.py - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) - cb_client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) + cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) entities = cb_client.get_entity_list() logger.info(entities) @@ -63,30 +63,14 @@ interesting_entity_id = "urn:ngsi-ld:Room:001" sub_example = { "description": "Subscription to receive HTTP-Notifications about " - + interesting_entity_id, + + interesting_entity_id, "subject": { - "entities": [ - { - "id": "urn:ngsi-ld:Room:001", - "type": "Room" - } - ], - "condition": { - "attrs": [ - "temperature" - ] - } - }, - "notification": { - "http": { - "url": SERVER_URL - }, - "attrs": [ - "temperature" - ] + "entities": [{"id": "urn:ngsi-ld:Room:001", "type": "Room"}], + "condition": {"attrs": ["temperature"]}, }, + "notification": {"http": {"url": SERVER_URL}, "attrs": ["temperature"]}, "expires": datetime.datetime.now() + datetime.timedelta(minutes=15), - "throttling": 0 + "throttling": 0, } sub = Subscription(**sub_example) # Posting an example subscription for Room1 @@ -103,8 +87,8 @@ sub_to_update = cb_client.get_subscription(sub_id) # Update expiration time of the example subscription sub_to_update = sub_to_update.model_copy( - update={'expires': datetime.datetime.now() + - datetime.timedelta(minutes=15)}) + update={"expires": datetime.datetime.now() + datetime.timedelta(minutes=15)} + ) cb_client.update_subscription(sub_to_update) updated_subscription = cb_client.get_subscription(sub_id) logger.info(updated_subscription) diff --git a/examples/ngsi_v2/e04_ngsi_v2_context_subscriptions_mqtt.py b/examples/ngsi_v2/e04_ngsi_v2_context_subscriptions_mqtt.py index a1fbf60f..dcc53357 100644 --- a/examples/ngsi_v2/e04_ngsi_v2_context_subscriptions_mqtt.py +++ b/examples/ngsi_v2/e04_ngsi_v2_context_subscriptions_mqtt.py @@ -4,6 +4,7 @@ # create new subscription following the API Walkthrough example: # https://fiware-orion.readthedocs.io/en/master/user/walkthrough_apiv2.html#subscriptions """ + # ## Import packages import logging import datetime @@ -25,22 +26,23 @@ # You can also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # MQTT URL for eclipse mosquitto MQTT_BROKER_URL_INTERNAL = "mqtt://mosquitto:1883" MQTT_BROKER_URL_EXPOSED = str(settings.MQTT_BROKER_URL) # MQTT topic that the subscription will send to -mqtt_topic = ''.join([SERVICE, SERVICE_PATH]) +mqtt_topic = "".join([SERVICE, SERVICE_PATH]) # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) @@ -48,18 +50,15 @@ # # 1 Client setup # # create the client, for more details view the example: e01_http_clients.py - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) - cb_client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) - - room_001 = {"id": "urn:ngsi-ld:Room:001", - "type": "Room", - "temperature": {"value": 11, - "type": "Float"}, - "pressure": {"value": 111, - "type": "Integer"} - } + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) + cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) + + room_001 = { + "id": "urn:ngsi-ld:Room:001", + "type": "Room", + "temperature": {"value": 11, "type": "Float"}, + "pressure": {"value": 111, "type": "Integer"}, + } room_entity = ContextEntity(**room_001) cb_client.post_entity(entity=room_entity, update=True) @@ -76,31 +75,17 @@ # check the Subscription model or the official tutorials. sub_example = { "description": "Subscription to receive MQTT-Notifications about " - "urn:ngsi-ld:Room:001", + "urn:ngsi-ld:Room:001", "subject": { - "entities": [ - { - "id": "urn:ngsi-ld:Room:001", - "type": "Room" - } - ], - "condition": { - "attrs": [ - "temperature" - ] - } + "entities": [{"id": "urn:ngsi-ld:Room:001", "type": "Room"}], + "condition": {"attrs": ["temperature"]}, }, "notification": { - "mqtt": { - "url": MQTT_BROKER_URL_INTERNAL, - "topic": mqtt_topic - }, - "attrs": [ - "temperature" - ] + "mqtt": {"url": MQTT_BROKER_URL_INTERNAL, "topic": mqtt_topic}, + "attrs": ["temperature"], }, "expires": datetime.datetime.now() + datetime.timedelta(minutes=15), - "throttling": 0 + "throttling": 0, } # Generate Subscription object for validation sub = Subscription(**sub_example) @@ -115,33 +100,37 @@ # different events. Do not change their signature! def on_connect(client, userdata, flags, reason_code, properties=None): if reason_code != 0: - logger.error(f"MQTT Client failed to connect with the error code: " - f"{reason_code}") + logger.error( + f"MQTT Client failed to connect with the error code: " f"{reason_code}" + ) raise ConnectionError else: - logger.info(f"MQTT Client successfully connected with the reason code: {reason_code}") + logger.info( + f"MQTT Client successfully connected with the reason code: {reason_code}" + ) client.subscribe(mqtt_topic) def on_subscribe(client, userdata, mid, granted_qos, properties=None): logger.info(f"MQTT Client successfully subscribed: {granted_qos[0]}") - def on_message(client, userdata, msg): message = Message.model_validate_json(msg.payload) - logger.info("MQTT Client received this message:\n" + message.model_dump_json(indent=2)) - + logger.info( + "MQTT Client received this message:\n" + message.model_dump_json(indent=2) + ) def on_disconnect(client, userdata, flags, reasonCode, properties=None): - logger.info("MQTT Client disconnected with reasonCode " - + str(reasonCode)) + logger.info("MQTT Client disconnected with reasonCode " + str(reasonCode)) # MQTT client import paho.mqtt.client as mqtt - mqtt_client = mqtt.Client(userdata=None, - protocol=mqtt.MQTTv5, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - transport="tcp") + mqtt_client = mqtt.Client( + userdata=None, + protocol=mqtt.MQTTv5, + callback_api_version=mqtt.CallbackAPIVersion.VERSION2, + transport="tcp", + ) # add callbacks to the mqtt-client mqtt_client.on_connect = on_connect mqtt_client.on_subscribe = on_subscribe @@ -150,13 +139,15 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): # connect to the mqtt-broker to receive the notifications message mqtt_url = urlparse(MQTT_BROKER_URL_EXPOSED) - mqtt_client.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqtt_client.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # # 4 send new value via MQTT # @@ -164,21 +155,24 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): mqtt_client.loop_start() new_value = 55 - cb_client.update_attribute_value(entity_id='urn:ngsi-ld:Room:001', - attr_name='temperature', - value=new_value, - entity_type='Room') - cb_client.update_attribute_value(entity_id='urn:ngsi-ld:Room:001', - attr_name='pressure', - value=new_value, - entity_type='Room') + cb_client.update_attribute_value( + entity_id="urn:ngsi-ld:Room:001", + attr_name="temperature", + value=new_value, + entity_type="Room", + ) + cb_client.update_attribute_value( + entity_id="urn:ngsi-ld:Room:001", + attr_name="pressure", + value=new_value, + entity_type="Room", + ) time.sleep(1) # # 5 Deleting the example entity and the subscription # cb_client.delete_subscription(sub_id) - cb_client.delete_entity(entity_id=room_entity.id, - entity_type=room_entity.type) + cb_client.delete_entity(entity_id=room_entity.id, entity_type=room_entity.type) # # 6 Clean up (Optional) # # Close clients diff --git a/examples/ngsi_v2/e05_ngsi_v2_context_registrations.py b/examples/ngsi_v2/e05_ngsi_v2_context_registrations.py index db2caa72..b721680e 100644 --- a/examples/ngsi_v2/e05_ngsi_v2_context_registrations.py +++ b/examples/ngsi_v2/e05_ngsi_v2_context_registrations.py @@ -5,14 +5,13 @@ # 1. Create a context provider # 2. Create a context entity retrieving information from a context provider """ + # ## Import packages import logging from filip.clients.ngsi_v2.cb import ContextBrokerClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.base import NamedMetadata -from filip.models.ngsi_v2.context import \ - ContextAttribute, \ - ContextEntity +from filip.models.ngsi_v2.context import ContextAttribute, ContextEntity from filip.models.ngsi_v2.units import Unit from filip.models.ngsi_v2.registrations import Http, Provider, Registration from filip.config import settings @@ -27,18 +26,19 @@ # You can also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) -if __name__ == '__main__': +if __name__ == "__main__": # # 1 Creating models # # Create a building with a weather station as a context provider. @@ -46,39 +46,37 @@ # you have to use the api of the Context Entity Model. # create unit metadata for the temperature sensor of the weather station - temperature_metadata = NamedMetadata(name="unit", - type="Unit", - value=Unit(name="degree Celsius").model_dump()) + temperature_metadata = NamedMetadata( + name="unit", type="Unit", value=Unit(name="degree Celsius").model_dump() + ) # create the 'temperature' attribute of the weather station - temperature = ContextAttribute(type="Number", - value=20.5, - metadata=temperature_metadata) + temperature = ContextAttribute( + type="Number", value=20.5, metadata=temperature_metadata + ) # create the complete model of the weather station - weather_station = ContextEntity(id="urn:ngsi-ld:WeatherStation:001", - type="WeatherStation", - temperature=temperature) + weather_station = ContextEntity( + id="urn:ngsi-ld:WeatherStation:001", + type="WeatherStation", + temperature=temperature, + ) # print the complete weather station object print(f"{'*' * 80}\nWeather station with one attribute\n{'*' * 80}") print(weather_station.model_dump_json(indent=2)) # create an additional attribute 'wind_speed' of the weather station - wind_speed_metadata = NamedMetadata(name="unit", - type="Unit", - value=Unit(name="kilometre per " - "hour").model_dump()) + wind_speed_metadata = NamedMetadata( + name="unit", type="Unit", value=Unit(name="kilometre per " "hour").model_dump() + ) # create the temperature attribute of the weather station - wind_speed = ContextAttribute(type="Number", - value=60, - metadata=wind_speed_metadata) + wind_speed = ContextAttribute(type="Number", value=60, metadata=wind_speed_metadata) weather_station.add_attributes(attrs={"wind_speed": wind_speed}) # print the complete model print(f"{'*' * 80}\nWeather station with two attributes\n{'*' * 80}") print(weather_station.model_dump_json(indent=2)) - building = ContextEntity(id="urn:ngsi-ld:building:001", - type="Building") + building = ContextEntity(id="urn:ngsi-ld:building:001", type="Building") print(f"{'*' * 80}\nBuilding without own attributes\n{'*' * 80}") print(building.model_dump_json(indent=2)) @@ -98,19 +96,20 @@ "type": "Building", } ], - "attrs": ["temperature", "wind_speed"] + "attrs": ["temperature", "wind_speed"], }, - "provider": provider.model_dump() - }) - print(f"{'*' * 80}\nRegistration that makes the weather station a context provider for the building\n{'*' * 80}") + "provider": provider.model_dump(), + } + ) + print( + f"{'*' * 80}\nRegistration that makes the weather station a context provider for the building\n{'*' * 80}" + ) print(registration.model_dump_json(indent=2)) # # 3 Post created objects to Fiware # - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) - client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) + client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) client.post_entity(entity=weather_station) client.post_entity(entity=building) registration_id = client.post_registration(registration=registration) @@ -122,13 +121,13 @@ print(f"{'*' * 80}\nPosted registration to the context broker\n{'*' * 80}") print(registration.model_dump_json(indent=2)) - weather_station = client.get_entity(entity_id=weather_station.id, - entity_type=weather_station.type) + weather_station = client.get_entity( + entity_id=weather_station.id, entity_type=weather_station.type + ) print(f"{'*' * 80}\nPosted weather station to the context broker\n{'*' * 80}") print(weather_station.model_dump_json(indent=2)) - building = client.get_entity(entity_id=building.id, - entity_type=building.type) + building = client.get_entity(entity_id=building.id, entity_type=building.type) print(f"{'*' * 80}\nPosted building to the context broker\n{'*' * 80}") print(building.model_dump_json(indent=2)) @@ -148,8 +147,6 @@ # # 6 Delete objects # - client.delete_entity(entity_id=weather_station.id, - entity_type=weather_station.type) - client.delete_entity(entity_id=building.id, - entity_type=building.type) + client.delete_entity(entity_id=weather_station.id, entity_type=weather_station.type) + client.delete_entity(entity_id=building.id, entity_type=building.type) client.delete_registration(registration_id) diff --git a/examples/ngsi_v2/e06_ngsi_v2_autogenerate_context_data_models.py b/examples/ngsi_v2/e06_ngsi_v2_autogenerate_context_data_models.py index 598a2e51..bcb9fa1d 100644 --- a/examples/ngsi_v2/e06_ngsi_v2_autogenerate_context_data_models.py +++ b/examples/ngsi_v2/e06_ngsi_v2_autogenerate_context_data_models.py @@ -13,54 +13,57 @@ # definitions """ + # ## Import packages import os from pathlib import Path -from filip.models.ngsi_v2.context import ContextEntity, \ - NamedContextAttribute -from filip.utils.model_generation import create_data_model_file, \ - create_context_entity_model +from filip.models.ngsi_v2.context import ContextEntity, NamedContextAttribute +from filip.utils.model_generation import ( + create_data_model_file, + create_context_entity_model, +) # ## Parameters path = Path(os.getcwd()).joinpath("./data_models") -if __name__ == '__main__': +if __name__ == "__main__": # ## 1. Create a custom context entity model # # create an entity that looks like the one you want to creat a model for - attr = NamedContextAttribute(name="someAttr", - type="TEXT", - value="something") + attr = NamedContextAttribute(name="someAttr", type="TEXT", value="something") entity = ContextEntity(id="myId", type="MyType") entity.add_attributes(attrs=[attr]) # ### 1.1 create the model and write it to a json-schema file - model = create_context_entity_model(name="MyModel", - data=entity.model_dump(), - path=path.joinpath('entity.json')) + model = create_context_entity_model( + name="MyModel", data=entity.model_dump(), path=path.joinpath("entity.json") + ) # ### 1.2 create the model and write it to a python file - model = create_context_entity_model(name="MyModel", - data=entity.model_dump(), - path=path.joinpath('entity.py')) + model = create_context_entity_model( + name="MyModel", data=entity.model_dump(), path=path.joinpath("entity.py") + ) # ## 2. Parsing from external resources # - create_data_model_file(path=path.joinpath('commons.py'), - url='https://smart-data-models.github.io/data-models/' - 'common-schema.json') + create_data_model_file( + path=path.joinpath("commons.py"), + url="https://smart-data-models.github.io/data-models/" "common-schema.json", + ) # ## 3. Use generated data models - from examples.ngsi_v2.data_models.commons import \ - OpeningHoursSpecificationItem, \ - time, \ - DayOfWeek, \ - datetime + from examples.ngsi_v2.data_models.commons import ( + OpeningHoursSpecificationItem, + time, + DayOfWeek, + datetime, + ) opening_hours = OpeningHoursSpecificationItem( opens=time(hour=10), closes=time(hour=19), dayOfWeek=DayOfWeek.Saturday, - validFrom=datetime(year=2022, month=1, day=1)) + validFrom=datetime(year=2022, month=1, day=1), + ) - print(opening_hours.json(indent=2)) \ No newline at end of file + print(opening_hours.json(indent=2)) diff --git a/examples/ngsi_v2/e07_ngsi_v2_iota_basics.py b/examples/ngsi_v2/e07_ngsi_v2_iota_basics.py index c5aca9b4..d6235ea5 100644 --- a/examples/ngsi_v2/e07_ngsi_v2_iota_basics.py +++ b/examples/ngsi_v2/e07_ngsi_v2_iota_basics.py @@ -1,13 +1,21 @@ """ # # Examples for working with IoT Devices """ + # ## Import packages import logging import json from filip.clients.ngsi_v2 import IoTAClient from filip.models.base import FiwareHeader, DataType -from filip.models.ngsi_v2.iot import Device, ServiceGroup, TransportProtocol, \ - StaticDeviceAttribute, DeviceAttribute, LazyDeviceAttribute, DeviceCommand +from filip.models.ngsi_v2.iot import ( + Device, + ServiceGroup, + TransportProtocol, + StaticDeviceAttribute, + DeviceAttribute, + LazyDeviceAttribute, + DeviceCommand, +) from uuid import uuid4 from filip.config import settings @@ -23,15 +31,16 @@ # Here you can also change FIWARE service and service path. # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) @@ -43,14 +52,14 @@ # # For more details about this step see e01_http_clients.py. - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) - iota_client = IoTAClient(url=IOTA_URL, - fiware_header=fiware_header) + iota_client = IoTAClient(url=IOTA_URL, fiware_header=fiware_header) - print(f"IoTA: {json.dumps(iota_client.get_version(), indent=2)}" - f" located at the url: {iota_client.base_url}") + print( + f"IoTA: {json.dumps(iota_client.get_version(), indent=2)}" + f" located at the url: {iota_client.base_url}" + ) # # 2 Device Model # @@ -66,33 +75,37 @@ # and manipulated. # # Dictionary: - example_device_dict = {"device_id": "urn:ngsi-ld:sensor:001", - "service": SERVICE, - "service_path": SERVICE_PATH, - "entity_name": "sensor1", - "entity_type": "Sensor", - "timezone": 'Europe/Berlin', - "timestamp": True, - "apikey": "1234", - "protocol": "IoTA-UL", - "transport": "MQTT", - "lazy": [], - "commands": [], - "attributes": [], - "static_attributes": [], - "internal_attributes": [], - "explicitAttrs": False, - "ngsiVersion": "v2"} + example_device_dict = { + "device_id": "urn:ngsi-ld:sensor:001", + "service": SERVICE, + "service_path": SERVICE_PATH, + "entity_name": "sensor1", + "entity_type": "Sensor", + "timezone": "Europe/Berlin", + "timestamp": True, + "apikey": "1234", + "protocol": "IoTA-UL", + "transport": "MQTT", + "lazy": [], + "commands": [], + "attributes": [], + "static_attributes": [], + "internal_attributes": [], + "explicitAttrs": False, + "ngsiVersion": "v2", + } device1 = Device(**example_device_dict) # Direct Parameters: - device2 = Device(device_id="urn:ngsi-ld:sensor:002", - service=SERVICE, - service_path=SERVICE_PATH, - entity_name="sensor2", - entity_type="Sensor", - transport=TransportProtocol.HTTP, - endpoint="http://orion:1026") # URL for IoTAgent to reach Orion + device2 = Device( + device_id="urn:ngsi-ld:sensor:002", + service=SERVICE, + service_path=SERVICE_PATH, + entity_name="sensor2", + entity_type="Sensor", + transport=TransportProtocol.HTTP, + endpoint="http://orion:1026", + ) # URL for IoTAgent to reach Orion # ## 2.2 Device Attributes # @@ -104,9 +117,9 @@ # # These attributes represent static information (such as names) and are # mirrored 1:1 - device2.add_attribute(StaticDeviceAttribute(name="address", - type=DataType.TEXT, - value="Lichtenhof 3")) + device2.add_attribute( + StaticDeviceAttribute(name="address", type=DataType.TEXT, value="Lichtenhof 3") + ) # ### 2.2.2 DeviceAttribute # # These attributes represent live information of the device. @@ -116,8 +129,7 @@ # # DeviceAttributes, always keep the value in the context entity up-to-date # (polling) - device2.add_attribute(DeviceAttribute(name="temperature", - object_id="t")) + device2.add_attribute(DeviceAttribute(name="temperature", object_id="t")) # LazyDeviceAttributes, only update the value in the entity if it is # accessed (event based) device2.add_attribute(LazyDeviceAttribute(name="temperature")) @@ -134,8 +146,10 @@ # # 3 Interact with Fiware # # ## 3.1 Upload a new Device - print(f"Payload that will be sent to the IoT-Agent:\n " - f"{device2.model_dump_json(indent=2)}") + print( + f"Payload that will be sent to the IoT-Agent:\n " + f"{device2.model_dump_json(indent=2)}" + ) iota_client.post_device(device=device2, update=True) # # ## 3.2 Load a specific device as model @@ -161,9 +175,9 @@ # https://iotagent-node-lib.readthedocs.io/en/latest/api/index.html#service-group-api # ## 4.1. Create a service group - service_group1 = ServiceGroup(entity_type='Thing', - resource='/iot/json', - apikey=str(uuid4())) + service_group1 = ServiceGroup( + entity_type="Thing", resource="/iot/json", apikey=str(uuid4()) + ) iota_client.post_groups(service_groups=[service_group1]) # ## 4.2 Access a service group @@ -171,12 +185,10 @@ # All groups: retrieved_groups = iota_client.get_group_list() # a specific group - my_group = iota_client.get_group(resource='/iot/json', - apikey=service_group1.apikey) + my_group = iota_client.get_group(resource="/iot/json", apikey=service_group1.apikey) # ## 4.3 Delete a service group - iota_client.delete_group(resource='/iot/json', - apikey=service_group1.apikey) + iota_client.delete_group(resource="/iot/json", apikey=service_group1.apikey) # # 5 Clean up (Optional) # diff --git a/examples/ngsi_v2/e08_ngsi_v2_iota_paho_mqtt.py b/examples/ngsi_v2/e08_ngsi_v2_iota_paho_mqtt.py index 522c8d93..d592f712 100644 --- a/examples/ngsi_v2/e08_ngsi_v2_iota_paho_mqtt.py +++ b/examples/ngsi_v2/e08_ngsi_v2_iota_paho_mqtt.py @@ -2,6 +2,7 @@ # This example shows how to provision a virtual iot device in a FIWARE-based # IoT Platform using FiLiP and PahoMQTT """ + # ## Import packages import json import logging @@ -12,12 +13,13 @@ from urllib.parse import urlparse from filip.config import settings from filip.models import FiwareHeader -from filip.models.ngsi_v2.iot import \ - Device, \ - DeviceAttribute, \ - DeviceCommand, \ - ServiceGroup, \ - StaticDeviceAttribute +from filip.models.ngsi_v2.iot import ( + Device, + DeviceAttribute, + DeviceCommand, + ServiceGroup, + StaticDeviceAttribute, +) from filip.models.ngsi_v2.context import NamedCommand from filip.clients.ngsi_v2 import HttpClient, HttpClientConfig @@ -36,31 +38,31 @@ # Here you can also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # You may also change the ApiKey Information # ApiKey of the Device -DEVICE_APIKEY = 'filip-example-device' +DEVICE_APIKEY = "filip-example-device" # ApiKey of the ServiceGroup -SERVICE_GROUP_APIKEY = 'filip-example-service-group' +SERVICE_GROUP_APIKEY = "filip-example-service-group" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) -if __name__ == '__main__': +if __name__ == "__main__": # # 1 FiwareHeader # Since we want to use the multi-tenancy concept of fiware we always start # with creating a fiware header - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # # 2 Setup # @@ -73,51 +75,50 @@ # add the metadata for units using the unit models. You will later # notice that the library automatically augments the provided # information about units. - device_attr1 = DeviceAttribute(name='temperature', - object_id='t', - type="Number", - metadata={"unit": - {"type": "Unit", - "value": { - "name": "degree Celsius" - } - } - }) + device_attr1 = DeviceAttribute( + name="temperature", + object_id="t", + type="Number", + metadata={"unit": {"type": "Unit", "value": {"name": "degree Celsius"}}}, + ) # creating a static attribute that holds additional information - static_device_attr = StaticDeviceAttribute(name='info', - type="Text", - value="Filip example for " - "virtual IoT device") + static_device_attr = StaticDeviceAttribute( + name="info", type="Text", value="Filip example for " "virtual IoT device" + ) # creating a command that the IoT device will liston to - device_command = DeviceCommand(name='heater') + device_command = DeviceCommand(name="heater") # NOTE: You need to know that if you define an apikey for a single device it # will be only used for outgoing traffic. This is not clearly defined # in the official documentation. # https://fiware-iotagent-json.readthedocs.io/en/latest/usermanual/index.html - device = Device(device_id='urn:ngsi-ld:device:001', - entity_name='urn:ngsi-ld:device:001', - entity_type='Thing', - protocol='IoTA-JSON', - transport='MQTT', - apikey=DEVICE_APIKEY, - attributes=[device_attr1], - static_attributes=[static_device_attr], - commands=[device_command]) + device = Device( + device_id="urn:ngsi-ld:device:001", + entity_name="urn:ngsi-ld:device:001", + entity_type="Thing", + protocol="IoTA-JSON", + transport="MQTT", + apikey=DEVICE_APIKEY, + attributes=[device_attr1], + static_attributes=[static_device_attr], + commands=[device_command], + ) # you can also add additional attributes via the Device API - device_attr2 = DeviceAttribute(name='humidity', - object_id='h', - type="Number", - metadata={"unitText": - {"value": "percent", - "type": "Text"}}) + device_attr2 = DeviceAttribute( + name="humidity", + object_id="h", + type="Number", + metadata={"unitText": {"value": "percent", "type": "Text"}}, + ) device.add_attribute(attribute=device_attr2) # this will print our configuration that we will send - logging.info("This is our device configuration: \n" + device.model_dump_json(indent=2)) + logging.info( + "This is our device configuration: \n" + device.model_dump_json(indent=2) + ) # ## 2.2 Device Provision # @@ -132,10 +133,12 @@ # In order to change the apikey of our devices for incoming data we need to # create a service group that our device will be we attached to. # NOTE: This is important in order to adjust the apikey for incoming traffic. - service_group = ServiceGroup(service=fiware_header.service, - subservice=fiware_header.service_path, - apikey=SERVICE_GROUP_APIKEY, - resource='/iot/json') + service_group = ServiceGroup( + service=fiware_header.service, + subservice=fiware_header.service_path, + apikey=SERVICE_GROUP_APIKEY, + resource="/iot/json", + ) # Create the Http client node that once sent, the device can't be posted # again, and you need to use the update command. @@ -152,10 +155,13 @@ logging.info(f"{device.model_dump_json(indent=2)}") # check if the data entity is created in the context broker - entity = client.cb.get_entity(entity_type=device.entity_type, - entity_id=device.device_id) - logging.info("This is our data entity in the context broker belonging to our device: \n" + - entity.model_dump_json(indent=2)) + entity = client.cb.get_entity( + entity_type=device.entity_type, entity_id=device.device_id + ) + logging.info( + "This is our data entity in the context broker belonging to our device: \n" + + entity.model_dump_json(indent=2) + ) # # 3 MQTT Client # @@ -173,10 +179,14 @@ # client will not be able to execute them. def on_connect(client, userdata, flags, reason_code, properties=None): if reason_code != 0: - logger.error(f"MQTT Client failed to connect with the error code: '{reason_code}'") + logger.error( + f"MQTT Client failed to connect with the error code: '{reason_code}'" + ) raise ConnectionError else: - logger.info(f"MQTT Client successfully connected with the reason code: {reason_code}") + logger.info( + f"MQTT Client successfully connected with the reason code: {reason_code}" + ) # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. @@ -199,18 +209,21 @@ def on_message(client, userdata, msg): data = json.loads(msg.payload) res = {k: v for k, v in data.items()} print(f"MQTT Client on_message payload: {msg.payload}") - client.publish(topic=f"/json/{service_group.apikey}" - f"/{device.device_id}/cmdexe", - payload=json.dumps(res)) + client.publish( + topic=f"/json/{service_group.apikey}" f"/{device.device_id}/cmdexe", + payload=json.dumps(res), + ) def on_disconnect(client, userdata, flags, reasonCode, properties): logger.info("MQTT Client disconnected" + str(reasonCode)) - mqtt_client = mqtt.Client(client_id="filip-iot-example", - userdata=None, - protocol=mqtt.MQTTv5, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - transport="tcp") + mqtt_client = mqtt.Client( + client_id="filip-iot-example", + userdata=None, + protocol=mqtt.MQTTv5, + callback_api_version=mqtt.CallbackAPIVersion.VERSION2, + transport="tcp", + ) # bind callbacks to the client mqtt_client.on_connect = on_connect mqtt_client.on_subscribe = on_subscribe @@ -218,13 +231,15 @@ def on_disconnect(client, userdata, flags, reasonCode, properties): mqtt_client.on_disconnect = on_disconnect # connect to the server mqtt_url = urlparse(MQTT_BROKER_URL) - mqtt_client.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqtt_client.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # create a non-blocking thread for mqtt communication mqtt_client.loop_start() @@ -234,13 +249,17 @@ def on_disconnect(client, userdata, flags, reasonCode, properties): logger.info("Send data to platform:" + payload) mqtt_client.publish( topic=f"/json/{service_group.apikey}/{device.device_id}/attrs", - payload=payload) + payload=payload, + ) time.sleep(1) - entity = client.cb.get_entity(entity_id=device.device_id, - entity_type=device.entity_type) - logger.info("This is updated entity status after measurements are " - "received: \n" + entity.model_dump_json(indent=2)) + entity = client.cb.get_entity( + entity_id=device.device_id, entity_type=device.entity_type + ) + logger.info( + "This is updated entity status after measurements are " + "received: \n" + entity.model_dump_json(indent=2) + ) # create and send a command via the context broker for i in range(10): @@ -249,19 +268,21 @@ def on_disconnect(client, userdata, flags, reasonCode, properties): else: value = False - context_command = NamedCommand(name=device_command.name, - value=value) - client.cb.post_command(entity_id=entity.id, - entity_type=entity.type, - command=context_command) + context_command = NamedCommand(name=device_command.name, value=value) + client.cb.post_command( + entity_id=entity.id, entity_type=entity.type, command=context_command + ) time.sleep(1) # check the entity the command attribute should now show the PENDING - entity = client.cb.get_entity(entity_id=device.device_id, - entity_type=device.entity_type) - logger.info("This is updated entity status after the command was sent " - "and the acknowledge message was received: " - "\n" + entity.model_dump_json(indent=2)) + entity = client.cb.get_entity( + entity_id=device.device_id, entity_type=device.entity_type + ) + logger.info( + "This is updated entity status after the command was sent " + "and the acknowledge message was received: " + "\n" + entity.model_dump_json(indent=2) + ) # close the mqtt listening thread mqtt_client.loop_stop() @@ -271,7 +292,7 @@ def on_disconnect(client, userdata, flags, reasonCode, properties): # # 4 Cleanup the server and delete everything # client.iota.delete_device(device_id=device.device_id) - client.iota.delete_group(resource=service_group.resource, - apikey=service_group.apikey) - client.cb.delete_entity(entity_id=entity.id, - entity_type=entity.type) + client.iota.delete_group( + resource=service_group.resource, apikey=service_group.apikey + ) + client.cb.delete_entity(entity_id=entity.id, entity_type=entity.type) diff --git a/examples/ngsi_v2/e09_ngsi_v2_iota_filip_mqtt.py b/examples/ngsi_v2/e09_ngsi_v2_iota_filip_mqtt.py index 1b068e1e..abac92b5 100644 --- a/examples/ngsi_v2/e09_ngsi_v2_iota_filip_mqtt.py +++ b/examples/ngsi_v2/e09_ngsi_v2_iota_filip_mqtt.py @@ -3,6 +3,7 @@ # using FiLiP's IoTA-MQTT Client. This client comes along with a convenient # API for handling MQTT communication with FIWARE's IoT-Agent """ + # ## Import packages import logging import random @@ -12,12 +13,13 @@ from urllib.parse import urlparse from filip.clients.mqtt import IoTAMQTTClient from filip.models import FiwareHeader -from filip.models.ngsi_v2.iot import \ - Device, \ - DeviceAttribute, \ - DeviceCommand, \ - ServiceGroup, \ - PayloadProtocol +from filip.models.ngsi_v2.iot import ( + Device, + DeviceAttribute, + DeviceCommand, + ServiceGroup, + PayloadProtocol, +) from filip.models.ngsi_v2.context import NamedCommand # ## Parameters @@ -35,24 +37,25 @@ # You can here also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # You may also change the ApiKey Information # ApiKey of the ServiceGroup -SERVICE_GROUP_APIKEY = 'filip-example-service-group' +SERVICE_GROUP_APIKEY = "filip-example-service-group" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) -if __name__ == '__main__': +if __name__ == "__main__": # # 1 Setup # @@ -60,40 +63,39 @@ # # Since we want to use the multi-tenancy concept of fiware we always start # with create a fiware header - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # ## 1.2 Device configuration # service_group_json = ServiceGroup( - apikey=SERVICE_PATH.strip('/'), - resource="/iot/json") - service_group_ul = ServiceGroup( - apikey=SERVICE_PATH.strip('/'), - resource="/iot/d") - - device_attr = DeviceAttribute(name='temperature', - object_id='t', - type="Number") - device_command = DeviceCommand(name='heater', type="Boolean") - - device_json = Device(device_id='my_json_device', - entity_name='my_json_device', - entity_type='Thing', - protocol='IoTA-JSON', - transport='MQTT', - apikey=service_group_json.apikey, - attributes=[device_attr], - commands=[device_command]) - - device_ul = Device(device_id='my_ul_device', - entity_name='my_ul_device', - entity_type='Thing', - protocol='PDI-IoTA-UltraLight', - transport='MQTT', - apikey=service_group_ul.apikey, - attributes=[device_attr], - commands=[device_command]) + apikey=SERVICE_PATH.strip("/"), resource="/iot/json" + ) + service_group_ul = ServiceGroup(apikey=SERVICE_PATH.strip("/"), resource="/iot/d") + + device_attr = DeviceAttribute(name="temperature", object_id="t", type="Number") + device_command = DeviceCommand(name="heater", type="Boolean") + + device_json = Device( + device_id="my_json_device", + entity_name="my_json_device", + entity_type="Thing", + protocol="IoTA-JSON", + transport="MQTT", + apikey=service_group_json.apikey, + attributes=[device_attr], + commands=[device_command], + ) + + device_ul = Device( + device_id="my_ul_device", + entity_name="my_ul_device", + entity_type="Thing", + protocol="PDI-IoTA-UltraLight", + transport="MQTT", + apikey=service_group_ul.apikey, + attributes=[device_attr], + commands=[device_command], + ) # ## 1.3 IoTAMQTTClient # @@ -114,7 +116,6 @@ def on_subscribe(mqttc, obj, mid, granted_qos, properties=None): def on_log(mqttc, obj, level, string): mqttc.logger.info(string) - mqttc.on_connect = on_connect mqttc.on_connect_fail = on_connect_fail mqttc.on_publish = on_publish @@ -132,7 +133,6 @@ def on_log(mqttc, obj, level, string): first_payload = "filip_test_1" second_payload = "filip_test_2" - def on_message_first(mqttc, obj, msg, properties=None): pass # do something @@ -141,20 +141,19 @@ def on_message_second(mqttc, obj, msg, properties=None): pass # do something - - mqttc.message_callback_add(sub=first_topic, - callback=on_message_first) - mqttc.message_callback_add(sub=second_topic, - callback=on_message_second) + mqttc.message_callback_add(sub=first_topic, callback=on_message_first) + mqttc.message_callback_add(sub=second_topic, callback=on_message_second) mqtt_broker_url = urlparse(MQTT_BROKER_URL) - mqttc.connect(host=mqtt_broker_url.hostname, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_broker_url.hostname, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) mqttc.subscribe(topic=first_topic) # create a non-blocking loop @@ -224,60 +223,58 @@ def on_message_second(mqttc, obj, msg, properties=None): for device in mqttc.devices: mqttc.delete_device(device.device_id) - def on_command(client, obj, msg): - apikey, device_id, payload = \ - client.get_encoder(PayloadProtocol.IOTA_JSON).decode_message( - msg=msg) + apikey, device_id, payload = client.get_encoder( + PayloadProtocol.IOTA_JSON + ).decode_message(msg=msg) # acknowledge a command. Here command are usually single # messages. The first key is equal to the commands name. - client.publish(device_id=device_id, - command_name=next(iter(payload)), - payload=payload) - + client.publish( + device_id=device_id, command_name=next(iter(payload)), payload=payload + ) mqttc.add_service_group(service_group_json) mqttc.add_device(device_json) - mqttc.add_command_callback(device_id=device_json.device_id, - callback=on_command) + mqttc.add_command_callback(device_id=device_json.device_id, callback=on_command) from filip.clients.ngsi_v2 import HttpClient, HttpClientConfig - httpc_config = HttpClientConfig(cb_url=CB_URL, - iota_url=IOTA_URL) - httpc = HttpClient(fiware_header=fiware_header, - config=httpc_config) + httpc_config = HttpClientConfig(cb_url=CB_URL, iota_url=IOTA_URL) + httpc = HttpClient(fiware_header=fiware_header, config=httpc_config) httpc.iota.post_group(service_group=service_group_json, update=True) httpc.iota.post_device(device=device_json, update=True) mqtt_broker_url = urlparse(MQTT_BROKER_URL) - mqttc.connect(host=mqtt_broker_url.hostname, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_broker_url.hostname, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) mqttc.subscribe() mqttc.loop_start() # ## 4.2 Command # - entity = httpc.cb.get_entity(entity_id=device_json.device_id, - entity_type=device_json.entity_type) - context_command = NamedCommand(name=device_json.commands[0].name, - value=False) + entity = httpc.cb.get_entity( + entity_id=device_json.device_id, entity_type=device_json.entity_type + ) + context_command = NamedCommand(name=device_json.commands[0].name, value=False) - httpc.cb.post_command(entity_id=entity.id, - entity_type=entity.type, - command=context_command) + httpc.cb.post_command( + entity_id=entity.id, entity_type=entity.type, command=context_command + ) time.sleep(2) - entity = httpc.cb.get_entity(entity_id=device_json.device_id, - entity_type=device_json.entity_type) + entity = httpc.cb.get_entity( + entity_id=device_json.device_id, entity_type=device_json.entity_type + ) # The entity.heater_status.value should now have the status 'OK' print(f"Heater status value: {entity.heater_status.value}") @@ -285,22 +282,26 @@ def on_command(client, obj, msg): # ## 4.3 Publish # payload = random.randint(0, 30) - mqttc.publish(device_id=device_json.device_id, - payload={device_json.attributes[0].object_id: payload}) + mqttc.publish( + device_id=device_json.device_id, + payload={device_json.attributes[0].object_id: payload}, + ) time.sleep(1) - entity = httpc.cb.get_entity(entity_id=device_json.device_id, - entity_type=device_json.entity_type) + entity = httpc.cb.get_entity( + entity_id=device_json.device_id, entity_type=device_json.entity_type + ) # Set Temperature Value print(f"Entity temperature value before publishing: {entity.temperature.value}") payload = random.randint(0, 30) - mqttc.publish(device_id=device_json.device_id, - attribute_name="temperature", - payload=payload) + mqttc.publish( + device_id=device_json.device_id, attribute_name="temperature", payload=payload + ) time.sleep(1) - entity = httpc.cb.get_entity(entity_id=device_json.device_id, - entity_type=device_json.entity_type) + entity = httpc.cb.get_entity( + entity_id=device_json.device_id, entity_type=device_json.entity_type + ) # Changed Temperature Value print(f"Entity temperature value after publishing: {entity.temperature.value}") diff --git a/examples/ngsi_v2/e10_ngsi_v2_quantumleap.py b/examples/ngsi_v2/e10_ngsi_v2_quantumleap.py index 03ede0b2..db8497ac 100644 --- a/examples/ngsi_v2/e10_ngsi_v2_quantumleap.py +++ b/examples/ngsi_v2/e10_ngsi_v2_quantumleap.py @@ -1,6 +1,7 @@ """ # Example for working with the QuantumLeapClient """ + # ## Import packages import logging import time @@ -11,6 +12,7 @@ from filip.models.base import FiwareHeader from filip.clients.ngsi_v2 import ContextBrokerClient, QuantumLeapClient from filip.utils.cleanup import clear_all + # ## Parameters # # To run this example you need a working Fiware v2 setup with a @@ -23,15 +25,16 @@ # Here you can also change FIWARE service and service path. # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/example' +SERVICE_PATH = "/example" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) if __name__ == "__main__": @@ -48,24 +51,28 @@ clear_all(fiware_header=fiware_header, cb_url=CB_URL, ql_url=QL_URL) ql_client = QuantumLeapClient(url=QL_URL, fiware_header=fiware_header) - print(f"Quantum Leap Client version: {ql_client.get_version()['version']}" - f" located at url: {ql_client.base_url}") + print( + f"Quantum Leap Client version: {ql_client.get_version()['version']}" + f" located at url: {ql_client.base_url}" + ) cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) - print(f"Context Broker version: {cb_client.get_version()['orion']['version']}" - f" located at url: {cb_client.base_url}") + print( + f"Context Broker version: {cb_client.get_version()['orion']['version']}" + f" located at url: {cb_client.base_url}" + ) # ## 2 Interact with QL # # ### 2.1 Create a ContextEntity to work with # # For more information see: e01_ngsi_v2_context_basics.py - hall = {"id": "Hall_1", - "type": "Room", - "temperature": {"value": random.randint(0, 100), - "type": "Integer"}, - } + hall = { + "id": "Hall_1", + "type": "Room", + "temperature": {"value": random.randint(0, 100), "type": "Integer"}, + } hall_entity = ContextEntity(**hall) cb_client.post_entity(hall_entity) @@ -76,20 +83,14 @@ # Note: The IPs must be the ones that Orion and quantumleap can access, # e.g. service name or static IP, localhost will not work here. - subscription: Subscription = Subscription.model_validate({ - "subject": { - "entities": [ - { - "id": hall_entity.id - } - ] - }, - "notification": { # Notify QL automatically - "http": { - "url": "http://quantumleap:8668/v2/notify" - } + subscription: Subscription = Subscription.model_validate( + { + "subject": {"entities": [{"id": hall_entity.id}]}, + "notification": { # Notify QL automatically + "http": {"url": "http://quantumleap:8668/v2/notify"} + }, } - }) + ) subscription_id = cb_client.post_subscription(subscription=subscription) # get all subscriptions @@ -101,40 +102,53 @@ if entity.id == hall_entity.id: try: ql_client.post_notification( - notification=Message( - data=[hall_entity], - subscriptionId=sub.id)) + notification=Message(data=[hall_entity], subscriptionId=sub.id) + ) subscription_id = sub.id except: logger.error("Can not notify QL") # notify QL via Orion for i in range(5, 10): - cb_client.update_attribute_value(entity_id=hall_entity.id, - entity_type=hall_entity.type, - attr_name="temperature", - value=i) + cb_client.update_attribute_value( + entity_id=hall_entity.id, + entity_type=hall_entity.type, + attr_name="temperature", + value=i, + ) time.sleep(1) # get historical data as object and you can directly convert them to pandas dataframes try: - print(f"get_entity_by_id method converted to pandas:\n" - f"{ql_client.get_entity_by_id(hall_entity.id).to_pandas()}\n") - - print(f"get_entity_values_by_id method:\n" - f"{ql_client.get_entity_values_by_id(hall_entity.id)}\n") - - print(f"get_entity_attr_by_id method:\n" - f"{ql_client.get_entity_attr_by_id(hall_entity.id, 'temperature')}\n") - - print(f"get_entity_attr_values_by_id method:\n" - f"{ql_client.get_entity_attr_values_by_id(hall_entity.id, attr_name='temperature')}\n") - - print(f"get_entity_attr_by_type method:\n" - f"{ql_client.get_entity_attr_by_type(hall_entity.type, attr_name='temperature')}\n") - - print(f"get_entity_attr_values_by_type method:\n" - f"{ql_client.get_entity_attr_values_by_type(hall_entity.type, 'temperature')}") + print( + f"get_entity_by_id method converted to pandas:\n" + f"{ql_client.get_entity_by_id(hall_entity.id).to_pandas()}\n" + ) + + print( + f"get_entity_values_by_id method:\n" + f"{ql_client.get_entity_values_by_id(hall_entity.id)}\n" + ) + + print( + f"get_entity_attr_by_id method:\n" + f"{ql_client.get_entity_attr_by_id(hall_entity.id, 'temperature')}\n" + ) + + print( + f"get_entity_attr_values_by_id method:\n" + f"{ql_client.get_entity_attr_values_by_id(hall_entity.id, attr_name='temperature')}\n" + ) + + print( + f"get_entity_attr_by_type method:\n" + f"{ql_client.get_entity_attr_by_type(hall_entity.type, attr_name='temperature')}\n" + ) + + print( + f"get_entity_attr_values_by_type method:\n" + f"{ql_client.get_entity_attr_values_by_type(hall_entity.type, 'temperature')}" + ) except: logger.info("There might be no historical data for some calls.") @@ -142,15 +156,13 @@ # # Delete entity in QL try: - ql_client.delete_entity(entity_id=hall_entity.id, - entity_type=hall_entity.type) + ql_client.delete_entity(entity_id=hall_entity.id, entity_type=hall_entity.type) except: logger.error("Can not delete data from QL") # delete entity in CV try: - cb_client.delete_entity(entity_id=hall_entity.id, - entity_type=hall_entity.type) + cb_client.delete_entity(entity_id=hall_entity.id, entity_type=hall_entity.type) except: logger.error("Can not delete entity from context broker") diff --git a/examples/ngsi_v2/e11_ngsi_v2_context_specific_models.py b/examples/ngsi_v2/e11_ngsi_v2_context_specific_models.py index 0e42eb15..607fd1ea 100644 --- a/examples/ngsi_v2/e11_ngsi_v2_context_specific_models.py +++ b/examples/ngsi_v2/e11_ngsi_v2_context_specific_models.py @@ -13,6 +13,7 @@ # duplication of code unnecessary and allows for a more consistent # entities and systems. """ + import json from pprint import pprint from typing import Literal diff --git a/examples/ngsi_v2/e11_ngsi_v2_semantics/models.py b/examples/ngsi_v2/e11_ngsi_v2_semantics/models.py index c788461d..971d525b 100644 --- a/examples/ngsi_v2/e11_ngsi_v2_semantics/models.py +++ b/examples/ngsi_v2/e11_ngsi_v2_semantics/models.py @@ -5,655 +5,662 @@ from enum import Enum from typing import Dict, Union, List -from filip.semantics.semantics_models import\ - SemanticClass,\ - SemanticIndividual,\ - RelationField,\ - DataField,\ - SemanticDeviceClass,\ - DeviceAttributeField,\ - CommandField -from filip.semantics.semantics_manager import\ - SemanticsManager,\ - InstanceRegistry +from filip.semantics.semantics_models import ( + SemanticClass, + SemanticIndividual, + RelationField, + DataField, + SemanticDeviceClass, + DeviceAttributeField, + CommandField, +) +from filip.semantics.semantics_manager import SemanticsManager, InstanceRegistry semantic_manager: SemanticsManager = SemanticsManager( - instance_registry=InstanceRegistry(), + instance_registry=InstanceRegistry(), ) # ---------CLASSES--------- # class Thing(SemanticClass): - """ - Predefined root_class + """ + Predefined root_class - Source(s): - None (Predefined) - """ + Source(s): + None (Predefined) + """ - def __new__(cls, *args, **kwargs): - kwargs['semantic_manager'] = semantic_manager - return super().__new__(cls, *args, **kwargs) + def __new__(cls, *args, **kwargs): + kwargs["semantic_manager"] = semantic_manager + return super().__new__(cls, *args, **kwargs) - def __init__(self, *args, **kwargs): - kwargs['semantic_manager'] = semantic_manager - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) + def __init__(self, *args, **kwargs): + kwargs["semantic_manager"] = semantic_manager + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) class Room(Thing): - """ - Generated SemanticClass without description - - Source(s): - http://www.semanticweb.org/building (building circuits) - """ - - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.goalTemperature._rules = [('exactly|1', [['integer']])] - self.name._rules = [('exactly|1', [['string']])] - self.volume._rules = [('some', [['rational']])] - - self.hasOutlet._rules = [('only', [[Outlet]])] - self.hasSensor._rules = [('only', [[Sensor]])] - self.hasTenant._rules = [('only', [[Tenant]])] - - self.hasOutlet._instance_identifier = self.get_identifier() - self.hasSensor._instance_identifier = self.get_identifier() - self.hasTenant._instance_identifier = self.get_identifier() - self.goalTemperature._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() - self.volume._instance_identifier = self.get_identifier() - - # Data fields - - goalTemperature: DataField = DataField( - name='goalTemperature', - rule='exactly 1 integer', - semantic_manager=semantic_manager) - - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) - - volume: DataField = DataField( - name='volume', - rule='some rational', - semantic_manager=semantic_manager) - - # Relation fields - - hasOutlet: RelationField = RelationField( - name='hasOutlet', - rule='only Outlet', - inverse_of=['connectedTo'], - semantic_manager=semantic_manager) - - hasSensor: RelationField = RelationField( - name='hasSensor', - rule='only Sensor', - semantic_manager=semantic_manager) - - hasTenant: RelationField = RelationField( - name='hasTenant', - rule='only Tenant', - semantic_manager=semantic_manager) + """ + Generated SemanticClass without description + + Source(s): + http://www.semanticweb.org/building (building circuits) + """ + + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.goalTemperature._rules = [("exactly|1", [["integer"]])] + self.name._rules = [("exactly|1", [["string"]])] + self.volume._rules = [("some", [["rational"]])] + + self.hasOutlet._rules = [("only", [[Outlet]])] + self.hasSensor._rules = [("only", [[Sensor]])] + self.hasTenant._rules = [("only", [[Tenant]])] + + self.hasOutlet._instance_identifier = self.get_identifier() + self.hasSensor._instance_identifier = self.get_identifier() + self.hasTenant._instance_identifier = self.get_identifier() + self.goalTemperature._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() + self.volume._instance_identifier = self.get_identifier() + + # Data fields + + goalTemperature: DataField = DataField( + name="goalTemperature", + rule="exactly 1 integer", + semantic_manager=semantic_manager, + ) + + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) + + volume: DataField = DataField( + name="volume", rule="some rational", semantic_manager=semantic_manager + ) + + # Relation fields + + hasOutlet: RelationField = RelationField( + name="hasOutlet", + rule="only Outlet", + inverse_of=["connectedTo"], + semantic_manager=semantic_manager, + ) + + hasSensor: RelationField = RelationField( + name="hasSensor", rule="only Sensor", semantic_manager=semantic_manager + ) + + hasTenant: RelationField = RelationField( + name="hasTenant", rule="only Tenant", semantic_manager=semantic_manager + ) class Building(Thing): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.goalTemperature._rules = [('exactly|1', [['integer']])] - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.goalTemperature._rules = [("exactly|1", [["integer"]])] + self.name._rules = [("exactly|1", [["string"]])] - self.hasFloor._rules = [('min|1', [[Floor]])] + self.hasFloor._rules = [("min|1", [[Floor]])] - self.hasFloor._instance_identifier = self.get_identifier() - self.goalTemperature._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() + self.hasFloor._instance_identifier = self.get_identifier() + self.goalTemperature._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() - # Data fields + # Data fields - goalTemperature: DataField = DataField( - name='goalTemperature', - rule='exactly 1 integer', - semantic_manager=semantic_manager) + goalTemperature: DataField = DataField( + name="goalTemperature", + rule="exactly 1 integer", + semantic_manager=semantic_manager, + ) - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) - # Relation fields + # Relation fields - hasFloor: RelationField = RelationField( - name='hasFloor', - rule='min 1 Floor', - semantic_manager=semantic_manager) + hasFloor: RelationField = RelationField( + name="hasFloor", rule="min 1 Floor", semantic_manager=semantic_manager + ) class Sensor(SemanticDeviceClass, Thing): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.measures._rules = [('exactly|1', [['MeasurementType']])] - self.unit._rules = [('exactly|1', [['Unit']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.measures._rules = [("exactly|1", [["MeasurementType"]])] + self.unit._rules = [("exactly|1", [["Unit"]])] - self.measurement._instance_identifier = self.get_identifier() - self.measures._instance_identifier = self.get_identifier() - self.unit._instance_identifier = self.get_identifier() + self.measurement._instance_identifier = self.get_identifier() + self.measures._instance_identifier = self.get_identifier() + self.unit._instance_identifier = self.get_identifier() - # Data fields + # Data fields - measurement: DeviceAttributeField = DeviceAttributeField( - name='measurement', - semantic_manager=semantic_manager) + measurement: DeviceAttributeField = DeviceAttributeField( + name="measurement", semantic_manager=semantic_manager + ) - measures: DataField = DataField( - name='measures', - rule='exactly 1 MeasurementType', - semantic_manager=semantic_manager) + measures: DataField = DataField( + name="measures", + rule="exactly 1 MeasurementType", + semantic_manager=semantic_manager, + ) - unit: DataField = DataField( - name='unit', - rule='exactly 1 Unit', - semantic_manager=semantic_manager) + unit: DataField = DataField( + name="unit", rule="exactly 1 Unit", semantic_manager=semantic_manager + ) class Producer(SemanticDeviceClass, Thing): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.name._rules = [("exactly|1", [["string"]])] - self.controlCommand._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() - self.state._instance_identifier = self.get_identifier() + self.controlCommand._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() + self.state._instance_identifier = self.get_identifier() - # Data fields + # Data fields - controlCommand: CommandField = CommandField( - name='controlCommand', - semantic_manager=semantic_manager) + controlCommand: CommandField = CommandField( + name="controlCommand", semantic_manager=semantic_manager + ) - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) - state: DeviceAttributeField = DeviceAttributeField( - name='state', - semantic_manager=semantic_manager) + state: DeviceAttributeField = DeviceAttributeField( + name="state", semantic_manager=semantic_manager + ) class Outlet(SemanticDeviceClass, Thing): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: - self.connectedTo._rules = [('min|1', [[Circuit]]), ('exactly|1', [[Room]])] + self.connectedTo._rules = [("min|1", [[Circuit]]), ("exactly|1", [[Room]])] - self.connectedTo._instance_identifier = self.get_identifier() - self.controlCommand._instance_identifier = self.get_identifier() - self.state._instance_identifier = self.get_identifier() + self.connectedTo._instance_identifier = self.get_identifier() + self.controlCommand._instance_identifier = self.get_identifier() + self.state._instance_identifier = self.get_identifier() - # Data fields + # Data fields - controlCommand: CommandField = CommandField( - name='controlCommand', - semantic_manager=semantic_manager) + controlCommand: CommandField = CommandField( + name="controlCommand", semantic_manager=semantic_manager + ) - state: DeviceAttributeField = DeviceAttributeField( - name='state', - semantic_manager=semantic_manager) + state: DeviceAttributeField = DeviceAttributeField( + name="state", semantic_manager=semantic_manager + ) - # Relation fields + # Relation fields - connectedTo: RelationField = RelationField( - name='connectedTo', - rule='min 1 Circuit, exactly 1 Room', - inverse_of=['hasOutlet'], - semantic_manager=semantic_manager) + connectedTo: RelationField = RelationField( + name="connectedTo", + rule="min 1 Circuit, exactly 1 Room", + inverse_of=["hasOutlet"], + semantic_manager=semantic_manager, + ) class Floor(Thing): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.name._rules = [("exactly|1", [["string"]])] - self.hasRoom._rules = [('only', [[Room]])] + self.hasRoom._rules = [("only", [[Room]])] - self.hasRoom._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() + self.hasRoom._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() - # Data fields + # Data fields - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) - # Relation fields + # Relation fields - hasRoom: RelationField = RelationField( - name='hasRoom', - rule='only Room', - semantic_manager=semantic_manager) + hasRoom: RelationField = RelationField( + name="hasRoom", rule="only Room", semantic_manager=semantic_manager + ) class Tenant(Thing): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.goalTemperature._rules = [('exactly|1', [['integer']])] - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.goalTemperature._rules = [("exactly|1", [["integer"]])] + self.name._rules = [("exactly|1", [["string"]])] - self.goalTemperature._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() + self.goalTemperature._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() - # Data fields + # Data fields - goalTemperature: DataField = DataField( - name='goalTemperature', - rule='exactly 1 integer', - semantic_manager=semantic_manager) + goalTemperature: DataField = DataField( + name="goalTemperature", + rule="exactly 1 integer", + semantic_manager=semantic_manager, + ) - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) class Circuit(Thing): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.name._rules = [("exactly|1", [["string"]])] - self.hasOutlet._rules = [('min|1', [[Outlet]])] - self.hasProducer._rules = [('min|1', [[Producer]])] + self.hasOutlet._rules = [("min|1", [[Outlet]])] + self.hasProducer._rules = [("min|1", [[Producer]])] - self.hasOutlet._instance_identifier = self.get_identifier() - self.hasProducer._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() + self.hasOutlet._instance_identifier = self.get_identifier() + self.hasProducer._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() - # Data fields + # Data fields - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) - # Relation fields + # Relation fields - hasOutlet: RelationField = RelationField( - name='hasOutlet', - rule='min 1 Outlet', - inverse_of=['connectedTo'], - semantic_manager=semantic_manager) + hasOutlet: RelationField = RelationField( + name="hasOutlet", + rule="min 1 Outlet", + inverse_of=["connectedTo"], + semantic_manager=semantic_manager, + ) - hasProducer: RelationField = RelationField( - name='hasProducer', - rule='min 1 Producer', - semantic_manager=semantic_manager) + hasProducer: RelationField = RelationField( + name="hasProducer", rule="min 1 Producer", semantic_manager=semantic_manager + ) class HeatProducer(Producer): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.name._rules = [("exactly|1", [["string"]])] - self.controlCommand._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() - self.state._instance_identifier = self.get_identifier() + self.controlCommand._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() + self.state._instance_identifier = self.get_identifier() - # Data fields + # Data fields - controlCommand: CommandField = CommandField( - name='controlCommand', - semantic_manager=semantic_manager) + controlCommand: CommandField = CommandField( + name="controlCommand", semantic_manager=semantic_manager + ) - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) - state: DeviceAttributeField = DeviceAttributeField( - name='state', - semantic_manager=semantic_manager) + state: DeviceAttributeField = DeviceAttributeField( + name="state", semantic_manager=semantic_manager + ) class ColdProducer(Producer): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.name._rules = [("exactly|1", [["string"]])] - self.controlCommand._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() - self.state._instance_identifier = self.get_identifier() + self.controlCommand._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() + self.state._instance_identifier = self.get_identifier() - # Data fields + # Data fields - controlCommand: CommandField = CommandField( - name='controlCommand', - semantic_manager=semantic_manager) + controlCommand: CommandField = CommandField( + name="controlCommand", semantic_manager=semantic_manager + ) - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) - state: DeviceAttributeField = DeviceAttributeField( - name='state', - semantic_manager=semantic_manager) + state: DeviceAttributeField = DeviceAttributeField( + name="state", semantic_manager=semantic_manager + ) class AirProducer(Producer): - """ - Generated SemanticClass without description + """ + Generated SemanticClass without description - Source(s): - http://www.semanticweb.org/building (building circuits) - """ + Source(s): + http://www.semanticweb.org/building (building circuits) + """ - def __init__(self, *args, **kwargs): - is_initialised = 'id' in self.__dict__ - super().__init__(*args, **kwargs) - if not is_initialised: - self.name._rules = [('exactly|1', [['string']])] + def __init__(self, *args, **kwargs): + is_initialised = "id" in self.__dict__ + super().__init__(*args, **kwargs) + if not is_initialised: + self.name._rules = [("exactly|1", [["string"]])] - self.controlCommand._instance_identifier = self.get_identifier() - self.name._instance_identifier = self.get_identifier() - self.state._instance_identifier = self.get_identifier() + self.controlCommand._instance_identifier = self.get_identifier() + self.name._instance_identifier = self.get_identifier() + self.state._instance_identifier = self.get_identifier() - # Data fields + # Data fields - controlCommand: CommandField = CommandField( - name='controlCommand', - semantic_manager=semantic_manager) + controlCommand: CommandField = CommandField( + name="controlCommand", semantic_manager=semantic_manager + ) - name: DataField = DataField( - name='name', - rule='exactly 1 string', - semantic_manager=semantic_manager) + name: DataField = DataField( + name="name", rule="exactly 1 string", semantic_manager=semantic_manager + ) - state: DeviceAttributeField = DeviceAttributeField( - name='state', - semantic_manager=semantic_manager) + state: DeviceAttributeField = DeviceAttributeField( + name="state", semantic_manager=semantic_manager + ) # ---------Individuals--------- # class ExampleIndividual(SemanticIndividual): - _parent_classes: List[type] = [] + _parent_classes: List[type] = [] # ---------Datatypes--------- # semantic_manager.datatype_catalogue = { - 'MeasurementType': { - 'type': 'enum', - 'enum_values': ['Air_Quality', 'Temperature'], - }, - 'Unit': { - 'type': 'enum', - 'enum_values': ['Celsius', 'Kelvin', 'Relative_Humidity'], - }, - 'rational': { - 'type': 'number', - 'number_decimal_allowed': True, - }, - 'real': { - 'type': 'number', - }, - 'PlainLiteral': { - 'type': 'string', - }, - 'XMLLiteral': { - 'type': 'string', - }, - 'Literal': { - 'type': 'string', - }, - 'anyURI': { - 'type': 'string', - }, - 'base64Binary': { - 'type': 'string', - }, - 'boolean': { - 'type': 'enum', - 'enum_values': ['True', 'False'], - }, - 'byte': { - 'type': 'number', - 'number_range_min': -128, - 'number_range_max': 127, - 'number_has_range': True, - }, - 'dateTime': { - 'type': 'date', - }, - 'dateTimeStamp': { - 'type': 'date', - }, - 'decimal': { - 'type': 'number', - 'number_decimal_allowed': True, - }, - 'double': { - 'type': 'number', - 'number_decimal_allowed': True, - }, - 'float': { - 'type': 'number', - 'number_decimal_allowed': True, - }, - 'hexBinary': { - 'allowed_chars': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'], - 'type': 'string', - }, - 'int': { - 'type': 'number', - 'number_range_min': -2147483648, - 'number_range_max': 2147483647, - 'number_has_range': True, - }, - 'integer': { - 'type': 'number', - }, - 'language': { - 'type': 'string', - }, - 'long': { - 'type': 'number', - 'number_range_min': -9223372036854775808, - 'number_range_max': 9223372036854775807, - 'number_has_range': True, - }, - 'Name': { - 'type': 'string', - }, - 'NCName': { - 'forbidden_chars': [':'], - 'type': 'string', - }, - 'negativeInteger': { - 'type': 'number', - 'number_range_max': -1, - 'number_has_range': True, - }, - 'NMTOKEN': { - 'type': 'string', - }, - 'nonNegativeInteger': { - 'type': 'number', - 'number_range_min': 0, - 'number_has_range': True, - }, - 'nonPositiveInteger': { - 'type': 'number', - 'number_range_max': -1, - 'number_has_range': True, - }, - 'normalizedString': { - 'type': 'string', - }, - 'positiveInteger': { - 'type': 'number', - 'number_range_min': 0, - 'number_has_range': True, - }, - 'short': { - 'type': 'number', - 'number_range_min': -32768, - 'number_range_max': 32767, - 'number_has_range': True, - }, - 'string': { - 'type': 'string', - }, - 'token': { - 'type': 'string', - }, - 'unsignedByte': { - 'type': 'number', - 'number_range_min': 0, - 'number_range_max': 255, - 'number_has_range': True, - }, - 'unsignedInt': { - 'type': 'number', - 'number_range_min': 0, - 'number_range_max': 4294967295, - 'number_has_range': True, - }, - 'unsignedLong': { - 'type': 'number', - 'number_range_min': 0, - 'number_range_max': 18446744073709551615, - 'number_has_range': True, - }, - 'unsignedShort': { - 'type': 'number', - 'number_range_min': 0, - 'number_range_max': 65535, - 'number_has_range': True, - }, + "MeasurementType": { + "type": "enum", + "enum_values": ["Air_Quality", "Temperature"], + }, + "Unit": { + "type": "enum", + "enum_values": ["Celsius", "Kelvin", "Relative_Humidity"], + }, + "rational": { + "type": "number", + "number_decimal_allowed": True, + }, + "real": { + "type": "number", + }, + "PlainLiteral": { + "type": "string", + }, + "XMLLiteral": { + "type": "string", + }, + "Literal": { + "type": "string", + }, + "anyURI": { + "type": "string", + }, + "base64Binary": { + "type": "string", + }, + "boolean": { + "type": "enum", + "enum_values": ["True", "False"], + }, + "byte": { + "type": "number", + "number_range_min": -128, + "number_range_max": 127, + "number_has_range": True, + }, + "dateTime": { + "type": "date", + }, + "dateTimeStamp": { + "type": "date", + }, + "decimal": { + "type": "number", + "number_decimal_allowed": True, + }, + "double": { + "type": "number", + "number_decimal_allowed": True, + }, + "float": { + "type": "number", + "number_decimal_allowed": True, + }, + "hexBinary": { + "allowed_chars": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + ], + "type": "string", + }, + "int": { + "type": "number", + "number_range_min": -2147483648, + "number_range_max": 2147483647, + "number_has_range": True, + }, + "integer": { + "type": "number", + }, + "language": { + "type": "string", + }, + "long": { + "type": "number", + "number_range_min": -9223372036854775808, + "number_range_max": 9223372036854775807, + "number_has_range": True, + }, + "Name": { + "type": "string", + }, + "NCName": { + "forbidden_chars": [":"], + "type": "string", + }, + "negativeInteger": { + "type": "number", + "number_range_max": -1, + "number_has_range": True, + }, + "NMTOKEN": { + "type": "string", + }, + "nonNegativeInteger": { + "type": "number", + "number_range_min": 0, + "number_has_range": True, + }, + "nonPositiveInteger": { + "type": "number", + "number_range_max": -1, + "number_has_range": True, + }, + "normalizedString": { + "type": "string", + }, + "positiveInteger": { + "type": "number", + "number_range_min": 0, + "number_has_range": True, + }, + "short": { + "type": "number", + "number_range_min": -32768, + "number_range_max": 32767, + "number_has_range": True, + }, + "string": { + "type": "string", + }, + "token": { + "type": "string", + }, + "unsignedByte": { + "type": "number", + "number_range_min": 0, + "number_range_max": 255, + "number_has_range": True, + }, + "unsignedInt": { + "type": "number", + "number_range_min": 0, + "number_range_max": 4294967295, + "number_has_range": True, + }, + "unsignedLong": { + "type": "number", + "number_range_min": 0, + "number_range_max": 18446744073709551615, + "number_has_range": True, + }, + "unsignedShort": { + "type": "number", + "number_range_min": 0, + "number_range_max": 65535, + "number_has_range": True, + }, } class MeasurementType(str, Enum): - value_Air_Quality = 'Air_Quality' - value_Temperature = 'Temperature' + value_Air_Quality = "Air_Quality" + value_Temperature = "Temperature" class Unit(str, Enum): - value_Celsius = 'Celsius' - value_Kelvin = 'Kelvin' - value_Relative_Humidity = 'Relative_Humidity' + value_Celsius = "Celsius" + value_Kelvin = "Kelvin" + value_Relative_Humidity = "Relative_Humidity" # ---------Class Dict--------- # semantic_manager.class_catalogue = { - 'AirProducer': AirProducer, - 'Building': Building, - 'Circuit': Circuit, - 'ColdProducer': ColdProducer, - 'Floor': Floor, - 'HeatProducer': HeatProducer, - 'Outlet': Outlet, - 'Producer': Producer, - 'Room': Room, - 'Sensor': Sensor, - 'Tenant': Tenant, - 'Thing': Thing, - } + "AirProducer": AirProducer, + "Building": Building, + "Circuit": Circuit, + "ColdProducer": ColdProducer, + "Floor": Floor, + "HeatProducer": HeatProducer, + "Outlet": Outlet, + "Producer": Producer, + "Room": Room, + "Sensor": Sensor, + "Tenant": Tenant, + "Thing": Thing, +} semantic_manager.individual_catalogue = { - 'ExampleIndividual': ExampleIndividual, - } + "ExampleIndividual": ExampleIndividual, +} diff --git a/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_model_example.py b/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_model_example.py index 2296c46c..68dbc80f 100644 --- a/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_model_example.py +++ b/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_model_example.py @@ -6,16 +6,16 @@ # Following the fiware state will be edited with automatic partial loading """ - """ To run this example you need a working Fiware v2 setup with a context-broker and an iota-broker. You can here set the addresses: """ from filip.config import settings + cb_url = settings.CB_URL iota_url = settings.IOTA_URL -if __name__ == '__main__': +if __name__ == "__main__": # # 0 Clean up Fiware state: # @@ -73,11 +73,13 @@ from filip.semantics.semantics_models import InstanceHeader from filip.models.base import NgsiVersion - header = InstanceHeader(cb_url=cb_url, - iota_url=iota_url, - ngsi_version=NgsiVersion.v2, - service="example", - service_path="/") + header = InstanceHeader( + cb_url=cb_url, + iota_url=iota_url, + ngsi_version=NgsiVersion.v2, + service="example", + service_path="/", + ) my_floor = Floor(id="my-first-floor", header=header) @@ -209,8 +211,10 @@ # we can check again if all DataFields are now valid: print("\u0332".join("DataFields of my_building:")) for field in my_building.get_data_fields(): - print(f"Name: {field.name}, Rule: {field.rule}, Values: " - f"{field.get_all_raw()}, Valid: {field.is_valid()}") + print( + f"Name: {field.name}, Rule: {field.rule}, Values: " + f"{field.get_all_raw()}, Valid: {field.is_valid()}" + ) print("") # Datafields can also specify rules with Datatype enums, in that case we @@ -237,8 +241,10 @@ # we can check again if all RelationFields are now valid: print("\u0332".join("RelationFields of my_building:")) for field in my_building.get_relation_fields(): - print(f"Name: {field.name}, Rule: {field.rule}, Values: " - f"{field.get_all_raw()}, Valid: {field.is_valid()}") + print( + f"Name: {field.name}, Rule: {field.rule}, Values: " + f"{field.get_all_raw()}, Valid: {field.is_valid()}" + ) print("") # As we can see the RelationField internally only saves the references to @@ -320,10 +326,11 @@ # Our sensor has one measurement property that he names internally m # We add this attribute to our measurement_field. - from filip.semantics.semantics_models import \ - DeviceAttribute, DeviceAttributeType + from filip.semantics.semantics_models import DeviceAttribute, DeviceAttributeType + my_sensor.measurement.add( - DeviceAttribute(name="m", attribute_type=DeviceAttributeType.active)) + DeviceAttribute(name="m", attribute_type=DeviceAttributeType.active) + ) # To access the value of the attribute we could call: # my_sensor.measurement[0].get_value() @@ -339,6 +346,7 @@ # A command has 1 property: # - name: The internal name that the specific device uses for this purpose from filip.semantics.semantics_models import Command + c1 = Command(name="open") my_outlet.controlCommand.add(c1) @@ -367,8 +375,7 @@ # instance. He can give the instance a name, and leave a comment my_floor.metadata.name = "First Basement" - my_floor.metadata.comment = "The first basement is directly below the " \ - "ground" + my_floor.metadata.comment = "The first basement is directly below the " "ground" # # 4 State Management # @@ -417,12 +424,13 @@ # we can use the corresponding functions in the semantic_manager: from filip.models.base import FiwareHeader + semantic_manager.load_instances_from_fiware( fiware_version=NgsiVersion.v2, fiware_header=FiwareHeader(service="Example", service_path="/"), cb_url=cb_url, iota_url=iota_url, - entity_ids=[], # list of ids to load + entity_ids=[], # list of ids to load # or entity_types=[...], # list of types to load ) @@ -435,8 +443,10 @@ # We can check this for each instance by calling: print("\u0332".join("Check instance validity:")) for instance in semantic_manager.get_all_local_instances(): - print(f'({instance.get_type()}, {instance.id}) is valid: ' - f'{instance.is_valid()}') + print( + f"({instance.get_type()}, {instance.id}) is valid: " + f"{instance.is_valid()}" + ) print("") # if we want to save the LocalState to fiware with an invalid instance @@ -535,9 +545,9 @@ # ### 5.2.1 Generate Data # # To generate a representation of the current local state simply call: - [elements, stylesheet] = \ - semantic_manager.generate_cytoscape_for_local_state( - display_only_used_individuals=True) + [elements, stylesheet] = semantic_manager.generate_cytoscape_for_local_state( + display_only_used_individuals=True + ) # As a result you receive a Tuple containing the graph elements (nodes, # edges) and a stylesheet. (For more details refer to the methode diff --git a/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_vocabulary_example.py b/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_vocabulary_example.py index 6c321d71..d5b27e3e 100644 --- a/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_vocabulary_example.py +++ b/examples/ngsi_v2/e11_ngsi_v2_semantics/semantics_vocabulary_example.py @@ -4,12 +4,13 @@ # the vocabulary configured # and exported as a python model file """ + from filip.semantics.vocabulary import DataFieldType from filip.semantics.vocabulary.vocabulary import VocabularySettings from filip.semantics.vocabulary_configurator import VocabularyConfigurator -if __name__ == '__main__': +if __name__ == "__main__": # # 1 Creating a new vocabulary # @@ -36,7 +37,7 @@ pascal_case_individual_labels=True, camel_case_property_labels=True, camel_case_datatype_labels=True, - pascal_case_datatype_enum_labels=True + pascal_case_datatype_enum_labels=True, ) # We create our new blank vocabulary: vocabulary = VocabularyConfigurator.create_vocabulary(settings=settings) @@ -49,20 +50,16 @@ # We always get a new vocabulary object returned # ### 2.0.1 as file - vocabulary = \ - VocabularyConfigurator.add_ontology_to_vocabulary_as_file( - vocabulary=vocabulary, - path_to_file='ontology_files/building circuits.owl') + vocabulary = VocabularyConfigurator.add_ontology_to_vocabulary_as_file( + vocabulary=vocabulary, path_to_file="ontology_files/building circuits.owl" + ) # ### 2.0.2 as string - with open('ontology_files/ParsingTesterOntology.ttl', 'r') as file: + with open("ontology_files/ParsingTesterOntology.ttl", "r") as file: data = file.read() - vocabulary = \ - VocabularyConfigurator.add_ontology_to_vocabulary_as_string( - vocabulary=vocabulary, - source_content=data, - source_name="ParsingExample" - ) + vocabulary = VocabularyConfigurator.add_ontology_to_vocabulary_as_string( + vocabulary=vocabulary, source_content=data, source_name="ParsingExample" + ) # ### 2.0.3 as link # @@ -70,11 +67,9 @@ # here: "saref.tll". Only for demonstration of the function, we do not need # saref in our example vocabulary - vocabulary = \ - VocabularyConfigurator.add_ontology_to_vocabulary_as_link( - vocabulary=vocabulary, - link="https://ontology.tno.nl/saref.ttl" - ) + vocabulary = VocabularyConfigurator.add_ontology_to_vocabulary_as_link( + vocabulary=vocabulary, link="https://ontology.tno.nl/saref.ttl" + ) # ## 2.1 Inspect added sources # @@ -84,8 +79,10 @@ # Here we print out the names and adding time of all contained sources: print("\u0332".join("Sources in vocabulary:")) for source in vocabulary.get_source_list(): - print(f'Name: {source.source_name}; Added: {source.timestamp}; ' - f'Id: {source.id}') + print( + f"Name: {source.source_name}; Added: {source.timestamp}; " + f"Id: {source.id}" + ) print("") # ## 2.2 Predefined source @@ -100,7 +97,7 @@ # To see if an ontology could be parsed completely we have to look at # the parsing logs: print("\u0332".join("Parsing Logs of vocabulary:")) - print(f'{VocabularyConfigurator.get_parsing_logs(vocabulary)}\n') + print(f"{VocabularyConfigurator.get_parsing_logs(vocabulary)}\n") # Here we see that a statement in the ParsingTesterOntology was dropped # as it was in a non supported OR format. The semantic logic is not # compatible with the OR combination of two relations. @@ -113,12 +110,15 @@ # the parsing logs. We could also look it up in the vocabulary as shown # above - source_id = [source.id for source in vocabulary.get_source_list() if - source.source_name == "ParsingExample"][0] + source_id = [ + source.id + for source in vocabulary.get_source_list() + if source.source_name == "ParsingExample" + ][0] vocabulary = VocabularyConfigurator.delete_source_from_vocabulary( - vocabulary=vocabulary, - source_id=source_id) + vocabulary=vocabulary, source_id=source_id + ) # # 3 Completeness # @@ -187,7 +187,7 @@ # To get the used label of an entity always use .get_label() # The easiest way to access an entity is the get_entity_by_iri function - entity = vocabulary.get_entity_by_iri('https://w3id.org/saref#Sensor') + entity = vocabulary.get_entity_by_iri("https://w3id.org/saref#Sensor") entity.set_label("SarefSensor") # # 6 IoT Devices @@ -218,7 +218,7 @@ # To see a list of all our available data-properties we can use: print("\u0332".join("Available Data-properties:")) for prop_iri, prop in vocabulary.data_properties.items(): - print(f'Label: {prop.get_label()}, Iri: {prop.iri}') + print(f"Label: {prop.get_label()}, Iri: {prop.iri}") print("") # This logic is the same if we want to look into classes, # object-properties, datatypes or individuals of our vocabulary. @@ -228,20 +228,20 @@ # We access the wanted properties over the specialised getter, # the general getter, or directly vocabulary.get_data_property( - "http://www.semanticweb.org/building#controlCommand").field_type = \ - DataFieldType.command + "http://www.semanticweb.org/building#controlCommand" + ).field_type = DataFieldType.command vocabulary.get_entity_by_iri( - "http://www.semanticweb.org/building#measurement").field_type = \ - DataFieldType.device_attribute + "http://www.semanticweb.org/building#measurement" + ).field_type = DataFieldType.device_attribute vocabulary.get_entity_by_iri( - "http://www.semanticweb.org/building#state").field_type = \ - DataFieldType.device_attribute + "http://www.semanticweb.org/building#state" + ).field_type = DataFieldType.device_attribute # To see which classes are now device classes we can use: print("\u0332".join("Device Classes:")) for class_ in vocabulary.get_classes(): if class_.is_iot_class(vocabulary=vocabulary): - print(f'Label: {class_.get_label()}, Iri: {class_.iri}') + print(f"Label: {class_.get_label()}, Iri: {class_.iri}") print("") # # 7 Exporting Vocabulary to models @@ -263,15 +263,16 @@ # The saref ontology was good to display some functions of the vocabulary # configurator, but to simplify the next example we remove it here - source_id = [source.id for source in vocabulary.get_source_list() if - source.source_name == "saref.ttl"][0] + source_id = [ + source.id + for source in vocabulary.get_source_list() + if source.source_name == "saref.ttl" + ][0] vocabulary = VocabularyConfigurator.delete_source_from_vocabulary( - vocabulary=vocabulary, - source_id=source_id) + vocabulary=vocabulary, source_id=source_id + ) # The export function takes two arguments: path_to_file and file_name # it creates the file: path_to_file/file_name.py overriding any existing # file - VocabularyConfigurator.generate_vocabulary_models(vocabulary, "", - "models") - + VocabularyConfigurator.generate_vocabulary_models(vocabulary, "", "models") diff --git a/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py b/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py index d5bd783d..4695d84c 100644 --- a/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py +++ b/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py @@ -7,6 +7,7 @@ # In short: this workflow shows you a way to keep use case model simple and # reusable while ensuring the compatability with FIWARE NGSI-V2 standards """ + from typing import Optional from pydantic import ConfigDict, BaseModel from pydantic.fields import Field, FieldInfo @@ -16,16 +17,16 @@ from filip.utils.cleanup import clear_context_broker from pprint import pprint from filip.config import settings + # Host address of Context Broker CB_URL = settings.CB_URL # You can here also change the used Fiware service # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Servicepath -SERVICE_PATH = '/' -fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) +SERVICE_PATH = "/" +fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # Reuse existing data model from the internet @@ -52,7 +53,7 @@ class PostalAddress(BaseModel): alias="addressLocality", default=None, description="The locality in which the street address is, and which is " - "in the region. For example, Mountain View.", + "in the region. For example, Mountain View.", ) postal_code: str = Field( alias="postalCode", @@ -105,8 +106,7 @@ class WeatherStationFIWARE(WeatherStation, ContextEntityKeyValues): # Workflow to utilize these data models. # 0. Initial client - cb_client = ContextBrokerClient(url=CB_URL, - fiware_header=fiware_header) + cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # clear cb clear_context_broker(cb_client=cb_client, fiware_header=fiware_header, url=CB_URL) @@ -121,8 +121,7 @@ class WeatherStationFIWARE(WeatherStation, ContextEntityKeyValues): "postal_code": 52072, }, ) - cb_client.post_entity(entity=weather_station, key_values=True, - update=True) + cb_client.post_entity(entity=weather_station, key_values=True, update=True) # 2. Update data weather_station.temperature = 30 # represent use case algorithm @@ -130,8 +129,9 @@ class WeatherStationFIWARE(WeatherStation, ContextEntityKeyValues): # 3. Query and validate data # represent querying data by data users - weather_station_data = cb_client.get_entity(entity_id="myWeatherStation", - response_format="keyValues") + weather_station_data = cb_client.get_entity( + entity_id="myWeatherStation", response_format="keyValues" + ) # validate with general model weather_station_2_general = WeatherStation.model_validate( weather_station_data.model_dump() @@ -143,15 +143,21 @@ class WeatherStationFIWARE(WeatherStation, ContextEntityKeyValues): # 4. Use data for different purposes # for use case specific usage - print("Data complied with general model can be forwarded to other platform/system:\n" - f"{weather_station_2_general.model_dump_json(indent=2)}") - print(f"For example, address still comply with existing model:\n" - f"{weather_station_2_general.address.model_dump_json(indent=2)}\n") + print( + "Data complied with general model can be forwarded to other platform/system:\n" + f"{weather_station_2_general.model_dump_json(indent=2)}" + ) + print( + f"For example, address still comply with existing model:\n" + f"{weather_station_2_general.address.model_dump_json(indent=2)}\n" + ) # for fiware specific usage - print("For usage within FIWARE system, id and type is helpful, e.g. for creating" - "notification for entity:\n" - f"{weather_station_2_fiware.model_dump_json(indent=2, include={'id', 'type'})}\n") + print( + "For usage within FIWARE system, id and type is helpful, e.g. for creating" + "notification for entity:\n" + f"{weather_station_2_fiware.model_dump_json(indent=2, include={'id', 'type'})}\n" + ) # clear cb clear_context_broker(cb_client=cb_client, fiware_header=fiware_header, url=CB_URL) diff --git a/examples/ngsi_v2/e13_ngsi_v2_secure_fiware_headers.py b/examples/ngsi_v2/e13_ngsi_v2_secure_fiware_headers.py index 89ac913a..417aed22 100644 --- a/examples/ngsi_v2/e13_ngsi_v2_secure_fiware_headers.py +++ b/examples/ngsi_v2/e13_ngsi_v2_secure_fiware_headers.py @@ -2,6 +2,7 @@ # This example shows how to generate an access token from an authnetication srever in order to access Fiware services # which are protected behind an authentication/authorisation layer. """ + import os import requests @@ -15,14 +16,14 @@ session = requests.Session() CB_URL = settings.CB_URL # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Servicepath -SERVICE_PATH = '/' +SERVICE_PATH = "/" # Provide client credentials which are used when generating access token from authentication server -CLIENT_ID = 'client_id' -CLIENT_SECRET = 'client_secret' +CLIENT_ID = "client_id" +CLIENT_SECRET = "client_secret" # TODO: Please adapt it according to your authentication server which is generating access token for the service -KEYCLOAK_HOST = 'https://keycloak.example.com' +KEYCLOAK_HOST = "https://keycloak.example.com" class KeycloakPython: @@ -36,66 +37,109 @@ def __init__(self, keycloak_host=None, client_id=None, client_secret=None): # self.keycloak_host = os.getenv('KEYCLOAK_HOST') # else: # self.keycloak_host = keycloak_host - self.keycloak_host = os.getenv('KEYCLOAK_HOST') if keycloak_host == None else keycloak_host - self.client_id = os.getenv('CLIENT_ID') if client_id == None else client_id - self.client_secret = os.getenv('CLIENT_SECRET') if client_secret == None else client_secret + self.keycloak_host = ( + os.getenv("KEYCLOAK_HOST") if keycloak_host == None else keycloak_host + ) + self.client_id = os.getenv("CLIENT_ID") if client_id == None else client_id + self.client_secret = ( + os.getenv("CLIENT_SECRET") if client_secret == None else client_secret + ) def get_access_token(self, keycloak_host=None, client_id=None, client_secret=None): """ - Get access token for a given client id and client secret. """ - self.keycloak_host = keycloak_host if keycloak_host != None else self.keycloak_host + self.keycloak_host = ( + keycloak_host if keycloak_host != None else self.keycloak_host + ) self.client_id = client_id if client_id != None else self.client_id - self.client_secret = client_secret if client_secret != None else self.client_secret - self.data = {'client_id': self.client_id, - 'client_secret': self.client_secret, - 'scope': 'email', - 'grant_type': 'client_credentials', - } + self.client_secret = ( + client_secret if client_secret != None else self.client_secret + ) + self.data = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "scope": "email", + "grant_type": "client_credentials", + } try: headers = {"content-type": "application/x-www-form-urlencoded"} - access_data = requests.post(self.keycloak_host, data=self.data, headers=headers) - expires_in = access_data.json()['expires_in'] - access_token = access_data.json()['access_token'] + access_data = requests.post( + self.keycloak_host, data=self.data, headers=headers + ) + expires_in = access_data.json()["expires_in"] + access_token = access_data.json()["access_token"] return access_token, expires_in except requests.exceptions.RequestException as err: raise KeycloakPythonException(err.args[0]) - def get_data(self, client_host, headers={}, keycloak_host=None, client_id=None, client_secret=None): + def get_data( + self, + client_host, + headers={}, + keycloak_host=None, + client_id=None, + client_secret=None, + ): """ - Get data for a given api. - Mandatory input - Target api, fiware-service and fiware-servicepath headers - Optional Inmput - Keycloak host, Client ID, Client Secret """ - access_token, expires_in = self.get_access_token(keycloak_host=keycloak_host, client_id=client_id, - client_secret=client_secret) - headers['Authorization'] = 'Bearer %s' % (access_token) + access_token, expires_in = self.get_access_token( + keycloak_host=keycloak_host, + client_id=client_id, + client_secret=client_secret, + ) + headers["Authorization"] = "Bearer %s" % (access_token) response = requests.get(client_host, headers=headers) return response.text - def post_data(self, client_host, data, headers={}, keycloak_host=None, client_id=None, client_secret=None): + def post_data( + self, + client_host, + data, + headers={}, + keycloak_host=None, + client_id=None, + client_secret=None, + ): """ - Post data for a given api. - Mandatory input - Target api, headers, request body. - Optional Inmput - Keycloak host, Client ID, Client Secret. """ - access_token, expires_in = self.get_access_token(keycloak_host=keycloak_host, client_id=client_id, - client_secret=client_secret) - headers['Content-Type'] = 'application/json' - headers['Authorization'] = 'Bearer %s' % (access_token) + access_token, expires_in = self.get_access_token( + keycloak_host=keycloak_host, + client_id=client_id, + client_secret=client_secret, + ) + headers["Content-Type"] = "application/json" + headers["Authorization"] = "Bearer %s" % (access_token) response = requests.post(client_host, data=data, headers=headers) return response - def patch_data(self, client_host, json, headers={}, keycloak_host=None, client_id=None, client_secret=None): + def patch_data( + self, + client_host, + json, + headers={}, + keycloak_host=None, + client_id=None, + client_secret=None, + ): """ - Patch data for a given api. - Mandatory input - Target api, headers, request body. - Optional Inmput - Keycloak host, Client ID, Client Secret. """ - access_token, expires_in = self.get_access_token(keycloak_host=keycloak_host, client_id=client_id, - client_secret=client_secret) - headers['Content-Type'] = 'application/json' - headers['Authorization'] = 'Bearer %s' % (access_token) + access_token, expires_in = self.get_access_token( + keycloak_host=keycloak_host, + client_id=client_id, + client_secret=client_secret, + ) + headers["Content-Type"] = "application/json" + headers["Authorization"] = "Bearer %s" % (access_token) response = requests.patch(url=client_host, json=json, headers=headers) return response @@ -108,14 +152,18 @@ def __init__(self, message): if __name__ == "__main__": # get token from keycloak - token, in_sec = KeycloakPython(KEYCLOAK_HOST, CLIENT_ID, CLIENT_SECRET).get_access_token() + token, in_sec = KeycloakPython( + KEYCLOAK_HOST, CLIENT_ID, CLIENT_SECRET + ).get_access_token() # create secure fiware header with authorisation token - fiware_header = FiwareHeaderSecure(service=SERVICE, - service_path=SERVICE_PATH, - authorization='Bearer %s' % token) + fiware_header = FiwareHeaderSecure( + service=SERVICE, service_path=SERVICE_PATH, authorization="Bearer %s" % token + ) # create a context broker client - cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header, session=session) + cb_client = ContextBrokerClient( + url=CB_URL, fiware_header=fiware_header, session=session + ) # you don't need to set any extra parameter for requesting the service besides setting session in the client object entity_list = cb_client.get_entity_list() diff --git a/examples/ngsi_v2/e14_ngsi_v2_notification_based_command.py b/examples/ngsi_v2/e14_ngsi_v2_notification_based_command.py index f4d3cbc9..eac00827 100644 --- a/examples/ngsi_v2/e14_ngsi_v2_notification_based_command.py +++ b/examples/ngsi_v2/e14_ngsi_v2_notification_based_command.py @@ -15,6 +15,7 @@ object. With the customization, you can simplify the payload format to a simple string. More information: https://fiware-orion.readthedocs.io/en/3.8.0/orion-api.html#custom-notifications """ + import json import time @@ -34,32 +35,31 @@ MQTT_BROKER_URL = settings.MQTT_BROKER_URL # FIWARE-Service -SERVICE = 'filip' +SERVICE = "filip" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # Setting up logging logging.basicConfig( - level='INFO', - format='%(asctime)s %(name)s %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + level="INFO", + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) logger = logging.getLogger(__name__) -def set_up_mqtt_actuator(normal_topic: str, - custom_topic: str): +def set_up_mqtt_actuator(normal_topic: str, custom_topic: str): """ This function sets up the MQTT actuator """ + def on_connect(client, userdata, flags, reasonCode, properties=None): if reasonCode != 0: - logger.error(f"Connection failed with error code: " - f"'{reasonCode}'") + logger.error(f"Connection failed with error code: " f"'{reasonCode}'") raise ConnectionError else: - logger.info("Successfully, connected with result code " + str( - reasonCode)) + logger.info("Successfully, connected with result code " + str(reasonCode)) client.subscribe(mqtt_topic) client.subscribe(mqtt_topic_custom) @@ -67,8 +67,7 @@ def on_subscribe(client, userdata, mid, granted_qos, properties=None): logger.info("Successfully subscribed to with QoS: %s", granted_qos) def on_message(client, userdata, msg): - logger.info("Receive MQTT command: " + msg.topic + " " + str( - msg.payload)) + logger.info("Receive MQTT command: " + msg.topic + " " + str(msg.payload)) if msg.topic == normal_topic: data = json.loads(msg.payload) data_pretty = json.dumps(data, indent=2) @@ -80,13 +79,14 @@ def on_message(client, userdata, msg): print(f"Turn heat power to: {msg.payload.decode()}") def on_disconnect(client, userdata, flags, reasonCode, properties=None): - logger.info("MQTT client disconnected with reasonCode " - + str(reasonCode)) - - mqtt_client = mqtt.Client(userdata=None, - protocol=mqtt.MQTTv5, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - transport="tcp") + logger.info("MQTT client disconnected with reasonCode " + str(reasonCode)) + + mqtt_client = mqtt.Client( + userdata=None, + protocol=mqtt.MQTTv5, + callback_api_version=mqtt.CallbackAPIVersion.VERSION2, + transport="tcp", + ) # add callbacks to the client mqtt_client.on_connect = on_connect mqtt_client.on_subscribe = on_subscribe @@ -97,25 +97,20 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): def create_subscription( - cbc: ContextBrokerClient, - entity_id: str, - notification: Notification): - sub = Subscription.model_validate({ - "description": "Test mqtt custom notification with payload message", - "subject": { - "entities": [ - { - "id": entity_id - } - ] - }, - "notification": notification.model_dump(), - "throttling": 0 - }) + cbc: ContextBrokerClient, entity_id: str, notification: Notification +): + sub = Subscription.model_validate( + { + "description": "Test mqtt custom notification with payload message", + "subject": {"entities": [{"id": entity_id}]}, + "notification": notification.model_dump(), + "throttling": 0, + } + ) cbc.post_subscription(sub) -if __name__ == '__main__': +if __name__ == "__main__": # FIWARE header fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) @@ -124,7 +119,7 @@ def create_subscription( clear_context_broker(url=CB_URL, fiware_header=fiware_header) # Set up a dummy MQTT actuator - mqtt_topic = "actuator/1" # Topic for the actuator 1 + mqtt_topic = "actuator/1" # Topic for the actuator 1 mqtt_topic_custom = "actuator/2" # Topic for the actuator 2 (custom payload) actuator_client = set_up_mqtt_actuator(mqtt_topic, mqtt_topic_custom) @@ -132,37 +127,34 @@ def create_subscription( actuator_client.loop_start() # Create entities for the actuators - actuator_1 = ContextEntity(id="actuator_1", type="Actuator", - toggle={ - "type": "Boolean", - "value": False - }) + actuator_1 = ContextEntity( + id="actuator_1", type="Actuator", toggle={"type": "Boolean", "value": False} + ) cb_client.post_entity(actuator_1) - actuator_2 = ContextEntity(id="actuator_2", type="Actuator", - heatPower={ - "type": "Number", - "value": 0 - }) + actuator_2 = ContextEntity( + id="actuator_2", type="Actuator", heatPower={"type": "Number", "value": 0} + ) cb_client.post_entity(actuator_2) # Create a notification for the actuator 1 with normal payload notification_normal = Notification( - mqtt={ - "url": MQTT_BROKER_URL, "topic": mqtt_topic - }, - attrs=["toggle"]) - create_subscription(cb_client, - entity_id=actuator_1.id, - notification=notification_normal) + mqtt={"url": MQTT_BROKER_URL, "topic": mqtt_topic}, attrs=["toggle"] + ) + create_subscription( + cb_client, entity_id=actuator_1.id, notification=notification_normal + ) # Create a subscription for the actuator 2 with custom payload notification_custom = Notification( mqttCustom={ - "url": MQTT_BROKER_URL, "topic": mqtt_topic_custom, - "payload": "${heatPower}"}) - create_subscription(cb_client, - entity_id=actuator_2.id, - notification=notification_custom) + "url": MQTT_BROKER_URL, + "topic": mqtt_topic_custom, + "payload": "${heatPower}", + } + ) + create_subscription( + cb_client, entity_id=actuator_2.id, notification=notification_custom + ) # Send command to actuator 1 actuator_1.toggle.value = True diff --git a/filip/__init__.py b/filip/__init__.py index 4d600ea4..3c0d91b2 100644 --- a/filip/__init__.py +++ b/filip/__init__.py @@ -1,7 +1,8 @@ """ filip-Module. See readme or documentation for more information. """ + from filip.config import settings from filip.clients.ngsi_v2 import HttpClient -__version__ = '0.6.0' +__version__ = "0.6.0" diff --git a/filip/clients/__init__.py b/filip/clients/__init__.py index f44f83f4..32eb46c5 100644 --- a/filip/clients/__init__.py +++ b/filip/clients/__init__.py @@ -1,3 +1,3 @@ """ Clients to interact with FIWARE's APIs -""" \ No newline at end of file +""" diff --git a/filip/clients/base_http_client.py b/filip/clients/base_http_client.py index b43337c6..f83acee0 100644 --- a/filip/clients/base_http_client.py +++ b/filip/clients/base_http_client.py @@ -1,6 +1,7 @@ """ Base http client module """ + import logging from pydantic import AnyHttpUrl from typing import Dict, ByteString, List, IO, Tuple, Union @@ -14,6 +15,7 @@ class NgsiURLVersion(str, Enum): """ URL part that defines the NGSI version for the API. """ + v2_url = "/v2" ld_url = "/ngsi-ld/v1" @@ -30,15 +32,17 @@ class BaseHttpClient: **kwargs: Optional arguments that ``request`` takes. """ - def __init__(self, - url: Union[AnyHttpUrl, str] = None, - *, - session: requests.Session = None, - fiware_header: Union[Dict, FiwareHeader, FiwareLDHeader] = None, - **kwargs): - - self.logger = logging.getLogger( - name=f"{self.__class__.__name__}") + + def __init__( + self, + url: Union[AnyHttpUrl, str] = None, + *, + session: requests.Session = None, + fiware_header: Union[Dict, FiwareHeader, FiwareLDHeader] = None, + **kwargs, + ): + + self.logger = logging.getLogger(name=f"{self.__class__.__name__}") self.logger.addHandler(logging.NullHandler()) self.logger.debug("Creating %s", self.__class__.__name__) @@ -58,7 +62,7 @@ def __init__(self, else: self.fiware_headers = fiware_header - self.headers.update(kwargs.pop('headers', {})) + self.headers.update(kwargs.pop("headers", {})) self.kwargs: Dict = kwargs # Context Manager Protocol @@ -111,7 +115,7 @@ def fiware_headers(self, headers: Union[Dict, FiwareHeader]) -> None: elif isinstance(headers, str): self._fiware_headers = FiwareLDHeader.parse_raw(headers) else: - raise TypeError(f'Invalid headers! {type(headers)}') + raise TypeError(f"Invalid headers! {type(headers)}") self.headers.update(self.fiware_headers.model_dump(by_alias=True)) @property @@ -170,10 +174,9 @@ def headers(self): return self._headers # modification to requests api - def get(self, - url: str, - params: Union[Dict, List[Tuple], ByteString] = None, - **kwargs) -> requests.Response: + def get( + self, url: str, params: Union[Dict, List[Tuple], ByteString] = None, **kwargs + ) -> requests.Response: """ Sends a GET request either using the provided session or the single session. @@ -188,8 +191,7 @@ def get(self, requests.Response """ - kwargs.update({k: v for k, v in self.kwargs.items() - if k not in kwargs.keys()}) + kwargs.update({k: v for k, v in self.kwargs.items() if k not in kwargs.keys()}) if self.session: return self.session.get(url=url, params=params, **kwargs) @@ -207,16 +209,15 @@ def options(self, url: str, **kwargs) -> requests.Response: Returns: requests.Response """ - kwargs.update({k: v for k, v in self.kwargs.items() - if k not in kwargs.keys()}) + kwargs.update({k: v for k, v in self.kwargs.items() if k not in kwargs.keys()}) if self.session: return self.session.options(url=url, **kwargs) return requests.options(url=url, **kwargs) - def head(self, url: str, - params: Union[Dict, List[Tuple], ByteString] = None, - **kwargs) -> requests.Response: + def head( + self, url: str, params: Union[Dict, List[Tuple], ByteString] = None, **kwargs + ) -> requests.Response: """ Sends a HEAD request either using the provided session or the single session. @@ -230,18 +231,19 @@ def head(self, url: str, Returns: requests.Response """ - kwargs.update({k: v for k, v in self.kwargs.items() - if k not in kwargs.keys()}) + kwargs.update({k: v for k, v in self.kwargs.items() if k not in kwargs.keys()}) if self.session: return self.session.head(url=url, params=params, **kwargs) return requests.head(url=url, params=params, **kwargs) - def post(self, - url: str, - data: Union[Dict, ByteString, List[Tuple], IO, str] = None, - json: Dict = None, - **kwargs) -> requests.Response: + def post( + self, + url: str, + data: Union[Dict, ByteString, List[Tuple], IO, str] = None, + json: Dict = None, + **kwargs, + ) -> requests.Response: """ Sends a POST request either using the provided session or the single session. @@ -257,18 +259,19 @@ def post(self, Returns: """ - kwargs.update({k: v for k, v in self.kwargs.items() - if k not in kwargs.keys()}) + kwargs.update({k: v for k, v in self.kwargs.items() if k not in kwargs.keys()}) if self.session: return self.session.post(url=url, data=data, json=json, **kwargs) return requests.post(url=url, data=data, json=json, **kwargs) - def put(self, - url: str, - data: Union[Dict, ByteString, List[Tuple], IO, str] = None, - json: Dict = None, - **kwargs) -> requests.Response: + def put( + self, + url: str, + data: Union[Dict, ByteString, List[Tuple], IO, str] = None, + json: Dict = None, + **kwargs, + ) -> requests.Response: """ Sends a PUT request either using the provided session or the single session. @@ -285,18 +288,19 @@ def put(self, Returns: request.Response """ - kwargs.update({k: v for k, v in self.kwargs.items() - if k not in kwargs.keys()}) + kwargs.update({k: v for k, v in self.kwargs.items() if k not in kwargs.keys()}) if self.session: return self.session.put(url=url, data=data, json=json, **kwargs) return requests.put(url=url, data=data, json=json, **kwargs) - def patch(self, - url: str, - data: Union[Dict, ByteString, List[Tuple], IO, str] = None, - json: Dict = None, - **kwargs) -> requests.Response: + def patch( + self, + url: str, + data: Union[Dict, ByteString, List[Tuple], IO, str] = None, + json: Dict = None, + **kwargs, + ) -> requests.Response: """ Sends a PATCH request either using the provided session or the single session. @@ -313,8 +317,7 @@ def patch(self, Returns: request.Response """ - kwargs.update({k: v for k, v in self.kwargs.items() - if k not in kwargs.keys()}) + kwargs.update({k: v for k, v in self.kwargs.items() if k not in kwargs.keys()}) if self.session: return self.session.patch(url=url, data=data, json=json, **kwargs) @@ -332,16 +335,13 @@ def delete(self, url: str, **kwargs) -> requests.Response: Returns: request.Response """ - kwargs.update({k: v for k, v in self.kwargs.items() - if k not in kwargs.keys()}) + kwargs.update({k: v for k, v in self.kwargs.items() if k not in kwargs.keys()}) if self.session: return self.session.delete(url=url, **kwargs) return requests.delete(url=url, **kwargs) - def log_error(self, - err: requests.RequestException, - msg: str = None) -> None: + def log_error(self, err: requests.RequestException, msg: str = None) -> None: """ Outputs the error messages from the client request function. If additional information is available in the server response this will diff --git a/filip/clients/mqtt/__init__.py b/filip/clients/mqtt/__init__.py index b771ff50..a128ce5b 100644 --- a/filip/clients/mqtt/__init__.py +++ b/filip/clients/mqtt/__init__.py @@ -1,4 +1,5 @@ """ MQTT client for streaming data via FIWARE's IoT-Agent """ -from .client import IoTAMQTTClient \ No newline at end of file + +from .client import IoTAMQTTClient diff --git a/filip/clients/mqtt/client.py b/filip/clients/mqtt/client.py index cda4b4ef..ea7d72b6 100644 --- a/filip/clients/mqtt/client.py +++ b/filip/clients/mqtt/client.py @@ -2,6 +2,7 @@ Implementation of an extended MQTT client that automatically handles the topic subscription for FIWARE's IoT communication pattern. """ + import itertools import logging import warnings @@ -12,11 +13,12 @@ from filip.clients.mqtt.encoder import BaseEncoder, Json, Ultralight from filip.models.mqtt import IoTAMQTTMessageType -from filip.models.ngsi_v2.iot import \ - Device, \ - PayloadProtocol, \ - ServiceGroup, \ - TransportProtocol +from filip.models.ngsi_v2.iot import ( + Device, + PayloadProtocol, + ServiceGroup, + TransportProtocol, +) class IoTAMQTTClient(mqtt.Client): @@ -121,16 +123,18 @@ def on_command(client, obj, msg): """ - def __init__(self, - client_id="", - clean_session=None, - userdata=None, - protocol=mqtt.MQTTv311, - transport="tcp", - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - devices: List[Device] = None, - service_groups: List[ServiceGroup] = None, - custom_encoder: Dict[str, BaseEncoder] = None): + def __init__( + self, + client_id="", + clean_session=None, + userdata=None, + protocol=mqtt.MQTTv311, + transport="tcp", + callback_api_version=mqtt.CallbackAPIVersion.VERSION2, + devices: List[Device] = None, + service_groups: List[ServiceGroup] = None, + custom_encoder: Dict[str, BaseEncoder] = None, + ): """ Args: client_id: @@ -186,16 +190,17 @@ def __init__(self, essentially saves boiler plate code. """ # initialize parent client - super().__init__(client_id=client_id, - clean_session=clean_session, - userdata=userdata, - protocol=protocol, - callback_api_version=callback_api_version, - transport=transport) + super().__init__( + client_id=client_id, + clean_session=clean_session, + userdata=userdata, + protocol=protocol, + callback_api_version=callback_api_version, + transport=transport, + ) # setup logging functionality - self.logger = logging.getLogger( - name=f"{self.__class__.__name__}") + self.logger = logging.getLogger(name=f"{self.__class__.__name__}") self.logger.addHandler(logging.NullHandler()) self.enable_logger(self.logger) @@ -213,8 +218,7 @@ def __init__(self, self.devices = devices # create dict with available encoders - self._encoders = {'IoTA-JSON': Json(), - 'PDI-IoTA-UltraLight': Ultralight()} + self._encoders = {"IoTA-JSON": Json(), "PDI-IoTA-UltraLight": Ultralight()} # add custom encoder for message parsing if custom_encoder: @@ -263,8 +267,9 @@ def get_encoder(self, encoder: Union[str, PayloadProtocol]): def add_encoder(self, encoder: Dict[str, BaseEncoder]): for value in encoder.values(): - assert isinstance(value, BaseEncoder), \ - f"Encoder must be a subclass of {type(BaseEncoder)}" + assert isinstance( + value, BaseEncoder + ), f"Encoder must be a subclass of {type(BaseEncoder)}" self._encoders.update(encoder) @@ -286,25 +291,26 @@ def __validate_device(self, device: Union[Device, Dict]) -> Device: assert isinstance(device, Device), "Invalid device configuration!" - assert device.transport == TransportProtocol.MQTT, \ - "Unsupported transport protocol found in device configuration!" + assert ( + device.transport == TransportProtocol.MQTT + ), "Unsupported transport protocol found in device configuration!" if device.apikey in self.service_groups.keys(): pass # check if matching service group is registered else: - msg = "Could not find matching service group! " \ - "Communication may not work correctly!" + msg = ( + "Could not find matching service group! " + "Communication may not work correctly!" + ) self.logger.warning(msg=msg) warnings.warn(message=msg) return device - def __create_topic(self, - *, - topic_type: IoTAMQTTMessageType, - device: Device, - attribute: str = None) -> str: + def __create_topic( + self, *, topic_type: IoTAMQTTMessageType, device: Device, attribute: str = None + ) -> str: """ Creates a topic for a device configuration based on the requested topic type. @@ -332,46 +338,61 @@ def __create_topic(self, If attribute name is missing for single measurements """ if topic_type == IoTAMQTTMessageType.MULTI: - topic = '/'.join((self._encoders[device.protocol].prefix, - device.apikey, - device.device_id, - 'attrs')) + topic = "/".join( + ( + self._encoders[device.protocol].prefix, + device.apikey, + device.device_id, + "attrs", + ) + ) elif topic_type == IoTAMQTTMessageType.SINGLE: if attribute: - attr = next(attr for attr in device.attributes - if attr.name == attribute) + attr = next( + attr for attr in device.attributes if attr.name == attribute + ) if attr.object_id: attr_suffix = attr.object_id else: attr_suffix = attr.name - topic = '/'.join((self._encoders[device.protocol].prefix, - device.apikey, - device.device_id, - 'attrs', - attr_suffix)) + topic = "/".join( + ( + self._encoders[device.protocol].prefix, + device.apikey, + device.device_id, + "attrs", + attr_suffix, + ) + ) else: raise ValueError("Missing argument name for single measurement") elif topic_type == IoTAMQTTMessageType.CMD: - topic = '/' + '/'.join((device.apikey, device.device_id, 'cmd')) + topic = "/" + "/".join((device.apikey, device.device_id, "cmd")) elif topic_type == IoTAMQTTMessageType.CMDEXE: - topic = '/'.join((self._encoders[device.protocol].prefix, - device.apikey, - device.device_id, - 'cmdexe')) + topic = "/".join( + ( + self._encoders[device.protocol].prefix, + device.apikey, + device.device_id, + "cmdexe", + ) + ) elif topic_type == IoTAMQTTMessageType.CONFIG: - topic = '/'.join((self._encoders[device.protocol].prefix, - device.apikey, - device.device_id, - 'configuration')) + topic = "/".join( + ( + self._encoders[device.protocol].prefix, + device.apikey, + device.device_id, + "configuration", + ) + ) else: raise KeyError("topic_type not supported") return topic - def __subscribe_commands(self, *, - device: Device = None, - qos=0, - options=None, - properties=None): + def __subscribe_commands( + self, *, device: Device = None, qos=0, options=None, properties=None + ): """ Subscribes commands based on device configuration. If device argument is omitted the function will subscribe to all topics of already registered @@ -390,19 +411,18 @@ def __subscribe_commands(self, *, """ if Device: if len(device.commands) > 0: - topic = self.__create_topic(device=device, - topic_type=IoTAMQTTMessageType.CMD) - super().subscribe(topic=topic, - qos=qos, - options=options, - properties=properties) + topic = self.__create_topic( + device=device, topic_type=IoTAMQTTMessageType.CMD + ) + super().subscribe( + topic=topic, qos=qos, options=options, properties=properties + ) else: # call itself but with device argument for all registered _devices for device in self._devices.values(): - self.__subscribe_commands(device=device, - qos=qos, - options=options, - properties=properties) + self.__subscribe_commands( + device=device, qos=qos, options=options, properties=properties + ) def get_service_group(self, apikey: str) -> ServiceGroup: """ @@ -446,14 +466,14 @@ def add_service_group(self, service_group: Union[ServiceGroup, Dict]): """ if isinstance(service_group, dict): service_group = ServiceGroup.model_validate(service_group) - assert isinstance(service_group, ServiceGroup), \ - "Invalid content for service group!" + assert isinstance( + service_group, ServiceGroup + ), "Invalid content for service group!" if self.service_groups.get(service_group.apikey, None) is None: pass else: - raise ValueError("Service group already exists! %s", - service_group.apikey) + raise ValueError("Service group already exists! %s", service_group.apikey) # add service group configuration to the service group list self.service_groups[service_group.apikey] = service_group @@ -469,11 +489,9 @@ def delete_service_group(self, apikey): """ group = self.service_groups.pop(apikey, None) if group: - self.logger.info("Successfully unregistered Service Group '%s'!", - apikey) + self.logger.info("Successfully unregistered Service Group '%s'!", apikey) else: - self.logger.error("Could not unregister service group '%s'!", - apikey) + self.logger.error("Could not unregister service group '%s'!", apikey) raise KeyError("Device not found!") def update_service_group(self, service_group: Union[ServiceGroup, Dict]): @@ -493,12 +511,12 @@ def update_service_group(self, service_group: Union[ServiceGroup, Dict]): """ if isinstance(service_group, dict): service_group = ServiceGroup.model_validate(service_group) - assert isinstance(service_group, ServiceGroup), \ - "Invalid content for service group" + assert isinstance( + service_group, ServiceGroup + ), "Invalid content for service group" if self.service_groups.get(service_group.apikey, None) is None: - raise KeyError("Service group not found! %s", - service_group.apikey) + raise KeyError("Service group not found! %s", service_group.apikey) # add service group configuration to the service group list self.service_groups[service_group.apikey] = service_group @@ -526,11 +544,9 @@ def get_device(self, device_id: str) -> Device: """ return self._devices[device_id] - def add_device(self, - device: Union[Device, Dict], - qos=0, - options=None, - properties=None): + def add_device( + self, device: Union[Device, Dict], qos=0, options=None, properties=None + ): """ Registers a device config with the mqtt client. Subsequently, the client will magically subscribe to the corresponding topics based @@ -563,10 +579,9 @@ def add_device(self, # add device configuration to the device list self._devices[device.device_id] = device # subscribes to the command topic - self.__subscribe_commands(device=device, - qos=qos, - options=options, - properties=properties) + self.__subscribe_commands( + device=device, qos=qos, options=options, properties=properties + ) def delete_device(self, device_id: str): """ @@ -580,20 +595,18 @@ def delete_device(self, device_id: str): """ device = self._devices.pop(device_id, None) if device: - topic = self.__create_topic(device=device, - topic_type=IoTAMQTTMessageType.CMD) + topic = self.__create_topic( + device=device, topic_type=IoTAMQTTMessageType.CMD + ) self.unsubscribe(topic=topic) self.message_callback_remove(sub=topic) - self.logger.info("Successfully unregistered Device '%s'!", - device_id) + self.logger.info("Successfully unregistered Device '%s'!", device_id) else: self.logger.error("Could not unregister device '%s'", device_id) - def update_device(self, - device: Union[Device, Dict], - qos=0, - options=None, - properties=None): + def update_device( + self, device: Union[Device, Dict], qos=0, options=None, properties=None + ): """ Updates a registered device configuration. There is no opportunity to only partially update the device. Hence, your device model should @@ -619,10 +632,9 @@ def update_device(self, # update device configuration in the device list self._devices[device.device_id] = device # subscribes to the command topic - self.__subscribe_commands(device=device, - qos=qos, - options=options, - properties=properties) + self.__subscribe_commands( + device=device, qos=qos, options=options, properties=properties + ) def add_command_callback(self, device_id: str, callback: Callable): """ @@ -660,21 +672,21 @@ def on_command(client, obj, msg): if device is None: raise KeyError("Device does not exist! %s", device_id) self.__subscribe_commands(device=device) - topic = self.__create_topic(device=device, - topic_type=IoTAMQTTMessageType.CMD) + topic = self.__create_topic(device=device, topic_type=IoTAMQTTMessageType.CMD) self.message_callback_add(topic, callback) - def publish(self, - topic=None, - payload: Union[Dict, Any] = None, - qos: int = 0, - retain: bool = False, - properties=None, - device_id: str = None, - attribute_name: str = None, - command_name: str = None, - timestamp: bool = False - ): + def publish( + self, + topic=None, + payload: Union[Dict, Any] = None, + qos: int = 0, + retain: bool = False, + properties=None, + device_id: str = None, + attribute_name: str = None, + command_name: str = None, + timestamp: bool = False, + ): """ Publish an MQTT Message to a specified topic. If you want to publish a device specific message to a device use the device_id argument for @@ -741,10 +753,9 @@ def publish(self, # create message for multi measurement payload if attribute_name is None and command_name is None: - assert isinstance(payload, dict), \ - "Payload must be a dictionary" + assert isinstance(payload, dict), "Payload must be a dictionary" - if timestamp and 'timeInstant' not in payload.keys(): + if timestamp and "timeInstant" not in payload.keys(): payload["timeInstant"] = datetime.utcnow() # validate if dict keys match device configuration @@ -752,69 +763,78 @@ def publish(self, for key in payload.keys(): for attr in device.attributes: key_constraint = key == "timeInstant" - def elif_action(msg): None + + def elif_action(msg): + None if attr.object_id is not None: key_constraint = key_constraint or (key in attr.object_id) - def elif_action(msg): msg[attr.object_id] = msg.pop(key) - - #could be made more compact by pulling up the second condition - #but would probably make the code less readable... + + def elif_action(msg): + msg[attr.object_id] = msg.pop(key) + + # could be made more compact by pulling up the second condition + # but would probably make the code less readable... if key_constraint: break - + elif key == attr.name: elif_action(msg_payload) break else: - err_msg = f"Attribute key '{key}' is not allowed " \ - f"in the message payload for this " \ - f"device configuration with device_id " \ - f"'{device_id}'" + err_msg = ( + f"Attribute key '{key}' is not allowed " + f"in the message payload for this " + f"device configuration with device_id " + f"'{device_id}'" + ) raise KeyError(err_msg) topic = self.__create_topic( - device=device, - topic_type=IoTAMQTTMessageType.MULTI) + device=device, topic_type=IoTAMQTTMessageType.MULTI + ) payload = self._encoders[device.protocol].encode_msg( device_id=device_id, payload=msg_payload, - msg_type=IoTAMQTTMessageType.MULTI) + msg_type=IoTAMQTTMessageType.MULTI, + ) # create message for command acknowledgement elif attribute_name is None and command_name: assert isinstance(payload, Dict), "Payload must be a dictionary" - assert len(payload.keys()) == 1, \ - "Cannot acknowledge multiple commands simultaneously" - assert next(iter(payload.keys())) in \ - [cmd.name for cmd in device.commands], \ - "Unknown command for this device!" + assert ( + len(payload.keys()) == 1 + ), "Cannot acknowledge multiple commands simultaneously" + assert next(iter(payload.keys())) in [ + cmd.name for cmd in device.commands + ], "Unknown command for this device!" topic = self.__create_topic( - device=device, - topic_type=IoTAMQTTMessageType.CMDEXE) + device=device, topic_type=IoTAMQTTMessageType.CMDEXE + ) payload = self._encoders[device.protocol].encode_msg( device_id=device_id, payload=payload, - msg_type=IoTAMQTTMessageType.CMDEXE) + msg_type=IoTAMQTTMessageType.CMDEXE, + ) # create message for single measurement elif attribute_name and command_name is None: topic = self.__create_topic( device=device, topic_type=IoTAMQTTMessageType.SINGLE, - attribute=attribute_name) + attribute=attribute_name, + ) payload = self._encoders[device.protocol].encode_msg( device_id=device_id, payload=payload, - msg_type=IoTAMQTTMessageType.SINGLE) + msg_type=IoTAMQTTMessageType.SINGLE, + ) else: raise ValueError("Inconsistent arguments!") - super().publish(topic=topic, - payload=payload, - qos=qos, - retain=retain, - properties=properties) + super().publish( + topic=topic, payload=payload, qos=qos, retain=retain, properties=properties + ) def subscribe(self, topic=None, qos=0, options=None, properties=None): """ @@ -835,13 +855,11 @@ def subscribe(self, topic=None, qos=0, options=None, properties=None): None """ if topic: - super().subscribe(topic=topic, - qos=qos, - options=options, - properties=properties) + super().subscribe( + topic=topic, qos=qos, options=options, properties=properties + ) else: for device in self._devices.values(): - self.__subscribe_commands(device=device, - qos=qos, - options=options, - properties=properties) + self.__subscribe_commands( + device=device, qos=qos, options=options, properties=properties + ) diff --git a/filip/clients/mqtt/encoder/__init__.py b/filip/clients/mqtt/encoder/__init__.py index 5636680c..108c8cbe 100644 --- a/filip/clients/mqtt/encoder/__init__.py +++ b/filip/clients/mqtt/encoder/__init__.py @@ -1,3 +1,3 @@ from .base_encoder import BaseEncoder from .json import Json -from .ulralight import Ultralight \ No newline at end of file +from .ulralight import Ultralight diff --git a/filip/clients/mqtt/encoder/base_encoder.py b/filip/clients/mqtt/encoder/base_encoder.py index 703ee6c6..2627c658 100644 --- a/filip/clients/mqtt/encoder/base_encoder.py +++ b/filip/clients/mqtt/encoder/base_encoder.py @@ -1,6 +1,7 @@ """ Abstract class for all IoTA MQTT message encoders """ + import logging from abc import ABC from datetime import datetime @@ -14,18 +15,19 @@ class BaseEncoder(ABC): """ Abstract class for all IoTA MQTT message encoders """ - prefix: str = '' + + prefix: str = "" def __init__(self): # setup logging functionality self.logger = logging.getLogger( - name=f"{self.__class__.__module__}." - f"{self.__class__.__name__}") + name=f"{self.__class__.__module__}." f"{self.__class__.__name__}" + ) self.logger.addHandler(logging.NullHandler()) - def decode_message(self, - msg: MQTTMessage, - decoder: str = 'utf-8') -> Tuple[str, str, str]: + def decode_message( + self, msg: MQTTMessage, decoder: str = "utf-8" + ) -> Tuple[str, str, str]: """ Decode message for ingoing traffic Args: @@ -37,12 +39,12 @@ def decode_message(self, device_id payload """ - topic = msg.topic.strip('/') - topic = topic.split('/') + topic = msg.topic.strip("/") + topic = topic.split("/") apikey = None device_id = None payload = msg.payload.decode(decoder) - if topic[-1] == 'cmd': + if topic[-1] == "cmd": apikey = topic[0] device_id = topic[1] @@ -51,10 +53,9 @@ def decode_message(self, return apikey, device_id, payload - def encode_msg(self, - device_id: str, - payload: Dict, - msg_type: IoTAMQTTMessageType) -> str: + def encode_msg( + self, device_id: str, payload: Dict, msg_type: IoTAMQTTMessageType + ) -> str: """ Encode message for outgoing traffic @@ -77,22 +78,20 @@ def _parse_timestamp(cls, payload: Dict) -> Dict: Returns: Dictionary containing the formatted payload """ - if payload.get('timeInstant', None): - timestamp = payload['timeInstant'] + if payload.get("timeInstant", None): + timestamp = payload["timeInstant"] if isinstance(timestamp, str): timestamp = datetime.fromisoformat(payload["timeInstant"]) if isinstance(timestamp, datetime): - payload['timeInstant'] = \ - convert_datetime_to_iso_8601_with_z_suffix( - payload['timeInstant']) + payload["timeInstant"] = convert_datetime_to_iso_8601_with_z_suffix( + payload["timeInstant"] + ) else: - raise ValueError('Not able to parse datetime') + raise ValueError("Not able to parse datetime") return payload @classmethod - def _raise_encoding_error(cls, - payload: Dict, - msg_type: IoTAMQTTMessageType): + def _raise_encoding_error(cls, payload: Dict, msg_type: IoTAMQTTMessageType): """ Helper function to provide consistent error messages Args: @@ -105,6 +104,8 @@ def _raise_encoding_error(cls, Raises: ValueError """ - ValueError(f"Message format not supported! \n " - f"Message Type: {msg_type} \n " - f"Payload: {payload}") + ValueError( + f"Message format not supported! \n " + f"Message Type: {msg_type} \n " + f"Payload: {payload}" + ) diff --git a/filip/clients/mqtt/encoder/json.py b/filip/clients/mqtt/encoder/json.py index 6fa8b100..06951dd0 100644 --- a/filip/clients/mqtt/encoder/json.py +++ b/filip/clients/mqtt/encoder/json.py @@ -1,6 +1,7 @@ """ Json encoder class for all IoTA-JSON MQTT message encoders """ + import json from typing import Any, Dict, Tuple from filip.clients.mqtt.encoder import BaseEncoder @@ -11,21 +12,18 @@ class Json(BaseEncoder): """ Json encoder class for all IoTA-JSON MQTT message encoders """ - prefix = '/json' + + prefix = "/json" def __init__(self): super().__init__() - def decode_message(self, msg, decoder='utf-8') -> Tuple[str, str, Dict]: - apikey, device_id, payload = super().decode_message(msg=msg, - decoder=decoder) + def decode_message(self, msg, decoder="utf-8") -> Tuple[str, str, Dict]: + apikey, device_id, payload = super().decode_message(msg=msg, decoder=decoder) payload = json.loads(payload) return apikey, device_id, payload - def encode_msg(self, - device_id, - payload: Any, - msg_type: IoTAMQTTMessageType) -> str: + def encode_msg(self, device_id, payload: Any, msg_type: IoTAMQTTMessageType) -> str: if msg_type == IoTAMQTTMessageType.SINGLE: return payload elif msg_type == IoTAMQTTMessageType.MULTI: diff --git a/filip/clients/mqtt/encoder/ulralight.py b/filip/clients/mqtt/encoder/ulralight.py index 4fdd3327..8f3c9b0a 100644 --- a/filip/clients/mqtt/encoder/ulralight.py +++ b/filip/clients/mqtt/encoder/ulralight.py @@ -3,8 +3,9 @@ from filip.clients.mqtt.encoder import BaseEncoder from filip.models.mqtt import IoTAMQTTMessageType + class Ultralight(BaseEncoder): - prefix = '/ul' + prefix = "/ul" def __init__(self): super().__init__() @@ -14,31 +15,30 @@ def __init__(self): def __eval_value(value: Union[bool, float, str]): return value - def decode_message(self, msg, decoder='utf-8') -> Tuple[str, str, Dict]: - apikey, device_id, payload = super().decode_message(msg=msg, - decoder=decoder) - payload = payload.split('@') + def decode_message(self, msg, decoder="utf-8") -> Tuple[str, str, Dict]: + apikey, device_id, payload = super().decode_message(msg=msg, decoder=decoder) + payload = payload.split("@") if not device_id == payload[0]: self.logger.warning("Received invalid command") - payload = payload[1].split('|') - payload = {payload[i]: self.__eval_value(payload[i + 1]) - for i in range(0, len(payload), 2)} + payload = payload[1].split("|") + payload = { + payload[i]: self.__eval_value(payload[i + 1]) + for i in range(0, len(payload), 2) + } return apikey, device_id, payload - def encode_msg(self, - device_id: str, - payload: Any, - msg_type: IoTAMQTTMessageType) -> str: + def encode_msg( + self, device_id: str, payload: Any, msg_type: IoTAMQTTMessageType + ) -> str: if msg_type == IoTAMQTTMessageType.SINGLE: return payload elif msg_type == IoTAMQTTMessageType.MULTI: payload = super()._parse_timestamp(payload=payload) - timestamp = str(payload.pop('timeInstant', '')) - data = '|'.join([f"{key}|{value}" for key, value in - payload.items()]) - data = '|'.join([timestamp, data]).strip('|') + timestamp = str(payload.pop("timeInstant", "")) + data = "|".join([f"{key}|{value}" for key, value in payload.items()]) + data = "|".join([timestamp, data]).strip("|") return data elif msg_type == IoTAMQTTMessageType.CMDEXE: for key, value in payload.items(): @@ -51,4 +51,4 @@ def encode_msg(self, else: raise ValueError("Cannot parse command acknowledgement!") return f"{device_id}@{key}|{value}" - super()._raise_encoding_error(payload=payload, msg_type=msg_type) \ No newline at end of file + super()._raise_encoding_error(payload=payload, msg_type=msg_type) diff --git a/filip/clients/ngsi_ld/__init__.py b/filip/clients/ngsi_ld/__init__.py index 6a7f885b..906122f9 100644 --- a/filip/clients/ngsi_ld/__init__.py +++ b/filip/clients/ngsi_ld/__init__.py @@ -1,3 +1,3 @@ """ This package will contain HTTP clients for FIWARE's NGSI-LD APIs -""" \ No newline at end of file +""" diff --git a/filip/clients/ngsi_ld/cb.py b/filip/clients/ngsi_ld/cb.py index 775e5a3a..88b3c12e 100644 --- a/filip/clients/ngsi_ld/cb.py +++ b/filip/clients/ngsi_ld/cb.py @@ -1,24 +1,29 @@ """ Context Broker Module for API Client """ + import json import os from math import inf from typing import Any, Dict, List, Union, Optional, Literal from urllib.parse import urljoin import requests -from pydantic import \ - TypeAdapter, \ - PositiveInt, \ - PositiveFloat +from pydantic import TypeAdapter, PositiveInt, PositiveFloat from filip.clients.base_http_client import BaseHttpClient, NgsiURLVersion from filip.config import settings from filip.models.base import FiwareLDHeader, PaginationMethod, core_context from filip.models.ngsi_v2.base import AttrsFormat from filip.models.ngsi_ld.subscriptions import SubscriptionLD -from filip.models.ngsi_ld.context import ContextLDEntity, ContextLDEntityKeyValues, \ - ContextProperty, ContextRelationship, NamedContextProperty, \ - NamedContextRelationship, ActionTypeLD, UpdateLD +from filip.models.ngsi_ld.context import ( + ContextLDEntity, + ContextLDEntityKeyValues, + ContextProperty, + ContextRelationship, + NamedContextProperty, + NamedContextRelationship, + ActionTypeLD, + UpdateLD, +) from filip.models.ngsi_v2.context import Query @@ -33,12 +38,14 @@ class ContextBrokerLDClient(BaseHttpClient): https://www.etsi.org/deliver/etsi_gs/CIM/001_099/009/01.04.01_60/gs_cim009v010401p.pdf """ - def __init__(self, - url: str = None, - *, - session: requests.Session = None, - fiware_header: FiwareLDHeader = None, - **kwargs): + def __init__( + self, + url: str = None, + *, + session: requests.Session = None, + fiware_header: FiwareLDHeader = None, + **kwargs, + ): """ Args: @@ -49,14 +56,11 @@ def __init__(self, """ # set service url url = url or settings.LD_CB_URL - #base_http_client overwrites empty header with FiwareHeader instead of FiwareLD - init_header = fiware_header if fiware_header else FiwareLDHeader() + # base_http_client overwrites empty header with FiwareHeader instead of FiwareLD + init_header = fiware_header if fiware_header else FiwareLDHeader() if init_header.link_header is None: init_header.set_context(core_context) - super().__init__(url=url, - session=session, - fiware_header=init_header, - **kwargs) + super().__init__(url=url, session=session, fiware_header=init_header, **kwargs) # set the version specific url-pattern self._url_version = NgsiURLVersion.ld_url.value # For uplink requests, the Content-Type header is essential, @@ -65,21 +69,23 @@ def __init__(self, # Content-Type will be ignored # default uplink content JSON - self.headers.update({'Content-Type': 'application/json'}) + self.headers.update({"Content-Type": "application/json"}) # default downlink content JSON-LD - self.headers.update({'Accept': 'application/ld+json'}) + self.headers.update({"Accept": "application/ld+json"}) if init_header.ngsild_tenant is not None: self.__make_tenant() - def __pagination(self, - *, - method: PaginationMethod = PaginationMethod.GET, - url: str, - headers: Dict, - limit: Union[PositiveInt, PositiveFloat] = None, - params: Dict = None, - data: str = None) -> List[Dict]: + def __pagination( + self, + *, + method: PaginationMethod = PaginationMethod.GET, + url: str, + headers: Dict, + limit: Union[PositiveInt, PositiveFloat] = None, + params: Dict = None, + data: str = None, + ) -> List[Dict]: """ NGSIv2 implements a pagination mechanism in order to help clients to retrieve large sets of resources. This mechanism works for all listing @@ -102,44 +108,44 @@ def __pagination(self, if limit is None: limit = inf if limit > 1000: - params['limit'] = 1000 # maximum items per request + params["limit"] = 1000 # maximum items per request else: - params['limit'] = limit + params["limit"] = limit if self.session: session = self.session else: session = requests.Session() with session: - res = session.request(method=method, - url=url, - params=params, - headers=headers, - data=data) + res = session.request( + method=method, url=url, params=params, headers=headers, data=data + ) if res.ok: items = res.json() # do pagination if self._url_version == NgsiURLVersion.v2_url.value: - count = int(res.headers['Fiware-Total-Count']) + count = int(res.headers["Fiware-Total-Count"]) elif self._url_version == NgsiURLVersion.ld_url.value: - count = int(res.headers['NGSILD-Results-Count']) + count = int(res.headers["NGSILD-Results-Count"]) else: count = 0 while len(items) < limit and len(items) < count: # Establishing the offset from where entities are retrieved - params['offset'] = len(items) - params['limit'] = min(1000, (limit - len(items))) - res = session.request(method=method, - url=url, - params=params, - headers=headers, - data=data) + params["offset"] = len(items) + params["limit"] = min(1000, (limit - len(items))) + res = session.request( + method=method, + url=url, + params=params, + headers=headers, + data=data, + ) if res.ok: items.extend(res.json()) else: res.raise_for_status() - self.logger.debug('Received: %s', items) + self.logger.debug("Received: %s", items) return items res.raise_for_status() @@ -149,7 +155,7 @@ def get_version(self) -> Dict: Returns: Dictionary with response """ - url = urljoin(self.base_url, '/version') + url = urljoin(self.base_url, "/version") try: res = self.get(url=url) if res.ok: @@ -165,12 +171,12 @@ def __make_tenant(self): is given in headers """ idhex = f"urn:ngsi-ld:{os.urandom(6).hex()}" - e = ContextLDEntity(id=idhex,type=f"urn:ngsi-ld:{os.urandom(6).hex()}") + e = ContextLDEntity(id=idhex, type=f"urn:ngsi-ld:{os.urandom(6).hex()}") try: self.post_entity(entity=e) self.delete_entity_by_id(idhex) except Exception as err: - self.log_error(err=err,msg="Error while creating tenant") + self.log_error(err=err, msg="Error while creating tenant") raise def get_statistics(self) -> Dict: @@ -179,7 +185,7 @@ def get_statistics(self) -> Dict: Returns: Dictionary with response """ - url = urljoin(self.base_url, 'statistics') + url = urljoin(self.base_url, "statistics") try: res = self.get(url=url) if res.ok: @@ -189,10 +195,9 @@ def get_statistics(self) -> Dict: self.logger.error(err) raise - def post_entity(self, - entity: ContextLDEntity, - append: bool = False, - update: bool = False): + def post_entity( + self, entity: ContextLDEntity, append: bool = False, update: bool = False + ): """ Function registers an Object with the NGSI-LD Context Broker, if it already exists it can be automatically updated @@ -204,21 +209,22 @@ def post_entity(self, it the way it is (update=False) """ - url = urljoin(self.base_url, f'{self._url_version}/entities') + url = urljoin(self.base_url, f"{self._url_version}/entities") headers = self.headers.copy() - if entity.model_dump().get('@context',None) is not None: - headers.update({'Content-Type':'application/ld+json'}) - headers.update({'Link':None}) + if entity.model_dump().get("@context", None) is not None: + headers.update({"Content-Type": "application/ld+json"}) + headers.update({"Link": None}) try: res = self.post( url=url, headers=headers, - json=entity.model_dump(exclude_unset=True, - exclude_defaults=True, - exclude_none=True)) + json=entity.model_dump( + exclude_unset=True, exclude_defaults=True, exclude_none=True + ), + ) if res.ok: self.logger.info("Entity successfully posted!") - return res.headers.get('Location') + return res.headers.get("Location") res.raise_for_status() except requests.RequestException as err: if err.response.status_code == 409: @@ -235,18 +241,18 @@ def override_entities(self, entities: List[ContextLDEntity]): Function to create or override existing entites with the NGSI-LD Context Broker. The batch operation with Upsert will be used. """ - return self.entity_batch_operation(entities=entities, - action_type=ActionTypeLD.UPSERT, - options="replace") + return self.entity_batch_operation( + entities=entities, action_type=ActionTypeLD.UPSERT, options="replace" + ) - def get_entity(self, - entity_id: str, - entity_type: str = None, - attrs: List[str] = None, - options: Optional[str] = None, - geometryProperty: Optional[str] = None, - ) \ - -> Union[ContextLDEntity, ContextLDEntityKeyValues, Dict[str, Any]]: + def get_entity( + self, + entity_id: str, + entity_type: str = None, + attrs: List[str] = None, + options: Optional[str] = None, + geometryProperty: Optional[str] = None, + ) -> Union[ContextLDEntity, ContextLDEntityKeyValues, Dict[str, Any]]: """ This operation must return one entity element only, but there may be more than one entity with the same ID (e.g. entities with same ID but @@ -273,19 +279,21 @@ def get_entity(self, Returns: ContextEntity """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}") headers = self.headers.copy() params = {} if entity_type: - params.update({'type': entity_type}) + params.update({"type": entity_type}) if attrs: - params.update({'attrs': ','.join(attrs)}) + params.update({"attrs": ",".join(attrs)}) if geometryProperty: - params.update({'geometryProperty': geometryProperty}) + params.update({"geometryProperty": geometryProperty}) if options: - if options != 'keyValues' and options != 'sysAttrs': - raise ValueError(f'Only available options are \'keyValues\' and \'sysAttrs\'') - params.update({'options': options}) + if options != "keyValues" and options != "sysAttrs": + raise ValueError( + f"Only available options are 'keyValues' and 'sysAttrs'" + ) + params.update({"options": options}) try: res = self.get(url=url, params=params, headers=headers) @@ -302,22 +310,30 @@ def get_entity(self, self.log_error(err=err, msg=msg) raise - GeometryShape = Literal["Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon"] - - def get_entity_list(self, - entity_id: Optional[str] = None, - id_pattern: Optional[str] = ".*", - entity_type: Optional[str] = None, - attrs: Optional[List[str]] = None, - q: Optional[str] = None, - georel: Optional[str] = None, - geometry: Optional[GeometryShape] = None, - coordinates: Optional[str] = None, - geoproperty: Optional[str] = None, - # csf: Optional[str] = None, # Context Source Filter - limit: Optional[PositiveInt] = None, - options: Optional[str] = None, - ) -> List[Union[ContextLDEntity, ContextLDEntityKeyValues]]: + GeometryShape = Literal[ + "Point", + "MultiPoint", + "LineString", + "MultiLineString", + "Polygon", + "MultiPolygon", + ] + + def get_entity_list( + self, + entity_id: Optional[str] = None, + id_pattern: Optional[str] = ".*", + entity_type: Optional[str] = None, + attrs: Optional[List[str]] = None, + q: Optional[str] = None, + georel: Optional[str] = None, + geometry: Optional[GeometryShape] = None, + coordinates: Optional[str] = None, + geoproperty: Optional[str] = None, + # csf: Optional[str] = None, # Context Source Filter + limit: Optional[PositiveInt] = None, + options: Optional[str] = None, + ) -> List[Union[ContextLDEntity, ContextLDEntityKeyValues]]: """ This operation retrieves a list of entities based on different query options. By default, the operation retrieves all the entities in the context broker. @@ -352,37 +368,39 @@ def get_entity_list(self, - sysAttrs (including createdAt and modifiedAt, etc.) - count (include number of all matched entities in response header) """ - url = urljoin(self.base_url, f'{self._url_version}/entities/') + url = urljoin(self.base_url, f"{self._url_version}/entities/") headers = self.headers.copy() params = {} if entity_id: - params.update({'id': entity_id}) + params.update({"id": entity_id}) if id_pattern: - params.update({'idPattern': id_pattern}) + params.update({"idPattern": id_pattern}) if entity_type: - params.update({'type': entity_type}) + params.update({"type": entity_type}) if attrs: - params.update({'attrs': ','.join(attrs)}) + params.update({"attrs": ",".join(attrs)}) if q: - params.update({'q': q}) + params.update({"q": q}) if georel: - params.update({'georel': georel}) + params.update({"georel": georel}) if geometry: - params.update({'geometry': geometry}) + params.update({"geometry": geometry}) if coordinates: - params.update({'coordinates': coordinates}) + params.update({"coordinates": coordinates}) if geoproperty: - params.update({'geoproperty': geoproperty}) + params.update({"geoproperty": geoproperty}) # if csf: # ContextSourceRegistration not supported yet # params.update({'csf': csf}) if limit: if limit > 1000: raise ValueError("limit must be an integer value <= 1000") - params.update({'limit': limit}) + params.update({"limit": limit}) if options: - if options != 'keyValues' and options != 'sysAttrs': - raise ValueError(f'Only available options are \'keyValues\' and \'sysAttrs\'') - params.update({'options': options}) + if options != "keyValues" and options != "sysAttrs": + raise ValueError( + f"Only available options are 'keyValues' and 'sysAttrs'" + ) + params.update({"options": options}) # params.update({'local': 'true'}) try: @@ -391,7 +409,9 @@ def get_entity_list(self, self.logger.info("Entity successfully retrieved!") entity_list: List[Union[ContextLDEntity, ContextLDEntityKeyValues]] = [] if options == "keyValues": - entity_list = [ContextLDEntityKeyValues(**item) for item in res.json()] + entity_list = [ + ContextLDEntityKeyValues(**item) for item in res.json() + ] return entity_list else: entity_list = [ContextLDEntity(**item) for item in res.json()] @@ -402,7 +422,9 @@ def get_entity_list(self, self.log_error(err=err, msg=msg) raise - def replace_existing_attributes_of_entity(self, entity: ContextLDEntity, append: bool = False): + def replace_existing_attributes_of_entity( + self, entity: ContextLDEntity, append: bool = False + ): """ The attributes previously existing in the entity are removed and replaced by the ones in the request. @@ -414,20 +436,21 @@ def replace_existing_attributes_of_entity(self, entity: ContextLDEntity, append: Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity.id}/attrs') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity.id}/attrs") headers = self.headers.copy() - if entity.model_dump().get('@context',None) is not None: - headers.update({'Content-Type':'application/ld+json'}) - headers.update({'Link':None}) + if entity.model_dump().get("@context", None) is not None: + headers.update({"Content-Type": "application/ld+json"}) + headers.update({"Link": None}) try: - res = self.patch(url=url, - headers=headers, - json=entity.model_dump(exclude={'id', 'type'}, - exclude_unset=True, - exclude_none=True)) + res = self.patch( + url=url, + headers=headers, + json=entity.model_dump( + exclude={"id", "type"}, exclude_unset=True, exclude_none=True + ), + ) if res.ok: - self.logger.info(f"Entity {entity.id} successfully " - "updated!") + self.logger.info(f"Entity {entity.id} successfully " "updated!") else: res.raise_for_status() except requests.RequestException as err: @@ -437,11 +460,17 @@ def replace_existing_attributes_of_entity(self, entity: ContextLDEntity, append: self.log_error(err=err, msg=msg) raise - def update_entity_attribute(self, - entity_id: str, - attr: Union[ContextProperty, ContextRelationship, - NamedContextProperty, NamedContextRelationship], - attr_name: str = None): + def update_entity_attribute( + self, + entity_id: str, + attr: Union[ + ContextProperty, + ContextRelationship, + NamedContextProperty, + NamedContextRelationship, + ], + attr_name: str = None, + ): """ Updates a specified attribute from an entity. Args: @@ -451,35 +480,42 @@ def update_entity_attribute(self, several entities with the same entity id. """ headers = self.headers.copy() - if not isinstance(attr, NamedContextProperty) or not isinstance(attr, NamedContextRelationship): - assert attr_name is not None, "Missing name for attribute. " \ - "attr_name must be present if" \ - "attr is of type ContextAttribute" + if not isinstance(attr, NamedContextProperty) or not isinstance( + attr, NamedContextRelationship + ): + assert attr_name is not None, ( + "Missing name for attribute. " + "attr_name must be present if" + "attr is of type ContextAttribute" + ) else: - assert attr_name is None, "Invalid argument attr_name. Do not set " \ - "attr_name if attr is of type " \ - "NamedContextAttribute or NamedContextRelationship" + assert attr_name is None, ( + "Invalid argument attr_name. Do not set " + "attr_name if attr is of type " + "NamedContextAttribute or NamedContextRelationship" + ) - url = urljoin(self.base_url, - f'{self._url_version}/entities/{entity_id}/attrs/{attr_name}') + url = urljoin( + self.base_url, f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}" + ) jsonnn = {} if isinstance(attr, list) or isinstance(attr, NamedContextProperty): - jsonnn = attr.model_dump(exclude={'name'}, - exclude_unset=True, - exclude_none=True) + jsonnn = attr.model_dump( + exclude={"name"}, exclude_unset=True, exclude_none=True + ) else: prop = attr.model_dump() for key, value in prop.items(): - if value and value != 'Property': + if value and value != "Property": jsonnn[key] = value try: - res = self.patch(url=url, - headers=headers, - json=jsonnn) + res = self.patch(url=url, headers=headers, json=jsonnn) if res.ok: - self.logger.info(f"Attribute {attr_name} of {entity_id} successfully updated!") + self.logger.info( + f"Attribute {attr_name} of {entity_id} successfully updated!" + ) else: res.raise_for_status() except requests.RequestException as err: @@ -487,10 +523,9 @@ def update_entity_attribute(self, self.log_error(err=err, msg=msg) raise - def append_entity_attributes(self, - entity: ContextLDEntity, - options: Optional[str] = None - ): + def append_entity_attributes( + self, entity: ContextLDEntity, options: Optional[str] = None + ): """ Append new Entity attributes to an existing Entity within an NGSI-LD system Args: @@ -502,25 +537,27 @@ def append_entity_attributes(self, exist already. """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity.id}/attrs') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity.id}/attrs") headers = self.headers.copy() - if entity.model_dump().get('@context',None) is not None: - headers.update({'Content-Type':'application/ld+json'}) - headers.update({'Link':None}) + if entity.model_dump().get("@context", None) is not None: + headers.update({"Content-Type": "application/ld+json"}) + headers.update({"Link": None}) params = {} if options: - if options != 'noOverwrite': - raise ValueError(f'The only available value is \'noOverwrite\'') - params.update({'options': options}) + if options != "noOverwrite": + raise ValueError(f"The only available value is 'noOverwrite'") + params.update({"options": options}) try: - res = self.post(url=url, - headers=headers, - params=params, - json=entity.model_dump(exclude={'id', 'type'}, - exclude_unset=True, - exclude_none=True)) + res = self.post( + url=url, + headers=headers, + params=params, + json=entity.model_dump( + exclude={"id", "type"}, exclude_unset=True, exclude_none=True + ), + ) if res.ok: self.logger.info(f"Entity {entity.id} successfully updated!") else: @@ -534,9 +571,7 @@ def append_entity_attributes(self, # ): # pass - def delete_entity_by_id(self, - entity_id: str, - entity_type: Optional[str] = None): + def delete_entity_by_id(self, entity_id: str, entity_type: Optional[str] = None): """ Deletes an entity by its id. For deleting mulitple entities at once, entity_batch_operation() is more efficient. @@ -546,12 +581,12 @@ def delete_entity_by_id(self, entity_type: Type of entity to delete. """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}") headers = self.headers.copy() params = {} if entity_type: - params.update({'type': entity_type}) + params.update({"type": entity_type}) try: res = self.delete(url=url, headers=headers, params=params) @@ -564,9 +599,7 @@ def delete_entity_by_id(self, self.log_error(err=err, msg=msg) raise - def delete_attribute(self, - entity_id: str, - attribute_id: str): + def delete_attribute(self, entity_id: str, attribute_id: str): """ Deletes an attribute from an entity. Args: @@ -577,13 +610,18 @@ def delete_attribute(self, Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}/attrs/{attribute_id}') + url = urljoin( + self.base_url, + f"{self._url_version}/entities/{entity_id}/attrs/{attribute_id}", + ) headers = self.headers.copy() try: res = self.delete(url=url, headers=headers) if res.ok: - self.logger.info(f"Attribute {attribute_id} of Entity {entity_id} successfully deleted") + self.logger.info( + f"Attribute {attribute_id} of Entity {entity_id} successfully deleted" + ) else: res.raise_for_status() except requests.RequestException as err: @@ -592,8 +630,7 @@ def delete_attribute(self, raise # SUBSCRIPTION API ENDPOINTS - def get_subscription_list(self, - limit: PositiveInt = inf) -> List[SubscriptionLD]: + def get_subscription_list(self, limit: PositiveInt = inf) -> List[SubscriptionLD]: """ Returns a list of all the subscriptions present in the system. Args: @@ -601,18 +638,17 @@ def get_subscription_list(self, Returns: list of subscriptions """ - url = urljoin(self.base_url, f'{self._url_version}/subscriptions/') + url = urljoin(self.base_url, f"{self._url_version}/subscriptions/") headers = self.headers.copy() params = {} # We always use the 'count' option to check weather pagination is # required - params.update({'options': 'count'}) + params.update({"options": "count"}) try: - items = self.__pagination(limit=limit, - url=url, - params=params, - headers=headers) + items = self.__pagination( + limit=limit, url=url, params=params, headers=headers + ) adapter = TypeAdapter(List[SubscriptionLD]) return adapter.validate_python(items) except requests.RequestException as err: @@ -620,8 +656,9 @@ def get_subscription_list(self, self.log_error(err=err, msg=msg) raise - def post_subscription(self, subscription: SubscriptionLD, - update: bool = False) -> str: + def post_subscription( + self, subscription: SubscriptionLD, update: bool = False + ) -> str: """ Creates a new subscription. The subscription is represented by a Subscription object defined in filip.cb.models. @@ -643,34 +680,40 @@ def post_subscription(self, subscription: SubscriptionLD, """ existing_subscriptions = self.get_subscription_list() - sub_hash = subscription.model_dump_json(include={'subject', 'notification', 'type'}) + sub_hash = subscription.model_dump_json( + include={"subject", "notification", "type"} + ) for ex_sub in existing_subscriptions: - if sub_hash == ex_sub.model_dump_json(include={'subject', 'notification', 'type'}): + if sub_hash == ex_sub.model_dump_json( + include={"subject", "notification", "type"} + ): self.logger.info("Subscription already exists") if update: self.logger.info("Updated subscription") subscription.id = ex_sub.id self.update_subscription(subscription) else: - self.logger.warning(f"Subscription existed already with the id" - f" {ex_sub.id}") + self.logger.warning( + f"Subscription existed already with the id" f" {ex_sub.id}" + ) return ex_sub.id - url = urljoin(self.base_url, f'{self._url_version}/subscriptions') + url = urljoin(self.base_url, f"{self._url_version}/subscriptions") headers = self.headers.copy() - if subscription.model_dump().get('@context',None) is not None: - headers.update({'Content-Type':'application/ld+json'}) - headers.update({'Link':None}) + if subscription.model_dump().get("@context", None) is not None: + headers.update({"Content-Type": "application/ld+json"}) + headers.update({"Link": None}) try: res = self.post( url=url, headers=headers, - data=subscription.model_dump_json(exclude_unset=False, - exclude_defaults=False, - exclude_none=True)) + data=subscription.model_dump_json( + exclude_unset=False, exclude_defaults=False, exclude_none=True + ), + ) if res.ok: self.logger.info("Subscription successfully created!") - return res.headers['Location'].split('/')[-1] + return res.headers["Location"].split("/")[-1] res.raise_for_status() except requests.RequestException as err: msg = "Could not send subscription!" @@ -686,12 +729,14 @@ def get_subscription(self, subscription_id: str) -> SubscriptionLD: Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/subscriptions/{subscription_id}') + url = urljoin( + self.base_url, f"{self._url_version}/subscriptions/{subscription_id}" + ) headers = self.headers.copy() try: res = self.get(url=url, headers=headers) if res.ok: - self.logger.debug('Received: %s', res.json()) + self.logger.debug("Received: %s", res.json()) return SubscriptionLD(**res.json()) res.raise_for_status() except requests.RequestException as err: @@ -707,19 +752,24 @@ def update_subscription(self, subscription: SubscriptionLD) -> None: Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/subscriptions/{subscription.id}') + url = urljoin( + self.base_url, f"{self._url_version}/subscriptions/{subscription.id}" + ) headers = self.headers.copy() - if subscription.model_dump().get('@context',None) is not None: - headers.update({'Content-Type':'application/ld+json'}) - headers.update({'Link':None}) + if subscription.model_dump().get("@context", None) is not None: + headers.update({"Content-Type": "application/ld+json"}) + headers.update({"Link": None}) try: res = self.patch( url=url, headers=headers, - data=subscription.model_dump_json(exclude={'id'}, - exclude_unset=True, - exclude_defaults=True, - exclude_none=True)) + data=subscription.model_dump_json( + exclude={"id"}, + exclude_unset=True, + exclude_defaults=True, + exclude_none=True, + ), + ) if res.ok: self.logger.info("Subscription successfully updated!") else: @@ -735,14 +785,16 @@ def delete_subscription(self, subscription_id: str) -> None: Args: subscription_id: id of the subscription """ - url = urljoin(self.base_url, - f'{self._url_version}/subscriptions/{subscription_id}') + url = urljoin( + self.base_url, f"{self._url_version}/subscriptions/{subscription_id}" + ) headers = self.headers.copy() try: res = self.delete(url=url, headers=headers) if res.ok: - self.logger.info(f"Subscription '{subscription_id}' " - f"successfully deleted!") + self.logger.info( + f"Subscription '{subscription_id}' " f"successfully deleted!" + ) else: res.raise_for_status() except requests.RequestException as err: @@ -752,13 +804,17 @@ def delete_subscription(self, subscription_id: str) -> None: def log_multi_errors(self, errors: List[Dict]) -> None: for error in errors: - entity_id = error['entityId'] - error_details: dict = error['error'] - error_title = error_details.get('title') - error_status = error_details.get('status') + entity_id = error["entityId"] + error_details: dict = error["error"] + error_title = error_details.get("title") + error_status = error_details.get("status") # error_detail = error_details['detail'] - self.logger.error("Response status: %d, Entity: %s, Reason: %s", - error_status, entity_id, error_title) + self.logger.error( + "Response status: %d, Entity: %s, Reason: %s", + error_status, + entity_id, + error_title, + ) def handle_multi_status_response(self, res: requests.Response): """ @@ -775,24 +831,30 @@ def handle_multi_status_response(self, res: requests.Response): res.raise_for_status() if res.text: response_data = res.json() - if 'errors' in response_data: - errors = response_data['errors'] + if "errors" in response_data: + errors = response_data["errors"] self.log_multi_errors(errors) - if 'success' in response_data: - successList = response_data['success'] + if "success" in response_data: + successList = response_data["success"] if len(successList) == 0: - raise RuntimeError("Batch operation resulted in errors only, see logs") + raise RuntimeError( + "Batch operation resulted in errors only, see logs" + ) else: self.logger.info("Empty response received.") except json.JSONDecodeError: - self.logger.info("Error decoding JSON. Response may not be in valid JSON format.") + self.logger.info( + "Error decoding JSON. Response may not be in valid JSON format." + ) # Batch operation API - def entity_batch_operation(self, - *, - entities: List[ContextLDEntity], - action_type: Union[ActionTypeLD, str], - options: Literal['noOverwrite', 'replace', 'update'] = None) -> None: + def entity_batch_operation( + self, + *, + entities: List[ContextLDEntity], + action_type: Union[ActionTypeLD, str], + options: Literal["noOverwrite", "replace", "update"] = None, + ) -> None: """ This operation allows to create, update and/or delete several entities in a single batch operation. @@ -829,30 +891,33 @@ def entity_batch_operation(self, """ - url = urljoin(self.base_url, f'{self._url_version}/entityOperations/{action_type.value}') + url = urljoin( + self.base_url, f"{self._url_version}/entityOperations/{action_type.value}" + ) headers = self.headers.copy() - headers.update({'Content-Type': 'application/json'}) + headers.update({"Content-Type": "application/json"}) params = {} if options: - params.update({'options': options}) + params.update({"options": options}) update = UpdateLD(entities=entities) try: if action_type == ActionTypeLD.DELETE: id_list = [entity.id for entity in entities] res = self.post( - url=url, - headers=headers, - params=params, - data=json.dumps(id_list)) + url=url, headers=headers, params=params, data=json.dumps(id_list) + ) else: res = self.post( url=url, headers=headers, params=params, - data=json.dumps(update.model_dump(by_alias=True, - exclude_unset=True, - exclude_none=True, - ).get('entities')) + data=json.dumps( + update.model_dump( + by_alias=True, + exclude_unset=True, + exclude_none=True, + ).get("entities") + ), ) self.handle_multi_status_response(res) except RuntimeError as rerr: diff --git a/filip/clients/ngsi_v2/__init__.py b/filip/clients/ngsi_v2/__init__.py index 550dfd98..7d56c845 100644 --- a/filip/clients/ngsi_v2/__init__.py +++ b/filip/clients/ngsi_v2/__init__.py @@ -1,6 +1,7 @@ """ HTTP clients for FIWARE's NGSIv2 APIs """ + from .cb import ContextBrokerClient from .iota import IoTAClient from .quantumleap import QuantumLeapClient diff --git a/filip/clients/ngsi_v2/cb.py b/filip/clients/ngsi_v2/cb.py index c4f499f8..c7142d16 100644 --- a/filip/clients/ngsi_v2/cb.py +++ b/filip/clients/ngsi_v2/cb.py @@ -1,6 +1,7 @@ """ Context Broker Module for API Client """ + from __future__ import annotations import copy @@ -34,6 +35,7 @@ from filip.models.ngsi_v2.base import AttrsFormat from filip.models.ngsi_v2.subscriptions import Subscription, Message from filip.models.ngsi_v2.registrations import Registration + if TYPE_CHECKING: from filip.clients.ngsi_v2.iota import IoTAClient @@ -123,7 +125,7 @@ def __pagination( if res.ok: items = res.json() # do pagination - count = int(res.headers['Fiware-Total-Count']) + count = int(res.headers["Fiware-Total-Count"]) while len(items) < limit and len(items) < count: # Establishing the offset from where entities are retrieved @@ -236,7 +238,7 @@ def post_entity( the keyValues simplified entity representation, i.e. ContextEntityKeyValues. """ - url = urljoin(self.base_url, f'{self._url_version}/entities') + url = urljoin(self.base_url, f"{self._url_version}/entities") headers = self.headers.copy() params = {} options = [] @@ -246,10 +248,12 @@ def post_entity( else: assert isinstance(entity, ContextEntity) if options: - params.update({'options': ",".join(options)}) + params.update({"options": ",".join(options)}) try: res = self.post( - url=url, headers=headers, json=entity.model_dump(exclude_none=True), + url=url, + headers=headers, + json=entity.model_dump(exclude_none=True), params=params, ) if res.ok: @@ -258,8 +262,7 @@ def post_entity( res.raise_for_status() except requests.RequestException as err: if update and err.response.status_code == 422: - return self.override_entity( - entity=entity, key_values=key_values) + return self.override_entity(entity=entity, key_values=key_values) if patch and err.response.status_code == 422: if not key_values: return self.patch_entity( @@ -346,7 +349,7 @@ def get_entity_list( Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/entities/') + url = urljoin(self.base_url, f"{self._url_version}/entities/") headers = self.headers.copy() params = {} @@ -451,7 +454,7 @@ def get_entity( Returns: ContextEntity """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}") headers = self.headers.copy() params = {} if entity_type: @@ -516,7 +519,7 @@ def get_entity_attributes( Returns: Dict """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}/attrs') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}/attrs") headers = self.headers.copy() params = {} if entity_type: @@ -543,10 +546,12 @@ def get_entity_attributes( self.log_error(err=err, msg=msg) raise - def update_entity(self, entity: Union[ContextEntity, ContextEntityKeyValues, dict], - append_strict: bool = False, - key_values: bool = False - ): + def update_entity( + self, + entity: Union[ContextEntity, ContextEntityKeyValues, dict], + append_strict: bool = False, + key_values: bool = False, + ): """ The request payload is an object representing the attributes to append or update. @@ -590,7 +595,9 @@ def update_entity(self, entity: Union[ContextEntity, ContextEntityKeyValues, dic key_values=key_values, ) - def update_entity_properties(self, entity: ContextEntity, append_strict: bool = False): + def update_entity_properties( + self, entity: ContextEntity, append_strict: bool = False + ): """ The request payload is an object representing the attributes, of any type but Relationship, to append or update. @@ -620,8 +627,9 @@ def update_entity_properties(self, entity: ContextEntity, append_strict: bool = append_strict=append_strict, ) - def update_entity_relationships(self, entity: ContextEntity, - append_strict: bool = False): + def update_entity_relationships( + self, entity: ContextEntity, append_strict: bool = False + ): """ The request payload is an object representing only the attributes, of type Relationship, to append or update. @@ -654,7 +662,7 @@ def update_entity_relationships(self, entity: ContextEntity, def delete_entity( self, entity_id: str, - entity_type: str= None, + entity_type: str = None, delete_devices: bool = False, iota_client: IoTAClient = None, iota_url: AnyHttpUrl = settings.IOTA_URL, @@ -682,10 +690,10 @@ def delete_entity( Returns: None """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}") headers = self.headers.copy() if entity_type: - params = {'type': entity_type} + params = {"type": entity_type} else: params = None try: @@ -717,8 +725,7 @@ def delete_entity( headers=self.headers, ) - for device in iota_client_local.get_device_list( - entity_names=[entity_id]): + for device in iota_client_local.get_device_list(entity_names=[entity_id]): if entity_type: if device.entity_type == entity_type: iota_client_local.delete_device(device_id=device.device_id) @@ -766,15 +773,15 @@ def delete_entities(self, entities: List[ContextEntity]) -> None: self.update(entities=entities_with_attributes, action_type="delete") def update_or_append_entity_attributes( - self, - entity_id: str, - attrs: Union[List[NamedContextAttribute], - Dict[str, ContextAttribute], - Dict[str, Any]], - entity_type: str = None, - append_strict: bool = False, - forcedUpdate: bool = False, - key_values: bool = False + self, + entity_id: str, + attrs: Union[ + List[NamedContextAttribute], Dict[str, ContextAttribute], Dict[str, Any] + ], + entity_type: str = None, + append_strict: bool = False, + forcedUpdate: bool = False, + key_values: bool = False, ): """ The request payload is an object representing the attributes to @@ -810,11 +817,11 @@ def update_or_append_entity_attributes( None """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}/attrs') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}/attrs") headers = self.headers.copy() params = {} if entity_type: - params.update({'type': entity_type}) + params.update({"type": entity_type}) else: entity_type = "dummy" @@ -827,7 +834,7 @@ def update_or_append_entity_attributes( assert isinstance(attrs, dict), "for keyValues attrs has to be a dict" options.append("keyValues") if options: - params.update({'options': ",".join(options)}) + params.update({"options": ",".join(options)}) if key_values: entity = ContextEntityKeyValues(id=entity_id, type=entity_type, **attrs) @@ -844,10 +851,7 @@ def update_or_append_entity_attributes( res = self.post( url=url, headers=headers, - json=entity.model_dump( - exclude=excluded_keys, - exclude_none=True - ), + json=entity.model_dump(exclude=excluded_keys, exclude_none=True), params=params, ) if res.ok: @@ -859,8 +863,10 @@ def update_or_append_entity_attributes( self.log_error(err=err, msg=msg) raise - def _patch_entity_key_values(self, - entity: Union[ContextEntityKeyValues, dict],): + def _patch_entity_key_values( + self, + entity: Union[ContextEntityKeyValues, dict], + ): """ The entity are updated with a ContextEntityKeyValues object or a dictionary contain the simplified entity data. This corresponds to a @@ -874,38 +880,35 @@ def _patch_entity_key_values(self, """ if isinstance(entity, dict): entity = ContextEntityKeyValues(**entity) - url = urljoin(self.base_url, f'v2/entities/{entity.id}/attrs') + url = urljoin(self.base_url, f"v2/entities/{entity.id}/attrs") headers = self.headers.copy() - params = {"type": entity.type, - "options": AttrsFormat.KEY_VALUES.value - } + params = {"type": entity.type, "options": AttrsFormat.KEY_VALUES.value} try: - res = self.patch(url=url, - headers=headers, - json=entity.model_dump(exclude={'id', 'type'}, - exclude_unset=True), - params=params) + res = self.patch( + url=url, + headers=headers, + json=entity.model_dump(exclude={"id", "type"}, exclude_unset=True), + params=params, + ) if res.ok: - self.logger.info("Entity '%s' successfully " - "updated!", entity.id) + self.logger.info("Entity '%s' successfully " "updated!", entity.id) else: res.raise_for_status() except requests.RequestException as err: - msg = f"Could not update attributes of entity" \ - f" {entity.id} !" + msg = f"Could not update attributes of entity" f" {entity.id} !" self.log_error(err=err, msg=msg) raise def update_existing_entity_attributes( - self, - entity_id: str, - attrs: Union[List[NamedContextAttribute], - Dict[str, ContextAttribute], - Dict[str, Any]], - entity_type: str = None, - forcedUpdate: bool = False, - override_metadata: bool = False, - key_values: bool = False, + self, + entity_id: str, + attrs: Union[ + List[NamedContextAttribute], Dict[str, ContextAttribute], Dict[str, Any] + ], + entity_type: str = None, + forcedUpdate: bool = False, + override_metadata: bool = False, + key_values: bool = False, ): """ The entity attributes are updated with the ones in the payload. @@ -932,7 +935,7 @@ def update_existing_entity_attributes( None """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}/attrs') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}/attrs") headers = self.headers.copy() if entity_type: params = {"type": entity_type} @@ -952,12 +955,9 @@ def update_existing_entity_attributes( else: entity = ContextEntity(id=entity_id, type=entity_type) entity.add_attributes(attrs) - payload = entity.model_dump( - exclude={"id", "type"}, - exclude_none=True - ) + payload = entity.model_dump(exclude={"id", "type"}, exclude_none=True) if options: - params.update({'options': ",".join(options)}) + params.update({"options": ",".join(options)}) try: res = self.patch( @@ -975,10 +975,9 @@ def update_existing_entity_attributes( self.log_error(err=err, msg=msg) raise - def override_entity(self, - entity: Union[ContextEntity, ContextEntityKeyValues], - **kwargs - ): + def override_entity( + self, entity: Union[ContextEntity, ContextEntityKeyValues], **kwargs + ): """ The request payload is an object representing the attributes to override the existing entity. @@ -991,21 +990,22 @@ def override_entity(self, Returns: None """ - return self.replace_entity_attributes(entity_id=entity.id, - entity_type=entity.type, - attrs=entity.get_attributes(), - **kwargs - ) + return self.replace_entity_attributes( + entity_id=entity.id, + entity_type=entity.type, + attrs=entity.get_attributes(), + **kwargs, + ) def replace_entity_attributes( - self, - entity_id: str, - attrs: Union[List[Union[NamedContextAttribute, - Dict[str, ContextAttribute]]], - Dict], - entity_type: str = None, - forcedUpdate: bool = False, - key_values: bool = False, + self, + entity_id: str, + attrs: Union[ + List[Union[NamedContextAttribute, Dict[str, ContextAttribute]]], Dict + ], + entity_type: str = None, + forcedUpdate: bool = False, + key_values: bool = False, ): """ The attributes previously existing in the entity are removed and @@ -1030,7 +1030,7 @@ def replace_entity_attributes( Returns: None """ - url = urljoin(self.base_url, f'{self._url_version}/entities/{entity_id}/attrs') + url = urljoin(self.base_url, f"{self._url_version}/entities/{entity_id}/attrs") headers = self.headers.copy() params = {} options = [] @@ -1048,12 +1048,9 @@ def replace_entity_attributes( else: entity = ContextEntity(id=entity_id, type=entity_type) entity.add_attributes(attrs) - attrs = entity.model_dump( - exclude={"id", "type"}, - exclude_none=True - ) + attrs = entity.model_dump(exclude={"id", "type"}, exclude_none=True) if options: - params.update({'options': ",".join(options)}) + params.update({"options": ",".join(options)}) try: res = self.put( @@ -1098,8 +1095,9 @@ def get_attribute( Error """ - url = urljoin(self.base_url, - f'{self._url_version}/entities/{entity_id}/attrs/{attr_name}') + url = urljoin( + self.base_url, f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}" + ) headers = self.headers.copy() params = {} if entity_type: @@ -1119,15 +1117,16 @@ def get_attribute( self.log_error(err=err, msg=msg) raise - def update_entity_attribute(self, - entity_id: str, - attr: Union[ContextAttribute, - NamedContextAttribute], - *, - entity_type: str = None, - attr_name: str = None, - override_metadata: bool = True, - forcedUpdate: bool = False): + def update_entity_attribute( + self, + entity_id: str, + attr: Union[ContextAttribute, NamedContextAttribute], + *, + entity_type: str = None, + attr_name: str = None, + override_metadata: bool = True, + forcedUpdate: bool = False, + ): """ Updates a specified attribute from an entity. @@ -1168,8 +1167,9 @@ def update_entity_attribute(self, ) attr_name = attr.name - url = urljoin(self.base_url, - f'{self._url_version}/entities/{entity_id}/attrs/{attr_name}') + url = urljoin( + self.base_url, f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}" + ) params = {} if entity_type: params.update({"type": entity_type}) @@ -1180,16 +1180,13 @@ def update_entity_attribute(self, if forcedUpdate: options.append("forcedUpdate") if options: - params.update({'options': ",".join(options)}) + params.update({"options": ",".join(options)}) try: res = self.put( url=url, headers=headers, params=params, - json=attr.model_dump( - exclude={"name"}, - exclude_none=True - ), + json=attr.model_dump(exclude={"name"}, exclude_none=True), ) if res.ok: self.logger.info( @@ -1221,8 +1218,9 @@ def delete_entity_attribute( Error """ - url = urljoin(self.base_url, - f'{self._url_version}/entities/{entity_id}/attrs/{attr_name}') + url = urljoin( + self.base_url, f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}" + ) headers = self.headers.copy() params = {} if entity_type: @@ -1238,9 +1236,7 @@ def delete_entity_attribute( else: res.raise_for_status() except requests.RequestException as err: - msg = ( - f"Could not delete attribute '{attr_name}' of entity '{entity_id}'" - ) + msg = f"Could not delete attribute '{attr_name}' of entity '{entity_id}'" self.log_error(err=err, msg=msg) raise @@ -1262,8 +1258,10 @@ def get_attribute_value( Returns: """ - url = urljoin(self.base_url, - f'{self._url_version}/entities/{entity_id}/attrs/{attr_name}/value') + url = urljoin( + self.base_url, + f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}/value", + ) headers = self.headers.copy() params = {} if entity_type: @@ -1282,13 +1280,15 @@ def get_attribute_value( self.log_error(err=err, msg=msg) raise - def update_attribute_value(self, *, - entity_id: str, - attr_name: str, - value: Any, - entity_type: str = None, - forcedUpdate: bool = False - ): + def update_attribute_value( + self, + *, + entity_id: str, + attr_name: str, + value: Any, + entity_type: str = None, + forcedUpdate: bool = False, + ): """ Updates the value of a specified attribute of an entity @@ -1306,17 +1306,19 @@ def update_attribute_value(self, *, Returns: """ - url = urljoin(self.base_url, - f'{self._url_version}/entities/{entity_id}/attrs/{attr_name}/value') + url = urljoin( + self.base_url, + f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}/value", + ) headers = self.headers.copy() params = {} if entity_type: - params.update({'type': entity_type}) + params.update({"type": entity_type}) options = [] if forcedUpdate: options.append("forcedUpdate") if options: - params.update({'options': ",".join(options)}) + params.update({"options": ",".join(options)}) try: if not isinstance(value, (dict, list)): headers.update({"Content-Type": "text/plain"}) @@ -1355,7 +1357,7 @@ def get_entity_types( Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/types') + url = urljoin(self.base_url, f"{self._url_version}/types") headers = self.headers.copy() params = {} if limit: @@ -1384,7 +1386,7 @@ def get_entity_type(self, entity_type: str) -> Dict[str, Any]: Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/types/{entity_type}') + url = urljoin(self.base_url, f"{self._url_version}/types/{entity_type}") headers = self.headers.copy() params = {} try: @@ -1407,7 +1409,7 @@ def get_subscription_list(self, limit: PositiveInt = inf) -> List[Subscription]: Returns: list of subscriptions """ - url = urljoin(self.base_url, f'{self._url_version}/subscriptions/') + url = urljoin(self.base_url, f"{self._url_version}/subscriptions/") headers = self.headers.copy() params = {} @@ -1456,12 +1458,10 @@ def post_subscription( """ existing_subscriptions = self.get_subscription_list() - sub_dict = subscription.model_dump(include={'subject', - 'notification'}) + sub_dict = subscription.model_dump(include={"subject", "notification"}) for ex_sub in existing_subscriptions: if self._subscription_dicts_are_equal( - sub_dict, - ex_sub.model_dump(include={'subject', 'notification'}) + sub_dict, ex_sub.model_dump(include={"subject", "notification"}) ): self.logger.info("Subscription already exists") if update: @@ -1519,7 +1519,9 @@ def get_subscription(self, subscription_id: str) -> Subscription: Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/subscriptions/{subscription_id}') + url = urljoin( + self.base_url, f"{self._url_version}/subscriptions/{subscription_id}" + ) headers = self.headers.copy() try: res = self.get(url=url, headers=headers) @@ -1565,17 +1567,16 @@ def update_subscription( DeprecationWarning, ) - url = urljoin(self.base_url, f'{self._url_version}/subscriptions/{subscription.id}') + url = urljoin( + self.base_url, f"{self._url_version}/subscriptions/{subscription.id}" + ) headers = self.headers.copy() headers.update({"Content-Type": "application/json"}) try: res = self.patch( url=url, headers=headers, - data=subscription.model_dump_json( - exclude={"id"}, - exclude_none=True - ), + data=subscription.model_dump_json(exclude={"id"}, exclude_none=True), ) if res.ok: self.logger.info("Subscription successfully updated!") @@ -1592,8 +1593,9 @@ def delete_subscription(self, subscription_id: str) -> None: Args: subscription_id: id of the subscription """ - url = urljoin(self.base_url, - f'{self._url_version}/subscriptions/{subscription_id}') + url = urljoin( + self.base_url, f"{self._url_version}/subscriptions/{subscription_id}" + ) headers = self.headers.copy() try: res = self.delete(url=url, headers=headers) @@ -1618,7 +1620,7 @@ def get_registration_list(self, *, limit: PositiveInt = None) -> List[Registrati Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/registrations/') + url = urljoin(self.base_url, f"{self._url_version}/registrations/") headers = self.headers.copy() params = {} @@ -1648,7 +1650,7 @@ def post_registration(self, registration: Registration): Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/registrations') + url = urljoin(self.base_url, f"{self._url_version}/registrations") headers = self.headers.copy() headers.update({"Content-Type": "application/json"}) try: @@ -1676,7 +1678,9 @@ def get_registration(self, registration_id: str) -> Registration: Returns: Registration """ - url = urljoin(self.base_url, f'{self._url_version}/registrations/{registration_id}') + url = urljoin( + self.base_url, f"{self._url_version}/registrations/{registration_id}" + ) headers = self.headers.copy() try: res = self.get(url=url, headers=headers) @@ -1698,17 +1702,16 @@ def update_registration(self, registration: Registration): Returns: """ - url = urljoin(self.base_url, f'{self._url_version}/registrations/{registration.id}') + url = urljoin( + self.base_url, f"{self._url_version}/registrations/{registration.id}" + ) headers = self.headers.copy() headers.update({"Content-Type": "application/json"}) try: res = self.patch( url=url, headers=headers, - data=registration.model_dump_json( - exclude={"id"}, - exclude_none=True - ), + data=registration.model_dump_json(exclude={"id"}, exclude_none=True), ) if res.ok: self.logger.info("Registration successfully updated!") @@ -1725,8 +1728,9 @@ def delete_registration(self, registration_id: str) -> None: Args: registration_id: id of the subscription """ - url = urljoin(self.base_url, - f'{self._url_version}/registrations/{registration_id}') + url = urljoin( + self.base_url, f"{self._url_version}/registrations/{registration_id}" + ) headers = self.headers.copy() try: res = self.delete(url=url, headers=headers) @@ -1741,14 +1745,15 @@ def delete_registration(self, registration_id: str) -> None: raise # Batch operation API - def update(self, - *, - entities: List[Union[ContextEntity, ContextEntityKeyValues]], - action_type: Union[ActionType, str], - update_format: str = None, - forcedUpdate: bool = False, - override_metadata: bool = False, - ) -> None: + def update( + self, + *, + entities: List[Union[ContextEntity, ContextEntityKeyValues]], + action_type: Union[ActionType, str], + update_format: str = None, + forcedUpdate: bool = False, + override_metadata: bool = False, + ) -> None: """ This operation allows to create, update and/or delete several entities in a single batch operation. @@ -1791,7 +1796,7 @@ def update(self, """ - url = urljoin(self.base_url, f'{self._url_version}/op/update') + url = urljoin(self.base_url, f"{self._url_version}/op/update") headers = self.headers.copy() headers.update({"Content-Type": "application/json"}) params = {} @@ -1806,7 +1811,7 @@ def update(self, ), "Only 'keyValues' is allowed as update format" options.append("keyValues") if options: - params.update({'options': ",".join(options)}) + params.update({"options": ",".join(options)}) update = Update(actionType=action_type, entities=entities) try: res = self.post( @@ -1845,7 +1850,7 @@ def query( follow the JSON entity representation format (described in the section "JSON Entity Representation"). """ - url = urljoin(self.base_url, f'{self._url_version}/op/query') + url = urljoin(self.base_url, f"{self._url_version}/op/query") headers = self.headers.copy() headers.update({"Content-Type": "application/json"}) params = {"options": "count"} @@ -1978,10 +1983,12 @@ def does_entity_exist(self, entity_id: str, entity_type: str) -> bool: raise return False - def patch_entity(self, - entity: ContextEntity, - old_entity: Optional[ContextEntity] = None, - override_attr_metadata: bool = True) -> None: + def patch_entity( + self, + entity: ContextEntity, + old_entity: Optional[ContextEntity] = None, + override_attr_metadata: bool = True, + ) -> None: """ Takes a given entity and updates the state in the CB to match it. It is an extended equivalent to the HTTP method PATCH, which applies @@ -2132,12 +2139,12 @@ def _value_is_not_none(value): If it's neither dict nore list, bool is used. """ if isinstance(value, dict): - return any([_value_is_not_none(value=_v) - for _v in value.values()]) + return any([_value_is_not_none(value=_v) for _v in value.values()]) if isinstance(value, list): - return any([_value_is_not_none(value=_v)for _v in value]) + return any([_value_is_not_none(value=_v) for _v in value]) else: return bool(value) + if first.keys() != second.keys(): warnings.warn( "Subscriptions contain a different set of fields. " @@ -2153,7 +2160,11 @@ def _value_is_not_none(value): return False if v != ex_value: self.logger.debug(f"Not equal fields for key {k}: ({v}, {ex_value})") - if not _value_is_not_none(v) and not _value_is_not_none(ex_value) or k == "timesSent": + if ( + not _value_is_not_none(v) + and not _value_is_not_none(ex_value) + or k == "timesSent" + ): continue return False return True diff --git a/filip/clients/ngsi_v2/client.py b/filip/clients/ngsi_v2/client.py index 5fb5f5ed..7bf784c0 100644 --- a/filip/clients/ngsi_v2/client.py +++ b/filip/clients/ngsi_v2/client.py @@ -1,6 +1,7 @@ """ Module for FIWARE api client """ + import logging import json import errno @@ -12,19 +13,17 @@ from filip.clients.base_http_client import BaseHttpClient from filip.config import settings from filip.models.base import FiwareHeader -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient, QuantumLeapClient -logger = logging.getLogger('client') +logger = logging.getLogger("client") class HttpClientConfig(BaseModel): """ Config class for http client """ + cb_url: Optional[AnyHttpUrl] = settings.CB_URL iota_url: Optional[AnyHttpUrl] = settings.IOTA_URL ql_url: Optional[AnyHttpUrl] = settings.QL_URL @@ -37,11 +36,14 @@ class HttpClient(BaseHttpClient): the principal of composition. Hence, each sub client is accessible from this client, but they share a general config and if provided a session. """ - def __init__(self, - config: Union[str, Path, HttpClientConfig, Dict] = None, - session: Session = None, - fiware_header: FiwareHeader = None, - **kwargs): + + def __init__( + self, + config: Union[str, Path, HttpClientConfig, Dict] = None, + session: Session = None, + fiware_header: FiwareHeader = None, + **kwargs + ): """ Constructor for master client Args: @@ -55,40 +57,48 @@ def __init__(self, else: self.config = HttpClientConfig() - super().__init__(session=session, - fiware_header=fiware_header, - **kwargs) + super().__init__(session=session, fiware_header=fiware_header, **kwargs) # initialize sub clients - self.cb = ContextBrokerClient(url=self.config.cb_url, - session=self.session, - fiware_header=self.fiware_headers, - **self.kwargs) - - self.iota = IoTAClient(url=self.config.iota_url, - session=self.session, - fiware_header=self.fiware_headers, - **self.kwargs) - - self.timeseries = QuantumLeapClient(url=self.config.ql_url, - session=self.session, - fiware_header=self.fiware_headers, - **self.kwargs) + self.cb = ContextBrokerClient( + url=self.config.cb_url, + session=self.session, + fiware_header=self.fiware_headers, + **self.kwargs + ) + + self.iota = IoTAClient( + url=self.config.iota_url, + session=self.session, + fiware_header=self.fiware_headers, + **self.kwargs + ) + + self.timeseries = QuantumLeapClient( + url=self.config.ql_url, + session=self.session, + fiware_header=self.fiware_headers, + **self.kwargs + ) # from here on deprecated? - auth_types = {'basicauth': self.__http_basic_auth, - 'digestauth': self.__http_digest_auth} + auth_types = { + "basicauth": self.__http_basic_auth, + "digestauth": self.__http_digest_auth, + } # 'oauth2': self.__oauth2} if self.config.auth: - assert self.config.auth['type'].lower() in auth_types.keys() - self.__get_secrets_file(path=self.config.auth['secret']) - auth_types[self.config.auth['type']]() + assert self.config.auth["type"].lower() in auth_types.keys() + self.__get_secrets_file(path=self.config.auth["secret"]) + auth_types[self.config.auth["type"]]() - self.__secrets = {"username": None, - "password": None, - "client_id": None, - "client_secret": None} + self.__secrets = { + "username": None, + "password": None, + "client_id": None, + "client_secret": None, + } @property def config(self): @@ -137,7 +147,7 @@ def __get_secrets_file(self, path=None): None """ try: - with open(path, 'r') as filename: + with open(path, "r") as filename: logger.info("Reading credentials from: %s", path) self.__secrets.update(json.load(filename)) @@ -158,8 +168,9 @@ def __http_basic_auth(self): """ try: self.session = Session() - self.session.auth = HTTPBasicAuth(self.__secrets['username'], - self.__secrets['password']) + self.session.auth = HTTPBasicAuth( + self.__secrets["username"], self.__secrets["password"] + ) except KeyError: pass @@ -172,8 +183,9 @@ def __http_digest_auth(self): """ try: self.session = Session() - self.session.auth = HTTPDigestAuth(self.__secrets['username'], - self.__secrets['password']) + self.session.auth = HTTPDigestAuth( + self.__secrets["username"], self.__secrets["password"] + ) except KeyError: pass @@ -200,7 +212,7 @@ def __http_digest_auth(self): # f"Other oauth2-workflows available are: " # f"{oauth2clients.keys()}") # workflow = 'authorization_code_grant' -# + # # oauthclient = oauth2clients[workflow](client_id=self.__secrets[ # 'client_id']) # self.session = OAuth2Session(client_id=None, @@ -210,7 +222,7 @@ def __http_digest_auth(self): # auto_refresh_kwargs={ # self.__secrets['client_id'], # self.__secrets['client_secret']}) -# + # # self.__token = self.session.fetch_token( # token_url=self.__secrets['token_url'], # username=self.__secrets['username'], diff --git a/filip/clients/ngsi_v2/iota.py b/filip/clients/ngsi_v2/iota.py index 49cd28ed..41bce52e 100644 --- a/filip/clients/ngsi_v2/iota.py +++ b/filip/clients/ngsi_v2/iota.py @@ -1,6 +1,7 @@ """ IoT-Agent Module for API Client """ + from __future__ import annotations import json @@ -35,18 +36,19 @@ class IoTAClient(BaseHttpClient): **kwargs (Optional): Optional arguments that ``request`` takes. """ - def __init__(self, - url: str = None, - *, - session: requests.Session = None, - fiware_header: FiwareHeader = None, - **kwargs): + def __init__( + self, + url: str = None, + *, + session: requests.Session = None, + fiware_header: FiwareHeader = None, + **kwargs, + ): # set service url url = url or settings.IOTA_URL - super().__init__(url=url, - session=session, - fiware_header=fiware_header, - **kwargs) + super().__init__( + url=url, session=session, fiware_header=fiware_header, **kwargs + ) # ABOUT API def get_version(self) -> Dict: @@ -56,7 +58,7 @@ def get_version(self) -> Dict: Returns: Dictionary with response """ - url = urljoin(self.base_url, 'iot/about') + url = urljoin(self.base_url, "iot/about") try: res = self.get(url=url, headers=self.headers) if res.ok: @@ -67,9 +69,11 @@ def get_version(self) -> Dict: raise # SERVICE GROUP API - def post_groups(self, - service_groups: Union[ServiceGroup, List[ServiceGroup]], - update: bool = False): + def post_groups( + self, + service_groups: Union[ServiceGroup, List[ServiceGroup]], + update: bool = False, + ): """ Creates a set of service groups for the given service and service_path. The service_group and subservice information will taken from the @@ -87,17 +91,22 @@ def post_groups(self, service_groups = [service_groups] for group in service_groups: if group.service: - assert group.service == self.headers['fiware-service'], \ - "Service group service does not math fiware service" + assert ( + group.service == self.headers["fiware-service"] + ), "Service group service does not math fiware service" if group.subservice: - assert group.subservice == self.headers['fiware-servicepath'], \ - "Service group subservice does not math fiware service path" + assert ( + group.subservice == self.headers["fiware-servicepath"] + ), "Service group subservice does not math fiware service path" - url = urljoin(self.base_url, 'iot/services') + url = urljoin(self.base_url, "iot/services") headers = self.headers - data = {'services': [group.model_dump(exclude={'service', 'subservice'}, - exclude_none=True) - for group in service_groups]} + data = { + "services": [ + group.model_dump(exclude={"service", "subservice"}, exclude_none=True) + for group in service_groups + ] + } try: res = self.post(url=url, headers=headers, json=data) if res.ok: @@ -105,13 +114,13 @@ def post_groups(self, elif res.status_code == 409: self.logger.warning(res.text) if len(service_groups) > 1: - self.logger.info("Trying to split bulk operation into " - "single operations") + self.logger.info( + "Trying to split bulk operation into " "single operations" + ) for group in service_groups: self.post_group(service_group=group, update=update) elif update is True: - self.update_group(service_group=service_groups[0], - fields=None) + self.update_group(service_group=service_groups[0], fields=None) else: res.raise_for_status() else: @@ -132,8 +141,7 @@ def post_group(self, service_group: ServiceGroup, update: bool = False): Returns: None """ - return self.post_groups(service_groups=[service_group], - update=update) + return self.post_groups(service_groups=[service_group], update=update) def get_group_list(self) -> List[ServiceGroup]: r""" @@ -145,13 +153,13 @@ def get_group_list(self) -> List[ServiceGroup]: Returns: """ - url = urljoin(self.base_url, 'iot/services') + url = urljoin(self.base_url, "iot/services") headers = self.headers try: res = self.get(url=url, headers=headers) if res.ok: ta = TypeAdapter(List[ServiceGroup]) - return ta.validate_python(res.json()['services']) + return ta.validate_python(res.json()["services"]) res.raise_for_status() except requests.RequestException as err: self.log_error(err=err, msg=None) @@ -168,19 +176,28 @@ def get_group(self, *, resource: str, apikey: str) -> ServiceGroup: """ groups = self.get_group_list() - groups = filter_group_list(group_list=groups, resources=resource, apikeys=apikey) + groups = filter_group_list( + group_list=groups, resources=resource, apikeys=apikey + ) if len(groups) == 1: group = groups[0] return group elif len(groups) == 0: - raise KeyError(f"Service group with resource={resource} and apikey={apikey} was not found") + raise KeyError( + f"Service group with resource={resource} and apikey={apikey} was not found" + ) else: - raise NotImplementedError("There is a wierd error, try get_group_list() for debugging") - - def update_groups(self, *, - service_groups: Union[ServiceGroup, List[ServiceGroup]], - add: False, - fields: Union[Set[str], List[str]] = None) -> None: + raise NotImplementedError( + "There is a wierd error, try get_group_list() for debugging" + ) + + def update_groups( + self, + *, + service_groups: Union[ServiceGroup, List[ServiceGroup]], + add: False, + fields: Union[Set[str], List[str]] = None, + ) -> None: """ Bulk operation for service group update. Args: @@ -196,15 +213,19 @@ def update_groups(self, *, for group in service_groups: self.update_group(service_group=group, fields=fields, add=add) - def update_group(self, *, service_group: ServiceGroup, - fields: Union[Set[str], List[str]] = None, - add: bool = True): + def update_group( + self, + *, + service_group: ServiceGroup, + fields: Union[Set[str], List[str]] = None, + add: bool = True, + ): """ Modifies the information for a service group configuration, identified by the resource and apikey query parameters. Takes a service group body as the payload. The body does not have to be complete: for incomplete bodies, just the existing attributes will be updated - + Args: service_group (ServiceGroup): Service to update. fields: Fields of the service_group to update. If 'None' all allowed @@ -218,17 +239,18 @@ def update_group(self, *, service_group: ServiceGroup, fields = set(fields) else: fields = None - url = urljoin(self.base_url, 'iot/services') + url = urljoin(self.base_url, "iot/services") headers = self.headers - params = service_group.model_dump(include={'resource', 'apikey'}) + params = service_group.model_dump(include={"resource", "apikey"}) try: - res = self.put(url=url, - headers=headers, - params=params, - json=service_group.model_dump( - include=fields, - exclude={'service', 'subservice'}, - exclude_none=True)) + res = self.put( + url=url, + headers=headers, + params=params, + json=service_group.model_dump( + include=fields, exclude={"service", "subservice"}, exclude_none=True + ), + ) if res.ok: self.logger.info("ServiceGroup updated!") elif (res.status_code == 404) & (add is True): @@ -242,7 +264,7 @@ def update_group(self, *, service_group: ServiceGroup, def delete_group(self, *, resource: str, apikey: str): """ Deletes a service group in in the IoT-Agent - + Args: resource: apikey: @@ -250,27 +272,32 @@ def delete_group(self, *, resource: str, apikey: str): Returns: """ - url = urljoin(self.base_url, 'iot/services') + url = urljoin(self.base_url, "iot/services") headers = self.headers - params = {'resource': resource, - 'apikey': apikey} + params = {"resource": resource, "apikey": apikey} try: res = self.delete(url=url, headers=headers, params=params) if res.ok: - self.logger.info("ServiceGroup with resource: '%s' and " - "apikey: '%s' successfully deleted!", - resource, apikey) + self.logger.info( + "ServiceGroup with resource: '%s' and " + "apikey: '%s' successfully deleted!", + resource, + apikey, + ) else: res.raise_for_status() except requests.RequestException as err: - msg = f"Could not delete ServiceGroup with resource " \ - f"'{resource}' and apikey '{apikey}'!" + msg = ( + f"Could not delete ServiceGroup with resource " + f"'{resource}' and apikey '{apikey}'!" + ) self.log_error(err=err, msg=msg) raise # DEVICE API - def post_devices(self, *, devices: Union[Device, List[Device]], - update: bool = False) -> None: + def post_devices( + self, *, devices: Union[Device, List[Device]], update: bool = False + ) -> None: """ Post a device from the device registry. No payload is required or received. @@ -284,11 +311,15 @@ def post_devices(self, *, devices: Union[Device, List[Device]], """ if not isinstance(devices, list): devices = [devices] - url = urljoin(self.base_url, 'iot/devices') + url = urljoin(self.base_url, "iot/devices") headers = self.headers - - data = {"devices": [json.loads(device.model_dump_json(exclude_none=True) - ) for device in devices]} + + data = { + "devices": [ + json.loads(device.model_dump_json(exclude_none=True)) + for device in devices + ] + } try: res = self.post(url=url, headers=headers, json=data) if res.ok: @@ -305,7 +336,7 @@ def post_devices(self, *, devices: Union[Device, List[Device]], def post_device(self, *, device: Device, update: bool = False) -> None: """ Post a device configuration to the IoT-Agent - + Args: device: IoT device configuration to send update: update device if configuration already exists @@ -315,12 +346,15 @@ def post_device(self, *, device: Device, update: bool = False) -> None: """ return self.post_devices(devices=[device], update=update) - def get_device_list(self, *, - limit: int = None, - offset: int = None, - device_ids: Union[str, List[str]] = None, - entity_names: Union[str, List[str]] = None, - entity_types: Union[str, List[str]] = None) -> List[Device]: + def get_device_list( + self, + *, + limit: int = None, + offset: int = None, + device_ids: Union[str, List[str]] = None, + entity_names: Union[str, List[str]] = None, + entity_types: Union[str, List[str]] = None, + ) -> List[Device]: """ Returns a list of all the devices in the device registry with all its data. The IoTAgent now only supports "limit" and "offset" as @@ -348,23 +382,20 @@ def get_device_list(self, *, """ if limit: if not 1 < limit < 1000: - self.logger.error("'limit' must be an integer between 1 and " - "1000!") + self.logger.error("'limit' must be an integer between 1 and " "1000!") raise ValueError - url = urljoin(self.base_url, 'iot/devices') + url = urljoin(self.base_url, "iot/devices") headers = self.headers - params = {key: value for key, value in locals().items() if value is not - None} + params = {key: value for key, value in locals().items() if value is not None} try: res = self.get(url=url, headers=headers, params=params) if res.ok: ta = TypeAdapter(List[Device]) - devices = ta.validate_python(res.json()['devices']) + devices = ta.validate_python(res.json()["devices"]) # filter by device_ids, entity_names or entity_types - devices = filter_device_list(devices, - device_ids, - entity_names, - entity_types) + devices = filter_device_list( + devices, device_ids, entity_names, entity_types + ) return devices res.raise_for_status() except requests.RequestException as err: @@ -374,7 +405,7 @@ def get_device_list(self, *, def get_device(self, *, device_id: str) -> Device: """ Returns all the information about a particular device. - + Args: device_id: Raises: @@ -383,7 +414,7 @@ def get_device(self, *, device_id: str) -> Device: Device """ - url = urljoin(self.base_url, f'iot/devices/{device_id}') + url = urljoin(self.base_url, f"iot/devices/{device_id}") headers = self.headers try: res = self.get(url=url, headers=headers) @@ -410,15 +441,19 @@ def update_device(self, *, device: Device, add: bool = True) -> None: Returns: None """ - url = urljoin(self.base_url, f'iot/devices/{device.device_id}') + url = urljoin(self.base_url, f"iot/devices/{device.device_id}") headers = self.headers try: - res = self.put(url=url, headers=headers, json=device.model_dump( - include={'attributes', 'lazy', 'commands', 'static_attributes'}, - exclude_none=True)) + res = self.put( + url=url, + headers=headers, + json=device.model_dump( + include={"attributes", "lazy", "commands", "static_attributes"}, + exclude_none=True, + ), + ) if res.ok: - self.logger.info("Device '%s' successfully updated!", - device.device_id) + self.logger.info("Device '%s' successfully updated!", device.device_id) elif (res.status_code == 404) & (add is True): self.post_device(device=device, update=False) else: @@ -428,8 +463,9 @@ def update_device(self, *, device: Device, add: bool = True) -> None: self.log_error(err=err, msg=msg) raise - def update_devices(self, *, devices: Union[Device, List[Device]], - add: False) -> None: + def update_devices( + self, *, devices: Union[Device, List[Device]], add: False + ) -> None: """ Bulk operation for device update. Args: @@ -444,16 +480,19 @@ def update_devices(self, *, devices: Union[Device, List[Device]], for device in devices: self.update_device(device=device, add=add) - def delete_device(self, *, device_id: str, - cb_url: AnyHttpUrl = settings.CB_URL, - delete_entity: bool = False, - force_entity_deletion: bool = False, - cb_client: ContextBrokerClient = None, - ) -> None: + def delete_device( + self, + *, + device_id: str, + cb_url: AnyHttpUrl = settings.CB_URL, + delete_entity: bool = False, + force_entity_deletion: bool = False, + cb_client: ContextBrokerClient = None, + ) -> None: """ Remove a device from the device registry. No payload is required or received. - + Args: device_id: str, ID of Device delete_entity: False -> Only delete the device entry, @@ -479,7 +518,10 @@ def delete_device(self, *, device_id: str, Returns: None """ - url = urljoin(self.base_url, f'iot/devices/{device_id}', ) + url = urljoin( + self.base_url, + f"iot/devices/{device_id}", + ) headers = self.headers device = self.get_device(device_id=device_id) @@ -502,9 +544,11 @@ def delete_device(self, *, device_id: str, # Zero because we count the remaining devices if len(devices) > 0 and not force_entity_deletion: - raise Exception(f"The corresponding entity to the device " - f"{device_id} was not deleted because it is " - f"linked to multiple devices. ") + raise Exception( + f"The corresponding entity to the device " + f"{device_id} was not deleted because it is " + f"linked to multiple devices. " + ) else: try: from filip.clients.ngsi_v2 import ContextBrokerClient @@ -512,18 +556,21 @@ def delete_device(self, *, device_id: str, if cb_client: cb_client_local = deepcopy(cb_client) else: - warnings.warn("No `ContextBrokerClient` " - "object providesd! Will try to generate " - "one. This usage is not recommended.") + warnings.warn( + "No `ContextBrokerClient` " + "object providesd! Will try to generate " + "one. This usage is not recommended." + ) cb_client_local = ContextBrokerClient( url=cb_url, fiware_header=self.fiware_headers, - headers=headers) + headers=headers, + ) cb_client_local.delete_entity( - entity_id=device.entity_name, - entity_type=device.entity_type) + entity_id=device.entity_name, entity_type=device.entity_type + ) except requests.RequestException as err: # Do not throw an error @@ -533,11 +580,13 @@ def delete_device(self, *, device_id: str, cb_client_local.close() - def patch_device(self, - device: Device, - patch_entity: bool = True, - cb_client: ContextBrokerClient = None, - cb_url: AnyHttpUrl = settings.CB_URL) -> None: + def patch_device( + self, + device: Device, + patch_entity: bool = True, + cb_client: ContextBrokerClient = None, + cb_url: AnyHttpUrl = settings.CB_URL, + ) -> None: """ Updates a device state in Fiware, if the device does not exists it is created, else its values are updated. @@ -571,20 +620,30 @@ def patch_device(self, # if the device settings were changed we need to delete the device # and repost it - settings_dict = {"device_id", "service", "service_path", - "entity_name", "entity_type", - "timestamp", "apikey", "endpoint", - "protocol", "transport", - "expressionLanguage"} + settings_dict = { + "device_id", + "service", + "service_path", + "entity_name", + "entity_type", + "timestamp", + "apikey", + "endpoint", + "protocol", + "transport", + "expressionLanguage", + } live_settings = live_device.model_dump(include=settings_dict) new_settings = device.model_dump(include=settings_dict) if not live_settings == new_settings: - self.delete_device(device_id=device.device_id, - delete_entity=True, - force_entity_deletion=True, - cb_client=cb_client) + self.delete_device( + device_id=device.device_id, + delete_entity=True, + force_entity_deletion=True, + cb_client=cb_client, + ) self.post_device(device=device) return @@ -600,61 +659,67 @@ def patch_device(self, # update context entry # 1. build context entity from information in device # 2. patch it - from filip.models.ngsi_v2.context import \ - ContextEntity, NamedContextAttribute + from filip.models.ngsi_v2.context import ContextEntity, NamedContextAttribute def build_context_entity_from_device(device: Device) -> ContextEntity: from filip.models.base import DataType - entity = ContextEntity(id=device.entity_name, - type=device.entity_type) + + entity = ContextEntity(id=device.entity_name, type=device.entity_type) for command in device.commands: - entity.add_attributes([ - # Command attribute will be registered by the device_update - NamedContextAttribute( - name=f"{command.name}_info", - type=DataType.COMMAND_RESULT - ), - NamedContextAttribute( - name=f"{command.name}_status", - type=DataType.COMMAND_STATUS - ) - ]) + entity.add_attributes( + [ + # Command attribute will be registered by the device_update + NamedContextAttribute( + name=f"{command.name}_info", type=DataType.COMMAND_RESULT + ), + NamedContextAttribute( + name=f"{command.name}_status", type=DataType.COMMAND_STATUS + ), + ] + ) for attribute in device.attributes: - entity.add_attributes([ - NamedContextAttribute( - name=attribute.name, - type=DataType.STRUCTUREDVALUE, - metadata=attribute.metadata - ) - ]) + entity.add_attributes( + [ + NamedContextAttribute( + name=attribute.name, + type=DataType.STRUCTUREDVALUE, + metadata=attribute.metadata, + ) + ] + ) for static_attribute in device.static_attributes: - entity.add_attributes([ - NamedContextAttribute( - name=static_attribute.name, - type=static_attribute.type, - value=static_attribute.value, - metadata=static_attribute.metadata - ) - ]) + entity.add_attributes( + [ + NamedContextAttribute( + name=static_attribute.name, + type=static_attribute.type, + value=static_attribute.value, + metadata=static_attribute.metadata, + ) + ] + ) return entity if patch_entity: from filip.clients.ngsi_v2 import ContextBrokerClient + if cb_client: cb_client_local = deepcopy(cb_client) else: - warnings.warn("No `ContextBrokerClient` object provided! " - "Will try to generate one. " - "This usage is not recommended.") + warnings.warn( + "No `ContextBrokerClient` object provided! " + "Will try to generate one. " + "This usage is not recommended." + ) cb_client_local = ContextBrokerClient( - url=cb_url, - fiware_header=self.fiware_headers, - headers=self.headers) + url=cb_url, fiware_header=self.fiware_headers, headers=self.headers + ) cb_client_local.patch_entity( - entity=build_context_entity_from_device(device)) + entity=build_context_entity_from_device(device) + ) cb_client_local.close() def does_device_exists(self, device_id: str) -> bool: @@ -680,14 +745,14 @@ def get_loglevel_of_agent(self): Returns: """ - url = urljoin(self.base_url, 'admin/log') + url = urljoin(self.base_url, "admin/log") headers = self.headers.copy() - del headers['fiware-service'] - del headers['fiware-servicepath'] + del headers["fiware-service"] + del headers["fiware-servicepath"] try: res = self.get(url=url, headers=headers) if res.ok: - return res.json()['level'] + return res.json()["level"] res.raise_for_status() except requests.RequestException as err: self.log_error(err=err) @@ -696,7 +761,7 @@ def get_loglevel_of_agent(self): def change_loglevel_of_agent(self, level: str): """ Change current loglevel of agent - + Args: level: @@ -704,18 +769,19 @@ def change_loglevel_of_agent(self, level: str): """ level = level.upper() - if level not in ['INFO', 'ERROR', 'FATAL', 'DEBUG', 'WARNING']: + if level not in ["INFO", "ERROR", "FATAL", "DEBUG", "WARNING"]: raise KeyError("Given log level is not supported") - url = urljoin(self.base_url, 'admin/log') + url = urljoin(self.base_url, "admin/log") headers = self.headers.copy() - del headers['fiware-service'] - del headers['fiware-servicepath'] + del headers["fiware-service"] + del headers["fiware-servicepath"] try: res = self.put(url=url, headers=headers, params=level) if res.ok: - self.logger.info("Loglevel of agent at %s " - "changed to '%s'", self.base_url, level) + self.logger.info( + "Loglevel of agent at %s " "changed to '%s'", self.base_url, level + ) else: res.raise_for_status() except requests.RequestException as err: diff --git a/filip/clients/ngsi_v2/quantumleap.py b/filip/clients/ngsi_v2/quantumleap.py index f9471f83..44640013 100644 --- a/filip/clients/ngsi_v2/quantumleap.py +++ b/filip/clients/ngsi_v2/quantumleap.py @@ -1,11 +1,12 @@ """ TimeSeries Module for QuantumLeap API Client """ + import logging import time from math import inf from collections import deque -from itertools import count,chain +from itertools import count, chain from typing import Dict, List, Union, Deque, Optional from urllib.parse import urljoin import requests @@ -15,13 +16,14 @@ from filip.clients.base_http_client import BaseHttpClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.subscriptions import Message -from filip.models.ngsi_v2.timeseries import \ - AggrPeriod, \ - AggrMethod, \ - AggrScope, \ - AttributeValues, \ - TimeSeries, \ - TimeSeriesHeader +from filip.models.ngsi_v2.timeseries import ( + AggrPeriod, + AggrMethod, + AggrScope, + AttributeValues, + TimeSeries, + TimeSeriesHeader, +) logger = logging.getLogger(__name__) @@ -42,18 +44,19 @@ class QuantumLeapClient(BaseHttpClient): **kwargs: """ - def __init__(self, - url: str = None, - *, - session: requests.Session = None, - fiware_header: FiwareHeader = None, - **kwargs): + def __init__( + self, + url: str = None, + *, + session: requests.Session = None, + fiware_header: FiwareHeader = None, + **kwargs, + ): # set service url url = url or settings.QL_URL - super().__init__(url=url, - session=session, - fiware_header=fiware_header, - **kwargs) + super().__init__( + url=url, session=session, fiware_header=fiware_header, **kwargs + ) # META API ENDPOINTS def get_version(self) -> Dict: @@ -63,7 +66,7 @@ def get_version(self) -> Dict: Returns: Dictionary with response """ - url = urljoin(self.base_url, 'version') + url = urljoin(self.base_url, "version") try: res = self.get(url=url, headers=self.headers) if res.ok: @@ -86,7 +89,7 @@ def get_health(self) -> Dict: Returns: Dictionary with response """ - url = urljoin(self.base_url, 'health') + url = urljoin(self.base_url, "health") try: res = self.get(url=url, headers=self.headers) if res.ok: @@ -111,42 +114,40 @@ def post_notification(self, notification: Message): Args: notification: Notification Message Object """ - url = urljoin(self.base_url, 'v2/notify') + url = urljoin(self.base_url, "v2/notify") headers = self.headers.copy() data = [] for entity in notification.data: data.append(entity.model_dump(exclude_none=True)) - data_set = { - "data": data, - "subscriptionId": notification.subscriptionId - } + data_set = {"data": data, "subscriptionId": notification.subscriptionId} try: - res = self.post( - url=url, - headers=headers, - json=data_set) + res = self.post(url=url, headers=headers, json=data_set) if res.ok: self.logger.debug(res.text) else: res.raise_for_status() except requests.exceptions.RequestException as err: - msg = f"Could not post notification for subscription id " \ - f"{notification.subscriptionId}" + msg = ( + f"Could not post notification for subscription id " + f"{notification.subscriptionId}" + ) self.log_error(err=err, msg=msg) raise - def post_subscription(self, - cb_url: Union[AnyHttpUrl, str], - ql_url: Union[AnyHttpUrl, str], - entity_type: str = None, - entity_id: str = None, - id_pattern: str = None, - attributes: str = None, - observed_attributes: str = None, - notified_attributes: str = None, - throttling: int = None, - time_index_attribute: str = None): + def post_subscription( + self, + cb_url: Union[AnyHttpUrl, str], + ql_url: Union[AnyHttpUrl, str], + entity_type: str = None, + entity_id: str = None, + id_pattern: str = None, + attributes: str = None, + observed_attributes: str = None, + notified_attributes: str = None, + throttling: int = None, + time_index_attribute: str = None, + ): """ Subscribe QL to process Orion notifications of certain type. This endpoint simplifies the creation of the subscription in orion @@ -184,12 +185,13 @@ def post_subscription(self, used as a time index. """ - raise DeprecationWarning("Subscription endpoint of Quantumleap API is " - "deprecated, use the ORION subscription endpoint " - "instead") + raise DeprecationWarning( + "Subscription endpoint of Quantumleap API is " + "deprecated, use the ORION subscription endpoint " + "instead" + ) - def delete_entity(self, entity_id: str, - entity_type: Optional[str] = None) -> str: + def delete_entity(self, entity_id: str, entity_type: Optional[str] = None) -> str: """ Given an entity (with type and id), delete all its historical records. @@ -205,10 +207,10 @@ def delete_entity(self, entity_id: str, Returns: The entity_id of entity that is deleted. """ - url = urljoin(self.base_url, f'v2/entities/{entity_id}') + url = urljoin(self.base_url, f"v2/entities/{entity_id}") headers = self.headers.copy() if entity_type is not None: - params = {'type': entity_type} + params = {"type": entity_type} else: params = {} @@ -222,11 +224,9 @@ def delete_entity(self, entity_id: str, while counter < 10: self.delete(url=url, headers=headers, params=params) try: - self.get_entity_by_id(entity_id=entity_id, - entity_type=entity_type) + self.get_entity_by_id(entity_id=entity_id, entity_type=entity_type) except requests.exceptions.RequestException as err: - self.logger.info("Entity id '%s' successfully deleted!", - entity_id) + self.logger.info("Entity id '%s' successfully deleted!", entity_id) return entity_id time.sleep(counter * 5) counter += 1 @@ -244,13 +244,14 @@ def delete_entity_type(self, entity_type: str) -> str: Returns: Entity type of the entities deleted. """ - url = urljoin(self.base_url, f'v2/types/{entity_type}') + url = urljoin(self.base_url, f"v2/types/{entity_type}") headers = self.headers.copy() try: res = self.delete(url=url, headers=headers) if res.ok: - self.logger.info("Entities of type '%s' successfully deleted!", - entity_type) + self.logger.info( + "Entities of type '%s' successfully deleted!", entity_type + ) return entity_type res.raise_for_status() except requests.exceptions.RequestException as err: @@ -259,26 +260,27 @@ def delete_entity_type(self, entity_type: str) -> str: raise # QUERY API ENDPOINTS - def __query_builder(self, - url, - *, - entity_id: str = None, - id_pattern: str = None, - options: str = None, - entity_type: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = 0, - georel: str = None, - geometry: str = None, - coords: str = None, - attrs: str = None, - aggr_scope: Union[str, AggrScope] = None - ) -> Deque[Dict]: + def __query_builder( + self, + url, + *, + entity_id: str = None, + id_pattern: str = None, + options: str = None, + entity_type: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = 0, + georel: str = None, + geometry: str = None, + coords: str = None, + attrs: str = None, + aggr_scope: Union[str, AggrScope] = None, + ) -> Deque[Dict]: """ Private Function to call respective API endpoints, chops large requests into multiple single requests and merges the @@ -314,7 +316,9 @@ def __query_builder(self, Returns: Dict """ - assert (id_pattern is None or entity_id is None), "Cannot have both id and idPattern as parameter." + assert ( + id_pattern is None or entity_id is None + ), "Cannot have both id and idPattern as parameter." params = {} headers = self.headers.copy() max_records_per_request = 10000 @@ -322,19 +326,19 @@ def __query_builder(self, res_q: Deque[Dict] = deque([]) if options: - params.update({'options': options}) + params.update({"options": options}) if entity_type: - params.update({'type': entity_type}) + params.update({"type": entity_type}) if aggr_method: aggr_method = AggrMethod(aggr_method) - params.update({'aggrMethod': aggr_method.value}) + params.update({"aggrMethod": aggr_method.value}) if aggr_period: aggr_period = AggrPeriod(aggr_period) - params.update({'aggrPeriod': aggr_period.value}) + params.update({"aggrPeriod": aggr_period.value}) if from_date: - params.update({'fromDate': from_date}) + params.update({"fromDate": from_date}) if to_date: - params.update({'toDate': to_date}) + params.update({"toDate": to_date}) # These values are required for the integrated pagination mechanism # maximum items per request if limit is None: @@ -342,40 +346,40 @@ def __query_builder(self, if offset is None: offset = 0 if georel: - params.update({'georel': georel}) + params.update({"georel": georel}) if coords: - params.update({'coords': coords}) + params.update({"coords": coords}) if geometry: - params.update({'geometry': geometry}) + params.update({"geometry": geometry}) if attrs: - params.update({'attrs': attrs}) + params.update({"attrs": attrs}) if aggr_scope: aggr_scope = AggrScope(aggr_scope) - params.update({'aggr_scope': aggr_scope.value}) + params.update({"aggr_scope": aggr_scope.value}) if entity_id: - params.update({'id': entity_id}) + params.update({"id": entity_id}) if id_pattern: - params.update({'idPattern': id_pattern}) + params.update({"idPattern": id_pattern}) # This loop will chop large requests into smaller junks. # The individual functions will then merge the final response models for i in count(0, max_records_per_request): try: - params['offset'] = offset + i + params["offset"] = offset + i - params['limit'] = min(limit - i, max_records_per_request) - if params['limit'] <= 0: + params["limit"] = min(limit - i, max_records_per_request) + if params["limit"] <= 0: break if last_n: - params['lastN'] = min(last_n - i, max_records_per_request) - if params['lastN'] <= 0: + params["lastN"] = min(last_n - i, max_records_per_request) + if params["lastN"] <= 0: break res = self.get(url=url, params=params, headers=headers) if res.ok: - self.logger.debug('Received: %s', res.json()) + self.logger.debug("Received: %s", res.json()) # revert append direction when using last_n if last_n: @@ -385,9 +389,11 @@ def __query_builder(self, res.raise_for_status() except requests.exceptions.RequestException as err: - if err.response.status_code == 404 and \ - err.response.json().get('error') == 'Not Found' and \ - len(res_q) > 0: + if ( + err.response.status_code == 404 + and err.response.json().get("error") == "Not Found" + and len(res_q) > 0 + ): break else: msg = "Could not load entity data" @@ -398,14 +404,16 @@ def __query_builder(self, return res_q # v2/entities - def get_entities(self, *, - entity_type: str = None, - id_pattern: str = None, - from_date: str = None, - to_date: str = None, - limit: int = 10000, - offset: int = None - ) -> List[TimeSeriesHeader]: + def get_entities( + self, + *, + entity_type: str = None, + id_pattern: str = None, + from_date: str = None, + to_date: str = None, + limit: int = 10000, + offset: int = None, + ) -> List[TimeSeriesHeader]: """ Get list of all available entities and their context information about EntityType and last update date. @@ -433,37 +441,39 @@ def get_entities(self, *, Returns: List of TimeSeriesHeader """ - url = urljoin(self.base_url, 'v2/entities') - res = self.__query_builder(url=url, - id_pattern=id_pattern, - entity_type=entity_type, - from_date=from_date, - to_date=to_date, - limit=limit, - offset=offset) - + url = urljoin(self.base_url, "v2/entities") + res = self.__query_builder( + url=url, + id_pattern=id_pattern, + entity_type=entity_type, + from_date=from_date, + to_date=to_date, + limit=limit, + offset=offset, + ) + ta = TypeAdapter(List[TimeSeriesHeader]) return ta.validate_python(res[0]) # /entities/{entityId} - def get_entity_by_id(self, - entity_id: str, - *, - attrs: str = None, - entity_type: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None - ) -> TimeSeries: - + def get_entity_by_id( + self, + entity_id: str, + *, + attrs: str = None, + entity_type: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + ) -> TimeSeries: """ History of N attributes of a given entity instance For example, query max water level of the central tank throughout the @@ -542,21 +552,23 @@ def get_entity_by_id(self, Returns: TimeSeries """ - url = urljoin(self.base_url, f'v2/entities/{entity_id}') - res_q = self.__query_builder(url=url, - attrs=attrs, - options=options, - entity_type=entity_type, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords) + url = urljoin(self.base_url, f"v2/entities/{entity_id}") + res_q = self.__query_builder( + url=url, + attrs=attrs, + options=options, + entity_type=entity_type, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + ) # merge response chunks res = TimeSeries.model_validate(res_q.popleft()) for item in res_q: @@ -565,23 +577,24 @@ def get_entity_by_id(self, return res # /entities/{entityId}/value - def get_entity_values_by_id(self, - entity_id: str, - *, - attrs: str = None, - entity_type: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None - ) -> TimeSeries: + def get_entity_values_by_id( + self, + entity_id: str, + *, + attrs: str = None, + entity_type: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + ) -> TimeSeries: """ History of N attributes (values only) of a given entity instance For example, query the average pressure, temperature and humidity ( @@ -612,21 +625,23 @@ def get_entity_values_by_id(self, Returns: Response Model """ - url = urljoin(self.base_url, f'v2/entities/{entity_id}/value') - res_q = self.__query_builder(url=url, - attrs=attrs, - options=options, - entity_type=entity_type, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords) + url = urljoin(self.base_url, f"v2/entities/{entity_id}/value") + res_q = self.__query_builder( + url=url, + attrs=attrs, + options=options, + entity_type=entity_type, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + ) # merge response chunks res = TimeSeries(entityId=entity_id, **res_q.popleft()) @@ -636,23 +651,24 @@ def get_entity_values_by_id(self, return res # /entities/{entityId}/attrs/{attrName} - def get_entity_attr_by_id(self, - entity_id: str, - attr_name: str, - *, - entity_type: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None - ) -> TimeSeries: + def get_entity_attr_by_id( + self, + entity_id: str, + attr_name: str, + *, + entity_type: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + ) -> TimeSeries: """ History of an attribute of a given entity instance For example, query max water level of the central tank throughout the @@ -683,53 +699,61 @@ def get_entity_attr_by_id(self, Returns: Response Model """ - url = urljoin(self.base_url, f'v2/entities/{entity_id}/attrs' - f'/{attr_name}') - req_q = self.__query_builder(url=url, - entity_id=entity_id, - options=options, - entity_type=entity_type, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords) + url = urljoin(self.base_url, f"v2/entities/{entity_id}/attrs" f"/{attr_name}") + req_q = self.__query_builder( + url=url, + entity_id=entity_id, + options=options, + entity_type=entity_type, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + ) # merge response chunks first = req_q.popleft() - res = TimeSeries(entityId=entity_id, - index=first.get('index'), - attributes=[AttributeValues(**first)]) + res = TimeSeries( + entityId=entity_id, + index=first.get("index"), + attributes=[AttributeValues(**first)], + ) for item in req_q: - res.extend(TimeSeries(entityId=entity_id, - index=item.get('index'), - attributes=[AttributeValues(**item)])) + res.extend( + TimeSeries( + entityId=entity_id, + index=item.get("index"), + attributes=[AttributeValues(**item)], + ) + ) return res # /entities/{entityId}/attrs/{attrName}/value - def get_entity_attr_values_by_id(self, - entity_id: str, - attr_name: str, - *, - entity_type: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None - ) -> TimeSeries: + def get_entity_attr_values_by_id( + self, + entity_id: str, + attr_name: str, + *, + entity_type: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + ) -> TimeSeries: """ History of an attribute (values only) of a given entity instance Similar to the previous, but focusing on the values regardless of the @@ -759,169 +783,192 @@ def get_entity_attr_values_by_id(self, Returns: Response Model """ - url = urljoin(self.base_url, f'v2/entities/{entity_id}/attrs' - f'/{attr_name}/value') - res_q = self.__query_builder(url=url, - options=options, - entity_type=entity_type, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords) + url = urljoin( + self.base_url, f"v2/entities/{entity_id}/attrs" f"/{attr_name}/value" + ) + res_q = self.__query_builder( + url=url, + options=options, + entity_type=entity_type, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + ) # merge response chunks first = res_q.popleft() res = TimeSeries( entityId=entity_id, - index=first.get('index'), - attributes=[AttributeValues(attrName=attr_name, - values=first.get('values'))]) + index=first.get("index"), + attributes=[ + AttributeValues(attrName=attr_name, values=first.get("values")) + ], + ) for item in res_q: res.extend( TimeSeries( entityId=entity_id, - index=item.get('index'), - attributes=[AttributeValues(attrName=attr_name, - values=item.get('values'))])) + index=item.get("index"), + attributes=[ + AttributeValues(attrName=attr_name, values=item.get("values")) + ], + ) + ) return res # /types/{entityType} - def get_entity_by_type(self, - entity_type: str, - *, - attrs: str = None, - entity_id: str = None, - id_pattern: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None, - aggr_scope: Union[str, AggrScope] = None - ) -> List[TimeSeries]: + def get_entity_by_type( + self, + entity_type: str, + *, + attrs: str = None, + entity_id: str = None, + id_pattern: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + aggr_scope: Union[str, AggrScope] = None, + ) -> List[TimeSeries]: """ History of N attributes of N entities of the same type. For example, query the average pressure, temperature and humidity of this month in all the weather stations. """ - url = urljoin(self.base_url, f'v2/types/{entity_type}') - res_q = self.__query_builder(url=url, - entity_id=entity_id, - id_pattern=id_pattern, - attrs=attrs, - options=options, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords, - aggr_scope=aggr_scope) + url = urljoin(self.base_url, f"v2/types/{entity_type}") + res_q = self.__query_builder( + url=url, + entity_id=entity_id, + id_pattern=id_pattern, + attrs=attrs, + options=options, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + aggr_scope=aggr_scope, + ) # merge chunks of response - res = [TimeSeries(entityType=entity_type, **item) - for item in res_q.popleft().get('entities')] + res = [ + TimeSeries(entityType=entity_type, **item) + for item in res_q.popleft().get("entities") + ] for chunk in res_q: - chunk = [TimeSeries(entityType=entity_type, **item) - for item in chunk.get('entities')] + chunk = [ + TimeSeries(entityType=entity_type, **item) + for item in chunk.get("entities") + ] for new, old in zip(chunk, res): old.extend(new) return res # /types/{entityType}/value - def get_entity_values_by_type(self, - entity_type: str, - *, - attrs: str = None, - entity_id: str = None, - id_pattern: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None, - aggr_scope: Union[str, AggrScope] = None - ) -> List[TimeSeries]: + def get_entity_values_by_type( + self, + entity_type: str, + *, + attrs: str = None, + entity_id: str = None, + id_pattern: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + aggr_scope: Union[str, AggrScope] = None, + ) -> List[TimeSeries]: """ History of N attributes (values only) of N entities of the same type. For example, query the average pressure, temperature and humidity ( values only, no metadata) of this month in all the weather stations. """ - url = urljoin(self.base_url, f'v2/types/{entity_type}/value') - res_q = self.__query_builder(url=url, - entity_id=entity_id, - id_pattern=id_pattern, - attrs=attrs, - options=options, - entity_type=entity_type, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords, - aggr_scope=aggr_scope) + url = urljoin(self.base_url, f"v2/types/{entity_type}/value") + res_q = self.__query_builder( + url=url, + entity_id=entity_id, + id_pattern=id_pattern, + attrs=attrs, + options=options, + entity_type=entity_type, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + aggr_scope=aggr_scope, + ) # merge chunks of response - res = [TimeSeries(entityType=entity_type, **item) - for item in res_q.popleft().get('values')] + res = [ + TimeSeries(entityType=entity_type, **item) + for item in res_q.popleft().get("values") + ] for chunk in res_q: - chunk = [TimeSeries(entityType=entity_type, **item) - for item in chunk.get('values')] + chunk = [ + TimeSeries(entityType=entity_type, **item) + for item in chunk.get("values") + ] for new, old in zip(chunk, res): old.extend(new) return res # /types/{entityType}/attrs/{attrName} - def get_entity_attr_by_type(self, - entity_type: str, - attr_name: str, - *, - entity_id: str = None, - id_pattern: str = None, - aggr_method: Union[str, AggrMethod] = None, - aggr_period: Union[str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None, - aggr_scope: Union[str, AggrScope] = None - ) -> List[TimeSeries]: + def get_entity_attr_by_type( + self, + entity_type: str, + attr_name: str, + *, + entity_id: str = None, + id_pattern: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + aggr_scope: Union[str, AggrScope] = None, + ) -> List[TimeSeries]: """ History of an attribute of N entities of the same type. For example, query the pressure measurements of this month in all the @@ -961,72 +1008,82 @@ def get_entity_attr_by_type(self, Returns: Response Model """ - url = urljoin(self.base_url, f'v2/types/{entity_type}/attrs' - f'/{attr_name}') - res_q = self.__query_builder(url=url, - entity_id=entity_id, - id_pattern=id_pattern, - options=options, - entity_type=entity_type, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords, - aggr_scope=aggr_scope) + url = urljoin(self.base_url, f"v2/types/{entity_type}/attrs" f"/{attr_name}") + res_q = self.__query_builder( + url=url, + entity_id=entity_id, + id_pattern=id_pattern, + options=options, + entity_type=entity_type, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + aggr_scope=aggr_scope, + ) # merge chunks of response first = res_q.popleft() - res = [TimeSeries(index=item.get('index'), - entityType=entity_type, - entityId=item.get('entityId'), - attributes=[ - AttributeValues( - attrName=first.get('attrName'), - values=item.get('values'))]) - for item in first.get('entities')] + res = [ + TimeSeries( + index=item.get("index"), + entityType=entity_type, + entityId=item.get("entityId"), + attributes=[ + AttributeValues( + attrName=first.get("attrName"), values=item.get("values") + ) + ], + ) + for item in first.get("entities") + ] for chunk in res_q: - chunk = [TimeSeries(index=item.get('index'), - entityType=entity_type, - entityId=item.get('entityId'), - attributes=[ - AttributeValues( - attrName=chunk.get('attrName'), - values=item.get('values'))]) - for item in chunk.get('entities')] + chunk = [ + TimeSeries( + index=item.get("index"), + entityType=entity_type, + entityId=item.get("entityId"), + attributes=[ + AttributeValues( + attrName=chunk.get("attrName"), values=item.get("values") + ) + ], + ) + for item in chunk.get("entities") + ] for new, old in zip(chunk, res): old.extend(new) return res # /types/{entityType}/attrs/{attrName}/value - def get_entity_attr_values_by_type(self, - entity_type: str, - attr_name: str, - *, - entity_id: str = None, - id_pattern: str = None, - aggr_method: Union[ - str, AggrMethod] = None, - aggr_period: Union[ - str, AggrPeriod] = None, - from_date: str = None, - to_date: str = None, - last_n: int = None, - limit: int = 10000, - offset: int = None, - georel: str = None, - geometry: str = None, - coords: str = None, - options: str = None, - aggr_scope: Union[str, AggrScope] = None - ) -> List[TimeSeries]: + def get_entity_attr_values_by_type( + self, + entity_type: str, + attr_name: str, + *, + entity_id: str = None, + id_pattern: str = None, + aggr_method: Union[str, AggrMethod] = None, + aggr_period: Union[str, AggrPeriod] = None, + from_date: str = None, + to_date: str = None, + last_n: int = None, + limit: int = 10000, + offset: int = None, + georel: str = None, + geometry: str = None, + coords: str = None, + options: str = None, + aggr_scope: Union[str, AggrScope] = None, + ) -> List[TimeSeries]: """ History of an attribute (values only) of N entities of the same type. For example, query the average pressure (values only, no metadata) of @@ -1058,55 +1115,68 @@ def get_entity_attr_values_by_type(self, Returns: Response Model """ - url = urljoin(self.base_url, f'v2/types/{entity_type}/attrs/' - f'{attr_name}/value') - res_q = self.__query_builder(url=url, - entity_id=entity_id, - id_pattern=id_pattern, - options=options, - entity_type=entity_type, - aggr_method=aggr_method, - aggr_period=aggr_period, - from_date=from_date, - to_date=to_date, - last_n=last_n, - limit=limit, - offset=offset, - georel=georel, - geometry=geometry, - coords=coords, - aggr_scope=aggr_scope) + url = urljoin( + self.base_url, f"v2/types/{entity_type}/attrs/" f"{attr_name}/value" + ) + res_q = self.__query_builder( + url=url, + entity_id=entity_id, + id_pattern=id_pattern, + options=options, + entity_type=entity_type, + aggr_method=aggr_method, + aggr_period=aggr_period, + from_date=from_date, + to_date=to_date, + last_n=last_n, + limit=limit, + offset=offset, + georel=georel, + geometry=geometry, + coords=coords, + aggr_scope=aggr_scope, + ) # merge chunks of response - res = [TimeSeries(index=item.get('index'), - entityType=entity_type, - entityId=item.get('entityId'), - attributes=[ - AttributeValues(attrName=attr_name, - values=item.get('values'))]) - for item in res_q.popleft().get('values')] + res = [ + TimeSeries( + index=item.get("index"), + entityType=entity_type, + entityId=item.get("entityId"), + attributes=[ + AttributeValues(attrName=attr_name, values=item.get("values")) + ], + ) + for item in res_q.popleft().get("values") + ] for chunk in res_q: - chunk = [TimeSeries(index=item.get('index'), - entityType=entity_type, - entityId=item.get('entityId'), - attributes=[ - AttributeValues(attrName=attr_name, - values=item.get('values'))]) - for item in chunk.get('values')] + chunk = [ + TimeSeries( + index=item.get("index"), + entityType=entity_type, + entityId=item.get("entityId"), + attributes=[ + AttributeValues(attrName=attr_name, values=item.get("values")) + ], + ) + for item in chunk.get("values") + ] for new, old in zip(chunk, res): old.extend(new) return res # v2/attrs - def get_entity_by_attrs(self, *, - entity_type: str = None, - from_date: str = None, - to_date: str = None, - limit: int = 10000, - offset: int = None - ) -> List[TimeSeries]: + def get_entity_by_attrs( + self, + *, + entity_type: str = None, + from_date: str = None, + to_date: str = None, + limit: int = 10000, + offset: int = None, + ) -> List[TimeSeries]: """ Get list of timeseries data grouped by each existing attribute name. The timeseries data include all entities corresponding to each @@ -1128,39 +1198,45 @@ def get_entity_by_attrs(self, *, limit (int): Maximum number of results to be retrieved. Default value : 10000 offset (int): Offset for the results. - + Returns: List of TimeSeriesEntities """ - url = urljoin(self.base_url, 'v2/attrs') - res_q = self.__query_builder(url=url, - entity_type=entity_type, - from_date=from_date, - to_date=to_date, - limit=limit, - offset=offset) + url = urljoin(self.base_url, "v2/attrs") + res_q = self.__query_builder( + url=url, + entity_type=entity_type, + from_date=from_date, + to_date=to_date, + limit=limit, + offset=offset, + ) first = res_q.popleft() - - res = chain.from_iterable(map(lambda x: self.transform_attr_response_model(x), - first.get("attrs"))) + + res = chain.from_iterable( + map(lambda x: self.transform_attr_response_model(x), first.get("attrs")) + ) for chunk in res_q: - chunk = chain.from_iterable(map(lambda x: self.transform_attr_response_model(x), - chunk.get("attrs"))) - + chunk = chain.from_iterable( + map(lambda x: self.transform_attr_response_model(x), chunk.get("attrs")) + ) + for new, old in zip(chunk, res): old.extend(new) - + return list(res) # v2/attrs/{attr_name} - def get_entity_by_attr_name(self, *, - attr_name: str, - entity_type: str = None, - from_date: str = None, - to_date: str = None, - limit: int = 10000, - offset: int = None - ) -> List[TimeSeries]: + def get_entity_by_attr_name( + self, + *, + attr_name: str, + entity_type: str = None, + from_date: str = None, + to_date: str = None, + limit: int = 10000, + offset: int = None, + ) -> List[TimeSeries]: """ Get list of all entities containing this attribute name, as well as getting the index and values of this attribute in every corresponding @@ -1186,13 +1262,15 @@ def get_entity_by_attr_name(self, *, Returns: List of TimeSeries """ - url = urljoin(self.base_url, f'/v2/attrs/{attr_name}') - res_q = self.__query_builder(url=url, - entity_type=entity_type, - from_date=from_date, - to_date=to_date, - limit=limit, - offset=offset) + url = urljoin(self.base_url, f"/v2/attrs/{attr_name}") + res_q = self.__query_builder( + url=url, + entity_type=entity_type, + from_date=from_date, + to_date=to_date, + limit=limit, + offset=offset, + ) first = res_q.popleft() res = self.transform_attr_response_model(first) @@ -1207,14 +1285,16 @@ def transform_attr_response_model(self, attr_response): res = [] attr_name = attr_response.get("attrName") for entity_group in attr_response.get("types"): - timeseries = map(lambda entity: - TimeSeries(entityId=entity.get("entityId"), - entityType=entity_group.get("entityType"), - index=entity.get("index"), - attributes=[ - AttributeValues(attrName=attr_name, - values=entity.get("values"))] - ), - entity_group.get("entities")) + timeseries = map( + lambda entity: TimeSeries( + entityId=entity.get("entityId"), + entityType=entity_group.get("entityType"), + index=entity.get("index"), + attributes=[ + AttributeValues(attrName=attr_name, values=entity.get("values")) + ], + ), + entity_group.get("entities"), + ) res.append(timeseries) return chain.from_iterable(res) diff --git a/filip/config.py b/filip/config.py index 075fe357..fa709d7e 100644 --- a/filip/config.py +++ b/filip/config.py @@ -4,12 +4,14 @@ `*.env` belongs to best practices in containerized applications. Pydantic provides a convenient and clean way to manage environments. """ + from pydantic import Field, AnyHttpUrl, AliasChoices, AnyUrl from pydantic_settings import BaseSettings, SettingsConfigDict from pathlib import Path import os from dotenv import find_dotenv -ROOT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) + +ROOT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) class Settings(BaseSettings): @@ -18,37 +20,47 @@ class Settings(BaseSettings): file or environment variables. The `.env.filip` can be located anywhere in the FiLiP repository. """ - model_config = SettingsConfigDict(env_file=find_dotenv(Path(ROOT_DIR) / '.env.filip'), - env_file_encoding='utf-8', - case_sensitive=False, extra="ignore") - - CB_URL: AnyHttpUrl = Field(default="http://127.0.0.1:1026", - validation_alias=AliasChoices( - 'ORION_URL', 'CB_URL', 'CB_HOST', - 'CONTEXTBROKER_URL', 'OCB_URL')) - LD_CB_URL: AnyHttpUrl = Field(default="http://127.0.0.1:1027", - validation_alias=AliasChoices('LD_ORION_URL', - 'LD_CB_URL', - 'ORION_LD_URL', - 'SCORPIO_URL', - 'STELLIO_URL')) - - IOTA_URL: AnyHttpUrl = Field(default="http://127.0.0.1:4041", - validation_alias='IOTA_URL') - - QL_URL: AnyHttpUrl = Field(default="http://127.0.0.1:8668", - validation_alias=AliasChoices('QUANTUMLEAP_URL', 'QL_URL')) - - MQTT_BROKER_URL: AnyUrl = Field(default="mqtt://127.0.0.1:1883", - validation_alias=AliasChoices( - 'MQTT_BROKER_URL', - 'MQTT_URL', - 'MQTT_BROKER')) - LD_MQTT_BROKER_URL: AnyUrl = Field(default="mqtt://127.0.0.1:1884", - validation_alias=AliasChoices( - 'LD_MQTT_BROKER_URL', - 'LD_MQTT_URL', - 'LD_MQTT_BROKER')) + + model_config = SettingsConfigDict( + env_file=find_dotenv(Path(ROOT_DIR) / ".env.filip"), + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + CB_URL: AnyHttpUrl = Field( + default="http://127.0.0.1:1026", + validation_alias=AliasChoices( + "ORION_URL", "CB_URL", "CB_HOST", "CONTEXTBROKER_URL", "OCB_URL" + ), + ) + LD_CB_URL: AnyHttpUrl = Field( + default="http://127.0.0.1:1027", + validation_alias=AliasChoices( + "LD_ORION_URL", "LD_CB_URL", "ORION_LD_URL", "SCORPIO_URL", "STELLIO_URL" + ), + ) + + IOTA_URL: AnyHttpUrl = Field( + default="http://127.0.0.1:4041", validation_alias="IOTA_URL" + ) + + QL_URL: AnyHttpUrl = Field( + default="http://127.0.0.1:8668", + validation_alias=AliasChoices("QUANTUMLEAP_URL", "QL_URL"), + ) + + MQTT_BROKER_URL: AnyUrl = Field( + default="mqtt://127.0.0.1:1883", + validation_alias=AliasChoices("MQTT_BROKER_URL", "MQTT_URL", "MQTT_BROKER"), + ) + LD_MQTT_BROKER_URL: AnyUrl = Field( + default="mqtt://127.0.0.1:1884", + validation_alias=AliasChoices( + "LD_MQTT_BROKER_URL", "LD_MQTT_URL", "LD_MQTT_BROKER" + ), + ) + # create settings object settings = Settings() diff --git a/filip/custom_types.py b/filip/custom_types.py index 0b3360f0..c5dd38c9 100644 --- a/filip/custom_types.py +++ b/filip/custom_types.py @@ -1,8 +1,9 @@ """ Variable types and classes used for better validation """ + from pydantic import UrlConstraints from typing_extensions import Annotated from pydantic_core import Url -AnyMqttUrl = Annotated[Url, UrlConstraints(allowed_schemes=['mqtt'])] +AnyMqttUrl = Annotated[Url, UrlConstraints(allowed_schemes=["mqtt"])] diff --git a/filip/models/base.py b/filip/models/base.py index 6bbcb70a..0492c72b 100644 --- a/filip/models/base.py +++ b/filip/models/base.py @@ -5,8 +5,7 @@ from aenum import Enum from pydantic import ConfigDict, BaseModel, Field, field_validator, computed_field -from filip.utils.validators import (validate_fiware_service_path, - validate_fiware_service) +from filip.utils.validators import validate_fiware_service_path, validate_fiware_service core_context = "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.6.jsonld" @@ -60,9 +59,9 @@ class DataType(str, Enum): "hh:mm:ss[Z|(+|-)hh:mm] (see XML schema for details).", ) RELATIONSHIP = "Relationship", "Reference to another context entity" - STRUCTUREDVALUE = "StructuredValue", ("Structured datatype must be an " - "array or object" - "serializable") + STRUCTUREDVALUE = "StructuredValue", ( + "Structured datatype must be an " "array or object" "serializable" + ) ARRAY = "Array", "Array of the types above" OBJECT = "Object", "JSON-Object of the types above, i.e. a dictionary" COMMAND = "command", "A command for IoT Devices" @@ -129,7 +128,7 @@ class FiwareHeaderSecure(FiwareHeader): default="", max_length=3000, description="authorization key", - pattern=r".*" + pattern=r".*", ) @@ -163,21 +162,23 @@ class FiwareLDHeader(BaseModel): Context Brokers to support hierarchical scopes: https://fiware-orion.readthedocs.io/en/master/user/service_path/index.html """ + model_config = ConfigDict(populate_by_name=True, validate_assignment=True) ngsild_tenant: str = Field( alias="NGSILD-Tenant", default=None, max_length=50, description="Alias to the Fiware service to used for multitenancy", - pattern=r"\w*$" + pattern=r"\w*$", ) link_header: str = Field( alias="Link", - default=f'<{core_context}>; ' - 'rel="http://www.w3.org/ns/json-ld#context"; ' - 'type="application/ld+json"', + default=f"<{core_context}>; " + 'rel="http://www.w3.org/ns/json-ld#context"; ' + 'type="application/ld+json"', description="Fiware service used for multi-tenancy", - pattern=r"\w*$") + pattern=r"\w*$", + ) # @computed_field # def Link(self) -> str: # link_header = f'<{self.context}>; ' \ diff --git a/filip/models/mqtt.py b/filip/models/mqtt.py index eed67684..5c20050e 100644 --- a/filip/models/mqtt.py +++ b/filip/models/mqtt.py @@ -1,6 +1,7 @@ """ Module contains models for MQTT communication with FIWARE's IoT-Agents. """ + from aenum import Enum @@ -8,9 +9,10 @@ class IoTAMQTTMessageType(str, Enum): """ Options for mqtt message type """ - _init_ = 'value __doc__' + + _init_ = "value __doc__" CMD = "cmd", "Command" CMDEXE = "cmdexe", "Command acknowledgement" - MULTI = "multi", "Multi measurement" + MULTI = "multi", "Multi measurement" SINGLE = "single", "Single measurement" - CONFIG = "configuration", "Configuration message" \ No newline at end of file + CONFIG = "configuration", "Configuration message" diff --git a/filip/models/ngsi_ld/__init__.py b/filip/models/ngsi_ld/__init__.py index 24e0e438..ad9b4135 100644 --- a/filip/models/ngsi_ld/__init__.py +++ b/filip/models/ngsi_ld/__init__.py @@ -1 +1 @@ -"""This package will contain models for FIWAREs NGSI-LD APIs""" \ No newline at end of file +"""This package will contain models for FIWAREs NGSI-LD APIs""" diff --git a/filip/models/ngsi_ld/base.py b/filip/models/ngsi_ld/base.py index 6e58e9f0..d0dbb37a 100644 --- a/filip/models/ngsi_ld/base.py +++ b/filip/models/ngsi_ld/base.py @@ -6,20 +6,20 @@ class GeoQuery(BaseModel): """ GeoQuery used for Subscriptions, as described in NGSI-LD Spec section 5.2.13 """ + geometry: str = Field( description="A valid GeoJSON [8] geometry, type excepting GeometryCollection" ) coordinates: Union[list, str] = Field( description="A JSON Array coherent with the geometry type as per " - "IETF RFC 7946 [8]" + "IETF RFC 7946 [8]" ) georel: str = Field( description="A valid geo-relationship as defined by clause 4.10 (near, " - "within, etc.)" + "within, etc.)" ) geoproperty: Optional[str] = Field( - default=None, - description="Attribute Name as a short-hand string" + default=None, description="Attribute Name as a short-hand string" ) model_config = ConfigDict(populate_by_name=True) diff --git a/filip/models/ngsi_ld/context.py b/filip/models/ngsi_ld/context.py index 13f7613a..78f1eaa9 100644 --- a/filip/models/ngsi_ld/context.py +++ b/filip/models/ngsi_ld/context.py @@ -1,16 +1,26 @@ """ NGSI LD models for context broker interaction """ + import logging from typing import Any, List, Dict, Union, Optional -from geojson_pydantic import Point, MultiPoint, LineString, MultiLineString, Polygon, \ - MultiPolygon +from geojson_pydantic import ( + Point, + MultiPoint, + LineString, + MultiLineString, + Polygon, + MultiPolygon, +) from typing_extensions import Self from aenum import Enum from pydantic import field_validator, ConfigDict, BaseModel, Field, model_validator from filip.models.ngsi_v2 import ContextEntity -from filip.utils.validators import FiwareRegex, \ - validate_fiware_datatype_string_protect, validate_fiware_standard_regex +from filip.utils.validators import ( + FiwareRegex, + validate_fiware_datatype_string_protect, + validate_fiware_standard_regex, +) from pydantic_core import ValidationError @@ -18,10 +28,14 @@ class DataTypeLD(str, Enum): """ In NGSI-LD the data types on context entities are only divided into properties and relationships. """ - _init_ = 'value __doc__' + + _init_ = "value __doc__" GEOPROPERTY = "GeoProperty", "A property that represents a geometry value" PROPERTY = "Property", "All attributes that do not represent a relationship" - RELATIONSHIP = "Relationship", "Reference to another context entity, which can be identified with a URN." + RELATIONSHIP = ( + "Relationship", + "Reference to another context entity, which can be identified with a URN.", + ) # NGSI-LD entity models @@ -40,59 +54,60 @@ class ContextProperty(BaseModel): >>> attr = ContextProperty(**data) """ - model_config = ConfigDict(extra='allow') # In order to allow nested properties - type: Optional[str] = Field( - default="Property", - title="type", - frozen=True - ) - value: Optional[Union[Union[float, int, bool, str, List, Dict[str, Any]], - List[Union[float, int, bool, str, List, - Dict[str, Any]]]]] = Field( - default=None, - title="Property value", - description="the actual data" - ) + + model_config = ConfigDict(extra="allow") # In order to allow nested properties + type: Optional[str] = Field(default="Property", title="type", frozen=True) + value: Optional[ + Union[ + Union[float, int, bool, str, List, Dict[str, Any]], + List[Union[float, int, bool, str, List, Dict[str, Any]]], + ] + ] = Field(default=None, title="Property value", description="the actual data") observedAt: Optional[str] = Field( - None, title="Timestamp", + None, + title="Timestamp", description="Representing a timestamp for the " - "incoming value of the property.", + "incoming value of the property.", max_length=256, min_length=1, ) field_validator("observedAt")(validate_fiware_datatype_string_protect) createdAt: Optional[str] = Field( - None, title="Timestamp", + None, + title="Timestamp", description="Representing a timestamp for the " - "creation time of the property.", + "creation time of the property.", max_length=256, min_length=1, ) field_validator("createdAt")(validate_fiware_datatype_string_protect) modifiedAt: Optional[str] = Field( - None, title="Timestamp", + None, + title="Timestamp", description="Representing a timestamp for the " - "last modification of the property.", + "last modification of the property.", max_length=256, min_length=1, ) field_validator("modifiedAt")(validate_fiware_datatype_string_protect) UnitCode: Optional[str] = Field( - None, title="Unit Code", + None, + title="Unit Code", description="Representing the unit of the value. " - "Should be part of the defined units " - "by the UN/ECE Recommendation No. 21" - "https://unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex2e.pdf ", + "Should be part of the defined units " + "by the UN/ECE Recommendation No. 21" + "https://unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex2e.pdf ", max_length=256, min_length=1, ) field_validator("UnitCode")(validate_fiware_datatype_string_protect) datasetId: Optional[str] = Field( - None, title="dataset Id", + None, + title="dataset Id", description="It allows identifying a set or group of property values", max_length=256, min_length=1, @@ -104,9 +119,10 @@ def get_model_fields_set(cls): """ Get all names and aliases of the model fields. """ - return set([field.validation_alias - for (_, field) in cls.model_fields.items()] + - [field_name for field_name in cls.model_fields]) + return set( + [field.validation_alias for (_, field) in cls.model_fields.items()] + + [field_name for field_name in cls.model_fields] + ) @field_validator("type") @classmethod @@ -120,8 +136,10 @@ def check_property_type(cls, value): """ valid_property_types = ["Property", "Relationship", "TemporalProperty"] if value not in valid_property_types: - msg = f'NGSI_LD Properties must have type {valid_property_types}, ' \ - f'not "{value}"' + msg = ( + f"NGSI_LD Properties must have type {valid_property_types}, " + f'not "{value}"' + ) logging.warning(msg=msg) raise ValueError(msg) return value @@ -134,13 +152,14 @@ class NamedContextProperty(ContextProperty): In the NGSI-LD data model, properties have a name, the type "property" and a value. """ + name: str = Field( title="Property name", description="The property name describes what kind of property the " - "attribute value represents of the entity, for example " - "current_speed. Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "attribute value represents of the entity, for example " + "current_speed. Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) @@ -165,14 +184,11 @@ class ContextGeoPropertyValue(BaseModel): } """ - type: Optional[str] = Field( - default=None, - title="type", - frozen=True - ) - model_config = ConfigDict(extra='allow') - @model_validator(mode='after') + type: Optional[str] = Field(default=None, title="type", frozen=True) + model_config = ConfigDict(extra="allow") + + @model_validator(mode="after") def check_geoproperty_value(self) -> Self: """ Check if the value is a valid GeoProperty @@ -212,32 +228,33 @@ class ContextGeoProperty(BaseModel): } """ - model_config = ConfigDict(extra='allow') - type: Optional[str] = Field( - default="GeoProperty", - title="type", - frozen=True - ) - value: Optional[Union[ContextGeoPropertyValue, - Point, LineString, Polygon, - MultiPoint, MultiPolygon, - MultiLineString]] = Field( - default=None, - title="GeoProperty value", - description="the actual data" - ) + + model_config = ConfigDict(extra="allow") + type: Optional[str] = Field(default="GeoProperty", title="type", frozen=True) + value: Optional[ + Union[ + ContextGeoPropertyValue, + Point, + LineString, + Polygon, + MultiPoint, + MultiPolygon, + MultiLineString, + ] + ] = Field(default=None, title="GeoProperty value", description="the actual data") observedAt: Optional[str] = Field( default=None, title="Timestamp", description="Representing a timestamp for the " - "incoming value of the property.", + "incoming value of the property.", max_length=256, min_length=1, ) field_validator("observedAt")(validate_fiware_datatype_string_protect) datasetId: Optional[str] = Field( - None, title="dataset Id", + None, + title="dataset Id", description="It allows identifying a set or group of property values", max_length=256, min_length=1, @@ -251,13 +268,14 @@ class NamedContextGeoProperty(ContextGeoProperty): In the NGSI-LD data model, properties have a name, the type "Geoproperty" and a value. """ + name: str = Field( title="Property name", description="The property name describes what kind of property the " - "attribute value represents of the entity, for example " - "current_speed. Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "attribute value represents of the entity, for example " + "current_speed. Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) @@ -280,22 +298,21 @@ class ContextRelationship(BaseModel): >>> attr = ContextRelationship(**data) """ - model_config = ConfigDict(extra='allow') # In order to allow nested relationships - type: Optional[str] = Field( - default="Relationship", - title="type", - frozen=True - ) - object: Optional[Union[Union[float, int, bool, str, List, Dict[str, Any]], - List[Union[float, int, bool, str, List, - Dict[str, Any]]]]] = Field( - default=None, - title="Realtionship object", - description="the actual object id" + + model_config = ConfigDict(extra="allow") # In order to allow nested relationships + type: Optional[str] = Field(default="Relationship", title="type", frozen=True) + object: Optional[ + Union[ + Union[float, int, bool, str, List, Dict[str, Any]], + List[Union[float, int, bool, str, List, Dict[str, Any]]], + ] + ] = Field( + default=None, title="Realtionship object", description="the actual object id" ) datasetId: Optional[str] = Field( - None, title="dataset Id", + None, + title="dataset Id", description="It allows identifying a set or group of property values", max_length=256, min_length=1, @@ -303,9 +320,10 @@ class ContextRelationship(BaseModel): field_validator("datasetId")(validate_fiware_datatype_string_protect) observedAt: Optional[str] = Field( - None, titel="Timestamp", + None, + titel="Timestamp", description="Representing a timestamp for the " - "incoming value of the property.", + "incoming value of the property.", max_length=256, min_length=1, ) @@ -335,13 +353,14 @@ class NamedContextRelationship(ContextRelationship): In the NGSI-LD data model, relationships have a name, the type "relationship" and an object. """ + name: str = Field( title="Attribute name", description="The attribute name describes what kind of property the " - "attribute value represents of the entity, for example " - "current_speed. Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "attribute value represents of the entity, for example " + "current_speed. Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, # pattern=FiwareRegex.string_protect.value, @@ -362,35 +381,37 @@ class ContextLDEntityKeyValues(BaseModel): is a string containing the entity's type name. """ - model_config = ConfigDict(extra='allow', validate_default=True, - validate_assignment=True) + + model_config = ConfigDict( + extra="allow", validate_default=True, validate_assignment=True + ) id: str = Field( ..., title="Entity Id", description="Id of an entity in an NGSI context broker. Allowed " - "characters are the ones in the plain ASCII set, except " - "the following ones: control characters, " - "whitespace, &, ?, / and #." - "the id should be structured according to the urn naming scheme.", - json_schema_extra={"example":"urn:ngsi-ld:Room:001"}, + "characters are the ones in the plain ASCII set, except " + "the following ones: control characters, " + "whitespace, &, ?, / and #." + "the id should be structured according to the urn naming scheme.", + json_schema_extra={"example": "urn:ngsi-ld:Room:001"}, max_length=256, min_length=1, # pattern=FiwareRegex.standard.value, # Make it FIWARE-Safe - frozen=True + frozen=True, ) field_validator("id")(validate_fiware_standard_regex) type: str = Field( ..., title="Entity Type", description="Id of an entity in an NGSI context broker. " - "Allowed characters are the ones in the plain ASCII set, " - "except the following ones: control characters, " - "whitespace, &, ?, / and #.", - json_schema_extra={"example":"Room"}, + "Allowed characters are the ones in the plain ASCII set, " + "except the following ones: control characters, " + "whitespace, &, ?, / and #.", + json_schema_extra={"example": "Room"}, max_length=256, min_length=1, # pattern=FiwareRegex.standard.value, # Make it FIWARE-Safe - frozen=True + frozen=True, ) field_validator("type")(validate_fiware_standard_regex) @@ -400,8 +421,9 @@ class PropertyFormat(str, Enum): Format to decide if properties of ContextEntity class are returned as List of NamedContextAttributes or as Dict of ContextAttributes. """ - LIST = 'list' - DICT = 'dict' + + LIST = "list" + DICT = "dict" class ContextLDEntity(ContextLDEntityKeyValues): @@ -437,36 +459,39 @@ class ContextLDEntity(ContextLDEntityKeyValues): >>> entity = ContextLDEntity(**data) """ - model_config = ConfigDict(extra='allow', - validate_default=True, - validate_assignment=True, - populate_by_name=True) + + model_config = ConfigDict( + extra="allow", + validate_default=True, + validate_assignment=True, + populate_by_name=True, + ) observationSpace: Optional[ContextGeoProperty] = Field( default=None, title="Observation Space", description="The geospatial Property representing " - "the geographic location that is being " - "observed, e.g. by a sensor. " - "For example, in the case of a camera, " - "the location of the camera and the " - "observationspace are different and " - "can be disjoint. " + "the geographic location that is being " + "observed, e.g. by a sensor. " + "For example, in the case of a camera, " + "the location of the camera and the " + "observationspace are different and " + "can be disjoint. ", ) context: Optional[Union[str, List[str], Dict]] = Field( title="@context", default=None, description="The @context in JSON-LD is used to expand terms, provided as short " - "hand strings, to concepts, specified as URIs, and vice versa, " - "to compact URIs into terms " - "The main implication of NGSI-LD API is that if the @context is " - "a compound one, i.e. an @context which references multiple " - "individual @context, served by resources behind different URIs, " - "then a wrapper @context has to be created and hosted.", + "hand strings, to concepts, specified as URIs, and vice versa, " + "to compact URIs into terms " + "The main implication of NGSI-LD API is that if the @context is " + "a compound one, i.e. an @context which references multiple " + "individual @context, served by resources behind different URIs, " + "then a wrapper @context has to be created and hosted.", examples=["https://n5geh.github.io/n5geh.test-context.io/context_saref.jsonld"], alias="@context", validation_alias="@context", - frozen=False + frozen=False, ) @field_validator("context") @@ -478,32 +503,33 @@ def return_context(cls, context): default=None, title="Operation Space", description="The geospatial Property representing " - "the geographic location in which an " - "Entity,e.g. an actuator is active. " - "For example, a crane can have a " - "certain operation space." + "the geographic location in which an " + "Entity,e.g. an actuator is active. " + "For example, a crane can have a " + "certain operation space.", ) createdAt: Optional[str] = Field( - None, title="Timestamp", + None, + title="Timestamp", description="Representing a timestamp for the " - "creation time of the property.", + "creation time of the property.", max_length=256, min_length=1, ) field_validator("createdAt")(validate_fiware_datatype_string_protect) modifiedAt: Optional[str] = Field( - None, title="Timestamp", + None, + title="Timestamp", description="Representing a timestamp for the " - "last modification of the property.", + "last modification of the property.", max_length=256, min_length=1, ) field_validator("modifiedAt")(validate_fiware_datatype_string_protect) - def __init__(self, - **data): + def __init__(self, **data): # There is currently no validation for extra fields data.update(self._validate_attributes(data)) super().__init__(**data) @@ -513,9 +539,10 @@ def get_model_fields_set(cls): """ Get all names and aliases of the model fields. """ - return set([field.validation_alias - for (_, field) in cls.model_fields.items()] + - [field_name for field_name in cls.model_fields]) + return set( + [field.validation_alias for (_, field) in cls.model_fields.items()] + + [field_name for field_name in cls.model_fields] + ) @classmethod def _validate_single_property(cls, attr) -> ContextProperty: @@ -532,8 +559,7 @@ def _validate_single_property(cls, attr) -> ContextProperty: elif attr.get("type") == "Property" or attr.get("type") is None: attr_instance = ContextProperty.model_validate(attr) else: - raise ValueError(f"Attribute {attr.get('type')} " - "is not a valid type") + raise ValueError(f"Attribute {attr.get('type')} " "is not a valid type") for subkey, subattr in attr.items(): if isinstance(subattr, dict) and subkey not in property_fields: attr_instance.model_extra.update( @@ -554,27 +580,22 @@ def _validate_attributes(cls, data: Dict): attrs[key] = cls._validate_single_property(attr=attr) return attrs - def model_dump( - self, - *args, - by_alias: bool = True, - **kwargs - ): + def model_dump(self, *args, by_alias: bool = True, **kwargs): return super().model_dump(*args, by_alias=by_alias, **kwargs) @field_validator("id") @classmethod def _validate_id(cls, id: str): if not id.startswith("urn:ngsi-ld:"): - logging.warning(msg='It is recommended that the entity id to be a URN,' - 'starting with the namespace "urn:ngsi-ld:"') + logging.warning( + msg="It is recommended that the entity id to be a URN," + 'starting with the namespace "urn:ngsi-ld:"' + ) return id - def get_properties(self, - response_format: Union[str, PropertyFormat] = - PropertyFormat.LIST) -> \ - Union[List[NamedContextProperty], - Dict[str, ContextProperty]]: + def get_properties( + self, response_format: Union[str, PropertyFormat] = PropertyFormat.LIST + ) -> Union[List[NamedContextProperty], Dict[str, ContextProperty]]: """ Get all properties of the entity. Args: @@ -589,10 +610,10 @@ def get_properties(self, final_dict = {} for key, value in self.model_dump(exclude_unset=True).items(): if key not in ContextLDEntity.get_model_fields_set(): - if value.get('type') != DataTypeLD.RELATIONSHIP: - if value.get('type') == DataTypeLD.GEOPROPERTY: + if value.get("type") != DataTypeLD.RELATIONSHIP: + if value.get("type") == DataTypeLD.GEOPROPERTY: final_dict[key] = ContextGeoProperty(**value) - elif value.get('type') == DataTypeLD.PROPERTY: + elif value.get("type") == DataTypeLD.PROPERTY: final_dict[key] = ContextProperty(**value) else: # named context property by default final_dict[key] = ContextProperty(**value) @@ -601,10 +622,10 @@ def get_properties(self, final_list = [] for key, value in self.model_dump(exclude_unset=True).items(): if key not in ContextLDEntity.get_model_fields_set(): - if value.get('type') != DataTypeLD.RELATIONSHIP: - if value.get('type') == DataTypeLD.GEOPROPERTY: + if value.get("type") != DataTypeLD.RELATIONSHIP: + if value.get("type") == DataTypeLD.GEOPROPERTY: final_list.append(NamedContextGeoProperty(name=key, **value)) - elif value.get('type') == DataTypeLD.PROPERTY: + elif value.get("type") == DataTypeLD.PROPERTY: final_list.append(NamedContextProperty(name=key, **value)) else: # named context property by default final_list.append(NamedContextProperty(name=key, **value)) @@ -614,29 +635,25 @@ def add_attributes(self, **kwargs): """ Invalid in NGSI-LD """ - raise NotImplementedError( - "This method should not be used in NGSI-LD") + raise NotImplementedError("This method should not be used in NGSI-LD") def get_attribute(self, **kwargs): """ Invalid in NGSI-LD """ - raise NotImplementedError( - "This method should not be used in NGSI-LD") + raise NotImplementedError("This method should not be used in NGSI-LD") def get_attributes(self, **kwargs): """ Invalid in NGSI-LD """ - raise NotImplementedError( - "This method should not be used in NGSI-LD") + raise NotImplementedError("This method should not be used in NGSI-LD") def delete_attributes(self, **kwargs): """ Invalid in NGSI-LD """ - raise NotImplementedError( - "This method should not be used in NGSI-LD") + raise NotImplementedError("This method should not be used in NGSI-LD") def delete_relationships(self, relationships: List[str]): """ @@ -648,16 +665,17 @@ def delete_relationships(self, relationships: List[str]): Returns: """ - all_relationships = self.get_relationships(response_format='dict') + all_relationships = self.get_relationships(response_format="dict") for relationship in relationships: # check they are relationships if relationship not in all_relationships: raise ValueError(f"Relationship {relationship} does not exist") delattr(self, relationship) - def delete_properties(self, props: Union[Dict[str, ContextProperty], - List[NamedContextProperty], - List[str]]): + def delete_properties( + self, + props: Union[Dict[str, ContextProperty], List[NamedContextProperty], List[str]], + ): """ Delete the given properties from the entity @@ -689,8 +707,9 @@ def delete_properties(self, props: Union[Dict[str, ContextProperty], for name in names: delattr(self, name) - def add_geo_properties(self, attrs: Union[Dict[str, ContextGeoProperty], - List[NamedContextGeoProperty]]) -> None: + def add_geo_properties( + self, attrs: Union[Dict[str, ContextGeoProperty], List[NamedContextGeoProperty]] + ) -> None: """ Add property to entity Args: @@ -699,14 +718,18 @@ def add_geo_properties(self, attrs: Union[Dict[str, ContextGeoProperty], None """ if isinstance(attrs, list): - attrs = {attr.name: ContextGeoProperty(**attr.model_dump(exclude={'name'}, - exclude_unset=True)) - for attr in attrs} + attrs = { + attr.name: ContextGeoProperty( + **attr.model_dump(exclude={"name"}, exclude_unset=True) + ) + for attr in attrs + } for key, attr in attrs.items(): self.__setattr__(name=key, value=attr) - def add_properties(self, attrs: Union[Dict[str, ContextProperty], - List[NamedContextProperty]]) -> None: + def add_properties( + self, attrs: Union[Dict[str, ContextProperty], List[NamedContextProperty]] + ) -> None: """ Add property to entity Args: @@ -715,14 +738,21 @@ def add_properties(self, attrs: Union[Dict[str, ContextProperty], None """ if isinstance(attrs, list): - attrs = {attr.name: ContextProperty(**attr.model_dump(exclude={'name'}, - exclude_unset=True)) - for attr in attrs} + attrs = { + attr.name: ContextProperty( + **attr.model_dump(exclude={"name"}, exclude_unset=True) + ) + for attr in attrs + } for key, attr in attrs.items(): self.__setattr__(name=key, value=attr) - def add_relationships(self, relationships: Union[Dict[str, ContextRelationship], - List[NamedContextRelationship]]) -> None: + def add_relationships( + self, + relationships: Union[ + Dict[str, ContextRelationship], List[NamedContextRelationship] + ], + ) -> None: """ Add relationship to entity Args: @@ -731,16 +761,16 @@ def add_relationships(self, relationships: Union[Dict[str, ContextRelationship], None """ if isinstance(relationships, list): - relationships = {attr.name: ContextRelationship(**attr.dict(exclude={'name'})) - for attr in relationships} + relationships = { + attr.name: ContextRelationship(**attr.dict(exclude={"name"})) + for attr in relationships + } for key, attr in relationships.items(): self.__setattr__(name=key, value=attr) - def get_relationships(self, - response_format: Union[str, PropertyFormat] = - PropertyFormat.LIST) \ - -> Union[List[NamedContextRelationship], - Dict[str, ContextRelationship]]: + def get_relationships( + self, response_format: Union[str, PropertyFormat] = PropertyFormat.LIST + ) -> Union[List[NamedContextRelationship], Dict[str, ContextRelationship]]: """ Get all relationships of the context entity @@ -757,9 +787,9 @@ def get_relationships(self, for key, value in self.model_dump(exclude_unset=True).items(): if key not in ContextLDEntity.get_model_fields_set(): try: - if value.get('type') == DataTypeLD.RELATIONSHIP: + if value.get("type") == DataTypeLD.RELATIONSHIP: final_dict[key] = ContextRelationship(**value) - except AttributeError: # if context attribute + except AttributeError: # if context attribute if isinstance(value, list): pass return final_dict @@ -767,7 +797,7 @@ def get_relationships(self, final_list = [] for key, value in self.model_dump(exclude_unset=True).items(): if key not in ContextLDEntity.get_model_fields_set(): - if value.get('type') == DataTypeLD.RELATIONSHIP: + if value.get("type") == DataTypeLD.RELATIONSHIP: final_list.append(NamedContextRelationship(name=key, **value)) return final_list @@ -802,7 +832,8 @@ class UpdateLD(BaseModel): """ Model for update action """ + entities: List[Union[ContextLDEntity, ContextLDEntityKeyValues]] = Field( description="an array of entities, each entity specified using the " - "JSON entity representation format " + "JSON entity representation format " ) diff --git a/filip/models/ngsi_ld/subscriptions.py b/filip/models/ngsi_ld/subscriptions.py index 73e6640a..f50d7928 100644 --- a/filip/models/ngsi_ld/subscriptions.py +++ b/filip/models/ngsi_ld/subscriptions.py @@ -1,6 +1,13 @@ from typing import List, Optional, Literal -from pydantic import ConfigDict, BaseModel, Field, HttpUrl, AnyUrl, \ - field_validator, model_validator +from pydantic import ( + ConfigDict, + BaseModel, + Field, + HttpUrl, + AnyUrl, + field_validator, + model_validator, +) import dateutil.parser from filip.models.ngsi_ld.base import GeoQuery, validate_ngsi_ld_query @@ -10,17 +17,16 @@ class EntityInfo(BaseModel): In v1.3.1 it is specified as EntityInfo In v1.6.1 it is specified in a new data type, namely EntitySelector """ + id: Optional[HttpUrl] = Field( - default=None, - description="Entity identifier (valid URI)" + default=None, description="Entity identifier (valid URI)" ) idPattern: Optional[str] = Field( - default=None, - description="Regular expression as per IEEE POSIX 1003.2™ [11]" + default=None, description="Regular expression as per IEEE POSIX 1003.2™ [11]" ) type: str = Field( description="Fully Qualified Name of an Entity Type or the Entity Type Name as a " - "short-hand string. See clause 4.6.2" + "short-hand string. See clause 4.6.2" ) model_config = ConfigDict(populate_by_name=True) @@ -65,24 +71,22 @@ class Endpoint(BaseModel): } ] """ - uri: AnyUrl = Field( - description="Dereferenceable URI" - ) + + uri: AnyUrl = Field(description="Dereferenceable URI") accept: Optional[str] = Field( default=None, description="MIME type for the notification payload body " - "(application/json, application/ld+json, " - "application/geo+json)" + "(application/json, application/ld+json, " + "application/geo+json)", ) receiverInfo: Optional[List[KeyValuePair]] = Field( default=None, description="Generic {key, value} array to convey optional information " - "to the receiver" + "to the receiver", ) notifierInfo: Optional[List[KeyValuePair]] = Field( default=None, - description="Generic {key, value} array to set up the communication " - "channel" + description="Generic {key, value} array to set up the communication " "channel", ) model_config = ConfigDict(populate_by_name=True) @@ -105,48 +109,46 @@ class NotificationParams(BaseModel): NGSI-LD Notification model. It contains the parameters that allow to convey the details of a notification, as described in NGSI-LD Spec section 5.2.14 """ + attributes: Optional[List[str]] = Field( default=None, description="Entity Attribute Names (Properties or Relationships) to be included " - "in the notification payload body. If undefined, it will mean all Attributes" + "in the notification payload body. If undefined, it will mean all Attributes", ) format: Optional[str] = Field( default="normalized", description="Conveys the representation format of the entities delivered at " - "notification time. By default, it will be in normalized format" - ) - endpoint: Endpoint = Field( - ..., - description="Notification endpoint details" + "notification time. By default, it will be in normalized format", ) + endpoint: Endpoint = Field(..., description="Notification endpoint details") # status can either be "ok" or "failed" status: Literal["ok", "failed"] = Field( default="ok", description="Status of the Notification. It shall be 'ok' if the last attempt " - "to notify the subscriber succeeded. It shall be 'failed' if the last" - " attempt to notify the subscriber failed" + "to notify the subscriber succeeded. It shall be 'failed' if the last" + " attempt to notify the subscriber failed", ) # Additional members timesSent: Optional[int] = Field( default=None, description="Number of times that the notification was sent. Provided by the " - "system when querying the details of a subscription" + "system when querying the details of a subscription", ) lastNotification: Optional[str] = Field( default=None, description="Timestamp corresponding to the instant when the last notification " - "was sent. Provided by the system when querying the details of a subscription" + "was sent. Provided by the system when querying the details of a subscription", ) lastFailure: Optional[str] = Field( default=None, description="Timestamp corresponding to the instant when the last notification" - " resulting in failure was sent. Provided by the system when querying the details of a subscription" + " resulting in failure was sent. Provided by the system when querying the details of a subscription", ) lastSuccess: Optional[str] = Field( default=None, description="Timestamp corresponding to the instant when the last successful " - "notification was sent. Provided by the system when querying the details of a subscription" + "notification was sent. Provided by the system when querying the details of a subscription", ) model_config = ConfigDict(populate_by_name=True) @@ -172,28 +174,29 @@ class TemporalQuery(BaseModel): "observedAt" """ + model_config = ConfigDict(populate_by_name=True) - timerel: Literal['before', 'after', 'between'] = Field( + timerel: Literal["before", "after", "between"] = Field( ..., description="String representing the temporal relationship as defined by clause " - "4.11 (Allowed values: 'before', 'after', and 'between') " + "4.11 (Allowed values: 'before', 'after', and 'between') ", ) timeAt: str = Field( ..., description="String representing the timeAt parameter as defined by clause " - "4.11. It shall be a DateTime " + "4.11. It shall be a DateTime ", ) endTimeAt: Optional[str] = Field( default=None, description="String representing the endTimeAt parameter as defined by clause " - "4.11. It shall be a DateTime. Cardinality shall be 1 if timerel is " - "equal to 'between' " + "4.11. It shall be a DateTime. Cardinality shall be 1 if timerel is " + "equal to 'between' ", ) timeproperty: Optional[str] = Field( default=None, description="String representing a Property name. The name of the Property that " - "contains the temporal data that will be used to resolve the " - "temporal query. If not specified, " + "contains the temporal data that will be used to resolve the " + "temporal query. If not specified, ", ) @field_validator("timeAt", "endTimeAt") @@ -209,8 +212,8 @@ def check_uri(cls, v: str): return v # when timerel=between, endTimeAt must be specified - @model_validator(mode='after') - def check_passwords_match(self) -> 'TemporalQuery': + @model_validator(mode="after") + def check_passwords_match(self) -> "TemporalQuery": if self.timerel == "between" and self.endTimeAt is None: raise ValueError('When timerel="between", endTimeAt must be specified') return self @@ -220,78 +223,60 @@ class SubscriptionLD(BaseModel): """ Context Subscription model according to NGSI-LD Spec section 5.2.12 """ + id: Optional[str] = Field( - default=None, - description="Subscription identifier (JSON-LD @id)" - ) - type: str = Field( - default="Subscription", - description="JSON-LD @type" + default=None, description="Subscription identifier (JSON-LD @id)" ) + type: str = Field(default="Subscription", description="JSON-LD @type") subscriptionName: Optional[str] = Field( - default=None - - , - description="A (short) name given to this Subscription" + default=None, description="A (short) name given to this Subscription" ) description: Optional[str] = Field( - default=None, - description="Subscription description" + default=None, description="Subscription description" ) entities: Optional[List[EntityInfo]] = Field( - default=None, - description="Entities subscribed" + default=None, description="Entities subscribed" ) watchedAttributes: Optional[List[str]] = Field( - default=None, - description="Watched Attributes (Properties or Relationships)" + default=None, description="Watched Attributes (Properties or Relationships)" ) notificationTrigger: Optional[List[str]] = Field( - default=None, - description="Notification triggers" + default=None, description="Notification triggers" ) timeInterval: Optional[int] = Field( - default=None, - description="Time interval in seconds" + default=None, description="Time interval in seconds" ) q: Optional[str] = Field( default=None, - description="Query met by subscribed entities to trigger the notification" + description="Query met by subscribed entities to trigger the notification", ) + @field_validator("q") @classmethod def check_q(cls, v: str): return validate_ngsi_ld_query(v) + geoQ: Optional[GeoQuery] = Field( default=None, - description="Geoquery met by subscribed entities to trigger the notification" - ) - csf: Optional[str] = Field( - default=None, - description="Context source filter" + description="Geoquery met by subscribed entities to trigger the notification", ) + csf: Optional[str] = Field(default=None, description="Context source filter") isActive: bool = Field( default=True, - description="Indicates if the Subscription is under operation (True) or paused (False)" - ) - notification: NotificationParams = Field( - ..., - description="Notification details" + description="Indicates if the Subscription is under operation (True) or paused (False)", ) + notification: NotificationParams = Field(..., description="Notification details") expiresAt: Optional[str] = Field( - default=None, - description="Expiration date for the subscription" + default=None, description="Expiration date for the subscription" ) throttling: Optional[int] = Field( default=None, - description="Minimal period of time in seconds between two consecutive notifications" + description="Minimal period of time in seconds between two consecutive notifications", ) temporalQ: Optional[TemporalQuery] = Field( - default=None, - description="Temporal Query" + default=None, description="Temporal Query" ) lang: Optional[str] = Field( - default=None, - description="Language filter applied to the query" + default=None, description="Language filter applied to the query" ) model_config = ConfigDict(populate_by_name=True) diff --git a/filip/models/ngsi_v2/__init__.py b/filip/models/ngsi_v2/__init__.py index cb14b31f..8b7cc214 100644 --- a/filip/models/ngsi_v2/__init__.py +++ b/filip/models/ngsi_v2/__init__.py @@ -1,4 +1,5 @@ """ This package contains models for FIWAREs NGSI-LD APIs """ + from .context import ContextEntity diff --git a/filip/models/ngsi_v2/base.py b/filip/models/ngsi_v2/base.py index 2b6d9fee..d54089ec 100644 --- a/filip/models/ngsi_v2/base.py +++ b/filip/models/ngsi_v2/base.py @@ -1,6 +1,7 @@ """ Shared models that are used by multiple submodules """ + import json from aenum import Enum @@ -442,9 +443,7 @@ def validate_value_type(cls, value, info: ValidationInfo): elif isinstance(value, BaseModel): value.model_dump_json() return value - raise TypeError( - f"{type(value)} does not match " f"{DataType.OBJECT}" - ) + raise TypeError(f"{type(value)} does not match " f"{DataType.OBJECT}") # allows geojson as structured value if type_ == DataType.GEOJSON: @@ -480,8 +479,7 @@ def validate_value_type(cls, value, info: ValidationInfo): return Feature(**value) elif _geo_json_type == "FeatureCollection": return FeatureCollection(**value) - raise TypeError(f"{type(value)} does not match " - f"{DataType.GEOJSON}") + raise TypeError(f"{type(value)} does not match " f"{DataType.GEOJSON}") # allows list, dict and BaseModel as structured value if type_ == DataType.STRUCTUREDVALUE: diff --git a/filip/models/ngsi_v2/context.py b/filip/models/ngsi_v2/context.py index 19567d9e..bab56a45 100644 --- a/filip/models/ngsi_v2/context.py +++ b/filip/models/ngsi_v2/context.py @@ -1,12 +1,12 @@ """ NGSIv2 models for context broker interaction """ + import json from typing import Any, List, Dict, Union, Optional, Set, Tuple from aenum import Enum -from pydantic import field_validator, ConfigDict, BaseModel, Field, \ - model_validator +from pydantic import field_validator, ConfigDict, BaseModel, Field, model_validator from pydantic_core.core_schema import ValidationInfo from filip.models.ngsi_v2.base import ( @@ -101,6 +101,7 @@ class ContextAttribute(BaseAttribute, BaseValueAttribute): >>> attr = ContextAttribute(**data) """ + # although `type` is a required field in the NGSIv2 specification, it is # set to optional here to allow for the possibility of setting # default-types in child classes. Pydantic will raise the correct error @@ -144,10 +145,10 @@ class ContextEntityKeyValues(BaseModel): ..., title="Entity Id", description="Id of an entity in an NGSI context broker. Allowed " - "characters are the ones in the plain ASCII set, except " - "the following ones: control characters, " - "whitespace, &, ?, / and #.", - json_schema_extra={"example":"Bcn-Welt"}, + "characters are the ones in the plain ASCII set, except " + "the following ones: control characters, " + "whitespace, &, ?, / and #.", + json_schema_extra={"example": "Bcn-Welt"}, max_length=256, min_length=1, frozen=True, @@ -157,10 +158,10 @@ class ContextEntityKeyValues(BaseModel): ..., title="Entity Type", description="Id of an entity in an NGSI context broker. " - "Allowed characters are the ones in the plain ASCII set, " - "except the following ones: control characters, " - "whitespace, &, ?, / and #.", - json_schema_extra={"example":"Room"}, + "Allowed characters are the ones in the plain ASCII set, " + "except the following ones: control characters, " + "whitespace, &, ?, / and #.", + json_schema_extra={"example": "Room"}, max_length=256, min_length=1, frozen=True, @@ -231,6 +232,7 @@ class ContextEntity(ContextEntityKeyValues): model_config = ConfigDict( extra="allow", validate_default=True, validate_assignment=True ) + # although `type` is a required field in the NGSIv2 specification, it is # set to optional here to allow for the possibility of setting # default-types in child classes. Pydantic will raise the correct error @@ -259,7 +261,7 @@ def _validate_attributes(cls, data: dict): return attrs - @field_validator('*') + @field_validator("*") @classmethod def check_attributes(cls, value, info: ValidationInfo): """ @@ -267,13 +269,17 @@ def check_attributes(cls, value, info: ValidationInfo): ensure full functionality. """ if info.field_name in ["id", "type"]: - return value + return value if info.field_name in cls.model_fields: - if not (isinstance(value, ContextAttribute) - or value == cls.model_fields[info.field_name].default): - raise ValueError(f"Attribute {info.field_name} must be a of " - f"type or subtype ContextAttribute") + if not ( + isinstance(value, ContextAttribute) + or value == cls.model_fields[info.field_name].default + ): + raise ValueError( + f"Attribute {info.field_name} must be a of " + f"type or subtype ContextAttribute" + ) return value @model_validator(mode="after") @@ -282,11 +288,13 @@ def check_attributes_after(cls, values): try: for attr in values.model_extra: if not isinstance(values.__getattr__(attr), ContextAttribute): - raise ValueError(f"Attribute {attr} must be a of type or " - f"subtype ContextAttribute. You most " - f"likely tried to directly assign an " - f"attribute without converting it to a " - f"proper Attribute-Type!") + raise ValueError( + f"Attribute {attr} must be a of type or " + f"subtype ContextAttribute. You most " + f"likely tried to directly assign an " + f"attribute without converting it to a " + f"proper Attribute-Type!" + ) except TypeError: pass return values @@ -632,8 +640,7 @@ class Query(BaseModel): ) expression: Optional[Expression] = Field( default=None, - description="An expression composed of q, mq, georel, geometry and " - "coords", + description="An expression composed of q, mq, georel, geometry and " "coords", ) metadata: Optional[List[str]] = Field( default=None, diff --git a/filip/models/ngsi_v2/iot.py b/filip/models/ngsi_v2/iot.py index 57a46670..fecc059a 100644 --- a/filip/models/ngsi_v2/iot.py +++ b/filip/models/ngsi_v2/iot.py @@ -1,6 +1,7 @@ """ Module contains models for accessing and interaction with FIWARE's IoT-Agents. """ + from __future__ import annotations import logging import itertools @@ -8,16 +9,26 @@ from enum import Enum from typing import Any, Dict, Optional, List, Union import pytz -from pydantic import field_validator, model_validator, ConfigDict, BaseModel, Field, AnyHttpUrl +from pydantic import ( + field_validator, + model_validator, + ConfigDict, + BaseModel, + Field, + AnyHttpUrl, +) from filip.models.base import NgsiVersion, DataType -from filip.models.ngsi_v2.base import \ - BaseAttribute, \ - BaseValueAttribute, \ - BaseNameAttribute -from filip.utils.validators import (validate_fiware_datatype_string_protect, - validate_fiware_datatype_standard, - validate_jexl_expression, - validate_expression_language) +from filip.models.ngsi_v2.base import ( + BaseAttribute, + BaseValueAttribute, + BaseNameAttribute, +) +from filip.utils.validators import ( + validate_fiware_datatype_string_protect, + validate_fiware_datatype_standard, + validate_jexl_expression, + validate_expression_language, +) logger = logging.getLogger() @@ -26,6 +37,7 @@ class ExpressionLanguage(str, Enum): """ Options for expression language """ + LEGACY = "legacy" JEXL = "jexl" @@ -34,6 +46,7 @@ class PayloadProtocol(str, Enum): """ Options for payload protocols """ + IOTA_JSON = "IoTA-JSON" IOTA_UL = "PDI-IoTA-UltraLight" LORAWAN = "LoRaWAN" @@ -43,6 +56,7 @@ class TransportProtocol(str, Enum): """ Options for transport protocols """ + MQTT = "MQTT" AMQP = "AMQP" HTTP = "HTTP" @@ -56,47 +70,51 @@ class IoTABaseAttribute(BaseAttribute, BaseNameAttribute): expression: Optional[str] = Field( default=None, description="indicates that the value of the target attribute will " - "not be the plain value or the measurement, but an " - "expression based on a combination of the reported values. " - "See the Expression Language definition for details " - "(https://iotagent-node-lib.readthedocs.io/en/latest/" - "api.html#expression-language-support)" + "not be the plain value or the measurement, but an " + "expression based on a combination of the reported values. " + "See the Expression Language definition for details " + "(https://iotagent-node-lib.readthedocs.io/en/latest/" + "api.html#expression-language-support)", ) entity_name: Optional[str] = Field( default=None, description="entity_name: the presence of this attribute indicates " - "that the value will not be stored in the original device " - "entity but in a new entity with an ID given by this " - "attribute. The type of this additional entity can be " - "configured with the entity_type attribute. If no type is " - "configured, the device entity type is used instead. " - "Entity names can be defined as expressions, using the " - "Expression Language definition " - "(https://iotagent-node-lib.readthedocs.io/en/latest/" - "api.html#expression-language-support). Allowed characters are" - " the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "that the value will not be stored in the original device " + "entity but in a new entity with an ID given by this " + "attribute. The type of this additional entity can be " + "configured with the entity_type attribute. If no type is " + "configured, the device entity type is used instead. " + "Entity names can be defined as expressions, using the " + "Expression Language definition " + "(https://iotagent-node-lib.readthedocs.io/en/latest/" + "api.html#expression-language-support). Allowed characters are" + " the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) - valid_entity_name = field_validator("entity_name")(validate_fiware_datatype_standard) + valid_entity_name = field_validator("entity_name")( + validate_fiware_datatype_standard + ) entity_type: Optional[str] = Field( default=None, description="configures the type of an alternative entity. " - "Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) - valid_entity_type = field_validator("entity_type")(validate_fiware_datatype_standard) + valid_entity_type = field_validator("entity_type")( + validate_fiware_datatype_standard + ) reverse: Optional[str] = Field( default=None, description="add bidirectionality expressions to the attribute. See " - "the bidirectionality transformation plugin in the " - "Data Mapping Plugins section for details. " - "(https://iotagent-node-lib.readthedocs.io/en/latest/api/" - "index.html#data-mapping-plugins)" + "the bidirectionality transformation plugin in the " + "Data Mapping Plugins section for details. " + "(https://iotagent-node-lib.readthedocs.io/en/latest/api/" + "index.html#data-mapping-plugins)", ) def __eq__(self, other): @@ -110,9 +128,9 @@ class DeviceAttribute(IoTABaseAttribute): """ Model for active device attributes """ + object_id: Optional[str] = Field( - default=None, - description="name of the attribute as coming from the device." + default=None, description="name of the attribute as coming from the device." ) @@ -120,14 +138,15 @@ class LazyDeviceAttribute(BaseNameAttribute): """ Model for lazy device attributes """ + type: Union[DataType, str] = Field( default=DataType.TEXT, description="The attribute type represents the NGSI value type of the " - "attribute value. Note that FIWARE NGSI has its own type " - "system for attribute values, so NGSI value types are not " - "the same as JSON types. Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "attribute value. Note that FIWARE NGSI has its own type " + "system for attribute values, so NGSI value types are not " + "the same as JSON types. Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) @@ -138,18 +157,19 @@ class DeviceCommand(BaseModel): """ Model for commands """ + name: str = Field( description="ID of the attribute in the target entity in the " - "Context Broker. Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "Context Broker. Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) valid_name = field_validator("name")(validate_fiware_datatype_string_protect) type: Union[DataType, str] = Field( description="name of the type of the attribute in the target entity. ", - default=DataType.COMMAND + default=DataType.COMMAND, ) @@ -157,6 +177,7 @@ class StaticDeviceAttribute(IoTABaseAttribute, BaseValueAttribute): """ Model for static device attributes """ + pass @@ -165,56 +186,60 @@ class ServiceGroup(BaseModel): Model for device service group. https://iotagent-node-lib.readthedocs.io/en/latest/api/index.html#service-group-api """ + service: Optional[str] = Field( - default=None, - description="ServiceGroup of the devices of this type" + default=None, description="ServiceGroup of the devices of this type" ) subservice: Optional[str] = Field( default=None, description="Subservice of the devices of this type.", - pattern="^/" + pattern="^/", ) resource: str = Field( description="string representing the Southbound resource that will be " - "used to assign a type to a device (e.g.: pathname in the " - "southbound port)." + "used to assign a type to a device (e.g.: pathname in the " + "southbound port)." ) apikey: str = Field( description="API Key string. It is a key used for devices belonging " - "to this service_group. If "", service_group does not use " - "apikey, but it must be specified." + "to this service_group. If " + ", service_group does not use " + "apikey, but it must be specified." ) timestamp: Optional[bool] = Field( default=None, description="Optional flag about whether or not to add the TimeInstant " - "attribute to the device entity created, as well as a " - "TimeInstant metadata to each attribute, with the current " - "timestamp. With NGSI-LD, the Standard observedAt " - "property-of-a-property is created instead." + "attribute to the device entity created, as well as a " + "TimeInstant metadata to each attribute, with the current " + "timestamp. With NGSI-LD, the Standard observedAt " + "property-of-a-property is created instead.", ) entity_type: Optional[str] = Field( default=None, description="name of the Entity type to assign to the group. " - "Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) - valid_entity_type = field_validator("entity_type")(validate_fiware_datatype_standard) + valid_entity_type = field_validator("entity_type")( + validate_fiware_datatype_standard + ) trust: Optional[str] = Field( default=None, description="trust token to use for secured access to the " - "Context Broker for this type of devices (optional; only " - "needed for secured scenarios)." + "Context Broker for this type of devices (optional; only " + "needed for secured scenarios).", ) cbHost: Optional[AnyHttpUrl] = Field( default=None, description="Context Broker connection information. This options can " - "be used to override the global ones for specific types of " - "devices." + "be used to override the global ones for specific types of " + "devices.", ) - @field_validator('cbHost') + + @field_validator("cbHost") @classmethod def validate_cbHost(cls, value): """ @@ -223,66 +248,70 @@ def validate_cbHost(cls, value): timezone """ return str(value) if value else value + lazy: Optional[List[LazyDeviceAttribute]] = Field( default=[], description="list of common lazy attributes of the device. For each " - "attribute, its name and type must be provided." + "attribute, its name and type must be provided.", ) commands: Optional[List[DeviceCommand]] = Field( default=[], description="list of common commands attributes of the device. For each " - "attribute, its name and type must be provided, additional " - "metadata is optional" + "attribute, its name and type must be provided, additional " + "metadata is optional", ) attributes: Optional[List[DeviceAttribute]] = Field( default=[], description="list of common commands attributes of the device. For " - "each attribute, its name and type must be provided, " - "additional metadata is optional." + "each attribute, its name and type must be provided, " + "additional metadata is optional.", ) static_attributes: Optional[List[StaticDeviceAttribute]] = Field( default=[], description="this attributes will be added to all the entities of this " - "group 'as is', additional metadata is optional." + "group 'as is', additional metadata is optional.", ) internal_attributes: Optional[List[Dict[str, Any]]] = Field( default=[], description="optional section with free format, to allow specific " - "IoT Agents to store information along with the devices " - "in the Device Registry." + "IoT Agents to store information along with the devices " + "in the Device Registry.", ) expressionLanguage: Optional[ExpressionLanguage] = Field( default=ExpressionLanguage.JEXL, description="optional boolean value, to set expression language used " - "to compute expressions, possible values are: " - "legacy or jexl, but legacy is deprecated. If it is set None, jexl is used." + "to compute expressions, possible values are: " + "legacy or jexl, but legacy is deprecated. If it is set None, jexl is used.", + ) + valid_expressionLanguage = field_validator("expressionLanguage")( + validate_expression_language ) - valid_expressionLanguage = field_validator("expressionLanguage")(validate_expression_language) explicitAttrs: Optional[bool] = Field( default=False, description="optional boolean value, to support selective ignore " - "of measures so that IOTA does not progress. If not " - "specified default is false." + "of measures so that IOTA does not progress. If not " + "specified default is false.", ) autoprovision: Optional[bool] = Field( default=True, description="optional boolean: If false, autoprovisioned devices " - "(i.e. devices that are not created with an explicit " - "provision operation but when the first measure arrives) " - "are not allowed in this group. " - "Default (in the case of omitting the field) is true." + "(i.e. devices that are not created with an explicit " + "provision operation but when the first measure arrives) " + "are not allowed in this group. " + "Default (in the case of omitting the field) is true.", ) ngsiVersion: Optional[NgsiVersion] = Field( default="v2", description="optional string value used in mixed mode to switch between" - " NGSI-v2 and NGSI-LD payloads. Possible values are: " - "v2 or ld. The default is v2. When not running in mixed " - "mode, this field is ignored.") + " NGSI-v2 and NGSI-LD payloads. Possible values are: " + "v2 or ld. The default is v2. When not running in mixed " + "mode, this field is ignored.", + ) defaultEntityNameConjunction: Optional[str] = Field( default=None, description="optional string value to set default conjunction string " - "used to compose a default entity_name when is not " - "provided at device provisioning time." + "used to compose a default entity_name when is not " + "provided at device provisioning time.", ) @@ -290,50 +319,51 @@ class DeviceSettings(BaseModel): """ Model for iot device settings """ + model_config = ConfigDict(validate_assignment=True) timezone: Optional[str] = Field( - default='Europe/London', - description="Time zone of the sensor if it has any" + default="Europe/London", description="Time zone of the sensor if it has any" ) timestamp: Optional[bool] = Field( default=None, description="Optional flag about whether or not to add the TimeInstant " - "attribute to the device entity created, as well as a " - "TimeInstant metadata to each attribute, with the current " - "timestamp. With NGSI-LD, the Standard observedAt " - "property-of-a-property is created instead." + "attribute to the device entity created, as well as a " + "TimeInstant metadata to each attribute, with the current " + "timestamp. With NGSI-LD, the Standard observedAt " + "property-of-a-property is created instead.", ) apikey: Optional[str] = Field( default=None, - description="Optional Apikey key string to use instead of group apikey" + description="Optional Apikey key string to use instead of group apikey", ) endpoint: Optional[AnyHttpUrl] = Field( default=None, description="Endpoint where the device is going to receive commands, " - "if any." + "if any.", ) protocol: Optional[Union[PayloadProtocol, str]] = Field( default=None, - description="Name of the device protocol, for its use with an " - "IoT Manager." + description="Name of the device protocol, for its use with an " "IoT Manager.", ) transport: Optional[Union[TransportProtocol, str]] = Field( default=None, description="Name of the device transport protocol, for the IoT Agents " - "with multiple transport protocols." + "with multiple transport protocols.", ) expressionLanguage: Optional[ExpressionLanguage] = Field( default=ExpressionLanguage.JEXL, description="optional boolean value, to set expression language used " - "to compute expressions, possible values are: " - "legacy or jexl, but legacy is deprecated. If it is set None, jexl is used." + "to compute expressions, possible values are: " + "legacy or jexl, but legacy is deprecated. If it is set None, jexl is used.", + ) + valid_expressionLanguage = field_validator("expressionLanguage")( + validate_expression_language ) - valid_expressionLanguage = field_validator("expressionLanguage")(validate_expression_language) explicitAttrs: Optional[bool] = Field( default=False, description="optional boolean value, to support selective ignore " - "of measures so that IOTA does not progress. If not " - "specified default is false." + "of measures so that IOTA does not progress. If not " + "specified default is false.", ) @@ -342,6 +372,7 @@ class Device(DeviceSettings): Model for iot devices. https://iotagent-node-lib.readthedocs.io/en/latest/api/index.html#device-api """ + model_config = ConfigDict(validate_default=True, validate_assignment=True) device_id: str = Field( description="Device ID that will be used to identify the device" @@ -349,65 +380,67 @@ class Device(DeviceSettings): service: Optional[str] = Field( default=None, description="Name of the service the device belongs to " - "(will be used in the fiware-service header).", - max_length=50 + "(will be used in the fiware-service header).", + max_length=50, ) service_path: Optional[str] = Field( default="/", description="Name of the subservice the device belongs to " - "(used in the fiware-servicepath header).", + "(used in the fiware-servicepath header).", max_length=51, - pattern="^/" + pattern="^/", ) entity_name: str = Field( description="Name of the entity representing the device in " - "the Context Broker Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "the Context Broker Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) - valid_entity_name = field_validator("entity_name")(validate_fiware_datatype_standard) + valid_entity_name = field_validator("entity_name")( + validate_fiware_datatype_standard + ) entity_type: str = Field( description="Type of the entity in the Context Broker. " - "Allowed characters " - "are the ones in the plain ASCII set, except the following " - "ones: control characters, whitespace, &, ?, / and #.", + "Allowed characters " + "are the ones in the plain ASCII set, except the following " + "ones: control characters, whitespace, &, ?, / and #.", max_length=256, min_length=1, ) - valid_entity_type = field_validator("entity_type")(validate_fiware_datatype_standard) + valid_entity_type = field_validator("entity_type")( + validate_fiware_datatype_standard + ) lazy: List[LazyDeviceAttribute] = Field( - default=[], - description="List of lazy attributes of the device" + default=[], description="List of lazy attributes of the device" ) commands: List[DeviceCommand] = Field( - default=[], - description="List of commands of the device" + default=[], description="List of commands of the device" ) attributes: List[DeviceAttribute] = Field( - default=[], - description="List of active attributes of the device" + default=[], description="List of active attributes of the device" ) static_attributes: Optional[List[StaticDeviceAttribute]] = Field( default=[], description="List of static attributes to append to the entity. All the" - " updateContext requests to the CB will have this set of " - "attributes appended." + " updateContext requests to the CB will have this set of " + "attributes appended.", ) internal_attributes: Optional[List[Dict[str, Any]]] = Field( default=[], description="List of internal attributes with free format for specific " - "IoT Agent configuration" + "IoT Agent configuration", ) ngsiVersion: NgsiVersion = Field( default=NgsiVersion.v2, description="optional string value used in mixed mode to switch between" - " NGSI-v2 and NGSI-LD payloads. Possible values are: " - "v2 or ld. The default is v2. When not running in " - "mixed mode, this field is ignored.") + " NGSI-v2 and NGSI-LD payloads. Possible values are: " + "v2 or ld. The default is v2. When not running in " + "mixed mode, this field is ignored.", + ) - @field_validator('timezone') + @field_validator("timezone") @classmethod def validate_timezone(cls, value): """ @@ -418,7 +451,7 @@ def validate_timezone(cls, value): assert value in pytz.all_timezones return value - @model_validator(mode='after') + @model_validator(mode="after") def validate_device_attributes_expression(self): """ Validates device attributes expressions based on the expression language (JEXL or Legacy, where Legacy is @@ -433,13 +466,17 @@ def validate_device_attributes_expression(self): if self.expressionLanguage == ExpressionLanguage.JEXL: for attribute in self.attributes: if attribute.expression: - validate_jexl_expression(attribute.expression, attribute.name, self.device_id) + validate_jexl_expression( + attribute.expression, attribute.name, self.device_id + ) elif self.expressionLanguage == ExpressionLanguage.LEGACY: - warnings.warn(f"No validation for legacy expression language of Device {self.device_id}.") + warnings.warn( + f"No validation for legacy expression language of Device {self.device_id}." + ) return self - @model_validator(mode='after') + @model_validator(mode="after") def validate_duplicated_device_attributes(self): """ Check whether device has identical attributes @@ -450,12 +487,12 @@ def validate_duplicated_device_attributes(self): The dict of Device instance after validation. """ for i, attr in enumerate(self.attributes): - for other_attr in self.attributes[:i] + self.attributes[i + 1:]: + for other_attr in self.attributes[:i] + self.attributes[i + 1 :]: if attr.model_dump() == other_attr.model_dump(): raise ValueError(f"Duplicated attributes found: {attr.name}") return self - @model_validator(mode='after') + @model_validator(mode="after") def validate_device_attributes_name_object_id(self): """ Validate the device regarding the behavior with devices attributes. @@ -471,18 +508,22 @@ def validate_device_attributes_name_object_id(self): The dict of Device instance after validation. """ for i, attr in enumerate(self.attributes): - for other_attr in self.attributes[:i] + self.attributes[i + 1:]: - if attr.object_id and other_attr.object_id and \ - attr.object_id == other_attr.object_id: + for other_attr in self.attributes[:i] + self.attributes[i + 1 :]: + if ( + attr.object_id + and other_attr.object_id + and attr.object_id == other_attr.object_id + ): raise ValueError(f"object_id {attr.object_id} is not unique") if attr.object_id and attr.object_id == other_attr.name: raise ValueError(f"object_id {attr.object_id} is not unique") return self - def get_attribute(self, attribute_name: str) -> Union[DeviceAttribute, - LazyDeviceAttribute, - StaticDeviceAttribute, - DeviceCommand]: + def get_attribute( + self, attribute_name: str + ) -> Union[ + DeviceAttribute, LazyDeviceAttribute, StaticDeviceAttribute, DeviceCommand + ]: """ Args: @@ -491,24 +532,29 @@ def get_attribute(self, attribute_name: str) -> Union[DeviceAttribute, Returns: """ - for attribute in itertools.chain(self.attributes, - self.lazy, - self.static_attributes, - self.internal_attributes, - self.commands): + for attribute in itertools.chain( + self.attributes, + self.lazy, + self.static_attributes, + self.internal_attributes, + self.commands, + ): if attribute.name == attribute_name: return attribute - msg = f"Device: {self.device_id}: Could not " \ - f"find attribute with name {attribute_name}" + msg = ( + f"Device: {self.device_id}: Could not " + f"find attribute with name {attribute_name}" + ) logger.error(msg) raise KeyError(msg) - def add_attribute(self, - attribute: Union[DeviceAttribute, - LazyDeviceAttribute, - StaticDeviceAttribute, - DeviceCommand], - update: bool = False) -> None: + def add_attribute( + self, + attribute: Union[ + DeviceAttribute, LazyDeviceAttribute, StaticDeviceAttribute, DeviceCommand + ], + update: bool = False, + ) -> None: """ Args: @@ -520,54 +566,56 @@ def add_attribute(self, """ try: if type(attribute) == DeviceAttribute: - if attribute.model_dump(exclude_none=True) in \ - [attr.model_dump(exclude_none=True) for attr in self.attributes]: + if attribute.model_dump(exclude_none=True) in [ + attr.model_dump(exclude_none=True) for attr in self.attributes + ]: raise ValueError self.attributes.append(attribute) - self.__setattr__(name='attributes', - value=self.attributes) + self.__setattr__(name="attributes", value=self.attributes) elif type(attribute) == LazyDeviceAttribute: if attribute in self.lazy: raise ValueError self.lazy.append(attribute) - self.__setattr__(name='lazy', - value=self.lazy) + self.__setattr__(name="lazy", value=self.lazy) elif type(attribute) == StaticDeviceAttribute: if attribute in self.static_attributes: raise ValueError self.static_attributes.append(attribute) - self.__setattr__(name='static_attributes', - value=self.static_attributes) + self.__setattr__(name="static_attributes", value=self.static_attributes) elif type(attribute) == DeviceCommand: if attribute in self.commands: raise ValueError self.commands.append(attribute) - self.__setattr__(name='commands', - value=self.commands) + self.__setattr__(name="commands", value=self.commands) else: raise ValueError except ValueError: if update: self.update_attribute(attribute, append=False) - logger.warning("Device: %s: Attribute already " - "exists. Will update: \n %s", - self.device_id, attribute.model_dump_json(indent=2)) + logger.warning( + "Device: %s: Attribute already " "exists. Will update: \n %s", + self.device_id, + attribute.model_dump_json(indent=2), + ) else: - logger.error("Device: %s: Attribute already " - "exists: \n %s", self.device_id, - attribute.model_dump_json(indent=2)) + logger.error( + "Device: %s: Attribute already " "exists: \n %s", + self.device_id, + attribute.model_dump_json(indent=2), + ) raise - def update_attribute(self, - attribute: Union[DeviceAttribute, - LazyDeviceAttribute, - StaticDeviceAttribute, - DeviceCommand], - append: bool = False) -> None: + def update_attribute( + self, + attribute: Union[ + DeviceAttribute, LazyDeviceAttribute, StaticDeviceAttribute, DeviceCommand + ], + append: bool = False, + ) -> None: """ Updates existing device attribute @@ -593,19 +641,25 @@ def update_attribute(self, self.commands[idx].model_dump().update(attribute.model_dump()) except ValueError: if append: - logger.warning("Device: %s: Could not find " - "attribute: \n %s", - self.device_id, attribute.model_dump_json(indent=2)) + logger.warning( + "Device: %s: Could not find " "attribute: \n %s", + self.device_id, + attribute.model_dump_json(indent=2), + ) self.add_attribute(attribute=attribute) else: - msg = f"Device: {self.device_id}: Could not find "\ - f"attribute: \n {attribute.model_dump_json(indent=2)}" + msg = ( + f"Device: {self.device_id}: Could not find " + f"attribute: \n {attribute.model_dump_json(indent=2)}" + ) raise KeyError(msg) - def delete_attribute(self, attribute: Union[DeviceAttribute, - LazyDeviceAttribute, - StaticDeviceAttribute, - DeviceCommand]): + def delete_attribute( + self, + attribute: Union[ + DeviceAttribute, LazyDeviceAttribute, StaticDeviceAttribute, DeviceCommand + ], + ): """ Deletes attribute from device Args: @@ -626,13 +680,18 @@ def delete_attribute(self, attribute: Union[DeviceAttribute, else: raise ValueError except ValueError: - logger.warning("Device: %s: Could not delete " - "attribute: \n %s", - self.device_id, attribute.model_dump_json(indent=2)) + logger.warning( + "Device: %s: Could not delete " "attribute: \n %s", + self.device_id, + attribute.model_dump_json(indent=2), + ) raise - logger.info("Device: %s: Attribute deleted! \n %s", - self.device_id, attribute.model_dump_json(indent=2)) + logger.info( + "Device: %s: Attribute deleted! \n %s", + self.device_id, + attribute.model_dump_json(indent=2), + ) def get_command(self, command_name: str): """ diff --git a/filip/models/ngsi_v2/registrations.py b/filip/models/ngsi_v2/registrations.py index 148bb74c..6dd03992 100644 --- a/filip/models/ngsi_v2/registrations.py +++ b/filip/models/ngsi_v2/registrations.py @@ -2,6 +2,7 @@ This module contains NGSIv2 models for context registrations in the context broker. """ + from typing import List, Union, Optional from datetime import datetime from aenum import Enum @@ -10,31 +11,35 @@ class ForwardingMode(str, Enum): - _init_ = 'value __doc__' + _init_ = "value __doc__" NONE = "none", "This provider does not support request forwarding." - QUERY = "query", "This provider only supports request forwarding to " \ - "query data." - UPDATE = "update", "This provider only supports request forwarding to " \ - "update data." - ALL = "all", "This provider supports both query and update forwarding " \ - "requests. (Default value)" + QUERY = "query", "This provider only supports request forwarding to " "query data." + UPDATE = ( + "update", + "This provider only supports request forwarding to " "update data.", + ) + ALL = ( + "all", + "This provider supports both query and update forwarding " + "requests. (Default value)", + ) class Provider(BaseModel): http: Http = Field( description="It is used to convey parameters for providers that " - "deliver information through the HTTP protocol. (Only " - "protocol supported nowadays). It must contain a subfield " - "named url with the URL that serves as the endpoint that " - "offers the providing interface. The endpoint must not " - "include the protocol specific part (for instance " - "/v2/entities). " + "deliver information through the HTTP protocol. (Only " + "protocol supported nowadays). It must contain a subfield " + "named url with the URL that serves as the endpoint that " + "offers the providing interface. The endpoint must not " + "include the protocol specific part (for instance " + "/v2/entities). " ) supportedForwardingMode: ForwardingMode = Field( default=ForwardingMode.ALL, description="It is used to convey the forwarding mode supported by " - "this context provider. By default all." + "this context provider. By default all.", ) @@ -43,25 +48,25 @@ class ForwardingInformation(BaseModel): timesSent: int = Field( description="(not editable, only present in GET operations): " - "Number of forwarding requests sent due to this " - "registration." + "Number of forwarding requests sent due to this " + "registration." ) lastForwarding: datetime = Field( description="(not editable, only present in GET operations): " - "Last forwarding timestamp in ISO8601 format." + "Last forwarding timestamp in ISO8601 format." ) lastFailure: Optional[datetime] = Field( default=None, description="(not editable, only present in GET operations): " - "Last failure timestamp in ISO8601 format. Not present " - "if registration has never had a problem with forwarding." + "Last failure timestamp in ISO8601 format. Not present " + "if registration has never had a problem with forwarding.", ) lastSuccess: Optional[datetime] = Field( default=None, description="(not editable, only present in GET operations): " - "Timestamp in ISO8601 format for last successful " - "request forwarding. Not present if registration has " - "never had a successful notification." + "Timestamp in ISO8601 format for last successful " + "request forwarding. Not present if registration has " + "never had a successful notification.", ) @@ -69,19 +74,20 @@ class DataProvided(BaseModel): """ Model for provided data """ + entities: List[EntityPattern] = Field( description="A list of objects, each one composed by an entity object" ) attrs: Optional[List[str]] = Field( default=None, description="List of attributes to be provided " - "(if not specified, all attributes)" + "(if not specified, all attributes)", ) expression: Optional[Union[str, Expression]] = Field( default=None, description="By means of a filtering expression, allows to express " - "what is the scope of the data provided. Currently only " - "geographical scopes are supported " + "what is the scope of the data provided. Currently only " + "geographical scopes are supported ", ) @@ -92,51 +98,52 @@ class Registration(BaseModel): (entities, attributes) of the context information space, including those located at specific geographical areas. """ + id: Optional[str] = Field( default=None, description="Unique identifier assigned to the registration. " - "Automatically generated at creation time." + "Automatically generated at creation time.", ) description: Optional[str] = Field( default=None, - description="A free text used by the client to describe the " - "registration.", - json_schema_extra={"example":"Relative Humidity Context Source"} + description="A free text used by the client to describe the " "registration.", + json_schema_extra={"example": "Relative Humidity Context Source"}, ) provider: Provider = Field( description="Object that describes the context source registered.", - json_schema_extra={"example": '"http": {"url": "http://localhost:1234"}'} + json_schema_extra={"example": '"http": {"url": "http://localhost:1234"}'}, ) dataProvided: DataProvided = Field( description="Object that describes the data provided by this source", - json_schema_extra={"example": '{' - ' "entities": [{"id": "room2", "type": "Room"}],' - ' "attrs": ["relativeHumidity"]' - '},' - } + json_schema_extra={ + "example": "{" + ' "entities": [{"id": "room2", "type": "Room"}],' + ' "attrs": ["relativeHumidity"]' + "}," + }, ) status: Optional[Status] = Field( default=Status.ACTIVE, description="Either active (for active registration) or inactive " - "(for inactive registration). If this field is not " - "provided at registration creation time, new registration " - "are created with the active status, which can be changed" - " by clients afterwards. For expired registration, this " - "attribute is set to expired (no matter if the client " - "updates it to active/inactive). Also, for subscriptions " - "experiencing problems with notifications, the status is " - "set to failed. As soon as the notifications start working " - "again, the status is changed back to active." + "(for inactive registration). If this field is not " + "provided at registration creation time, new registration " + "are created with the active status, which can be changed" + " by clients afterwards. For expired registration, this " + "attribute is set to expired (no matter if the client " + "updates it to active/inactive). Also, for subscriptions " + "experiencing problems with notifications, the status is " + "set to failed. As soon as the notifications start working " + "again, the status is changed back to active.", ) expires: Optional[datetime] = Field( default=None, description="Registration expiration date in ISO8601 format. " - "Permanent registrations must omit this field." + "Permanent registrations must omit this field.", ) forwardingInformation: Optional[ForwardingInformation] = Field( default=None, description="Information related to the forwarding operations made " - "against the provider. Automatically provided by the " - "implementation, in the case such implementation supports " - "forwarding capabilities." + "against the provider. Automatically provided by the " + "implementation, in the case such implementation supports " + "forwarding capabilities.", ) diff --git a/filip/models/ngsi_v2/subscriptions.py b/filip/models/ngsi_v2/subscriptions.py index 59925be3..e3804925 100644 --- a/filip/models/ngsi_v2/subscriptions.py +++ b/filip/models/ngsi_v2/subscriptions.py @@ -2,33 +2,37 @@ This module contains NGSIv2 models for context subscription in the context broker. """ + from typing import Any, List, Dict, Union, Optional from datetime import datetime from aenum import Enum -from pydantic import \ - field_validator, model_validator, ConfigDict, BaseModel, \ - conint, \ - Field, \ - Json -from .base import AttrsFormat, EntityPattern, Http, Status, Expression -from filip.utils.validators import ( - validate_mqtt_url, - validate_mqtt_topic +from pydantic import ( + field_validator, + model_validator, + ConfigDict, + BaseModel, + conint, + Field, + Json, ) +from .base import AttrsFormat, EntityPattern, Http, Status, Expression +from filip.utils.validators import validate_mqtt_url, validate_mqtt_topic from filip.models.ngsi_v2.context import ContextEntity -from filip.models.ngsi_v2.base import ( - EntityPattern, - Expression, - DataType -) +from filip.models.ngsi_v2.base import EntityPattern, Expression, DataType from filip.custom_types import AnyMqttUrl import warnings # The pydantic models still have a .json() function, but this method is deprecated. -warnings.filterwarnings("ignore", category=UserWarning, - message='Field name "json" shadows an attribute in parent "Http"') -warnings.filterwarnings("ignore", category=UserWarning, - message='Field name "json" shadows an attribute in parent "Mqtt"') +warnings.filterwarnings( + "ignore", + category=UserWarning, + message='Field name "json" shadows an attribute in parent "Http"', +) +warnings.filterwarnings( + "ignore", + category=UserWarning, + message='Field name "json" shadows an attribute in parent "Mqtt"', +) class NgsiPayloadAttr(BaseModel): @@ -40,6 +44,7 @@ class NgsiPayloadAttr(BaseModel): representations ( as specified in the orion api manual), done by the BaseValueAttr. model. """ + model_config = ConfigDict(extra="forbid") type: Union[DataType, str] = Field( default=DataType.TEXT, @@ -64,15 +69,9 @@ class NgsiPayload(BaseModel): - id and type are not mandatory - an attribute metadata field is not allowed """ - model_config = ConfigDict( - extra="allow", validate_default=True - ) - id: Optional[str] = Field( - default=None, - max_length=256, - min_length=1, - frozen=True - ) + + model_config = ConfigDict(extra="allow", validate_default=True) + id: Optional[str] = Field(default=None, max_length=256, min_length=1, frozen=True) type: Optional[Union[str, Enum]] = Field( default=None, max_length=256, @@ -80,7 +79,7 @@ class NgsiPayload(BaseModel): frozen=True, ) - @model_validator(mode='after') + @model_validator(mode="after") def validate_notification_attrs(self): for v in self.model_dump(exclude={"id", "type"}).values(): assert isinstance(NgsiPayloadAttr.model_validate(v), NgsiPayloadAttr) @@ -91,22 +90,23 @@ class Message(BaseModel): """ Model for a notification message, when sent to other NGSIv2-APIs """ + subscriptionId: Optional[str] = Field( default=None, description="Id of the subscription the notification comes from", ) data: List[ContextEntity] = Field( description="is an array with the notification data itself which " - "includes the entity and all concerned attributes. Each " - "element in the array corresponds to a different entity. " - "By default, the entities are represented in normalized " - "mode. However, using the attrsFormat modifier, a " - "simplified representation mode can be requested." + "includes the entity and all concerned attributes. Each " + "element in the array corresponds to a different entity. " + "By default, the entities are represented in normalized " + "mode. However, using the attrsFormat modifier, a " + "simplified representation mode can be requested." ) class HttpMethods(str, Enum): - _init_ = 'value __doc__' + _init_ = "value __doc__" POST = "POST", "Post Method" PUT = "PUT", "Put Method" @@ -117,56 +117,59 @@ class HttpCustom(Http): """ Model for custom notification patterns sent via HTTP """ + headers: Optional[Dict[str, Union[str, Json]]] = Field( default=None, description="a key-map of HTTP headers that are included in " - "notification messages." + "notification messages.", ) qs: Optional[Dict[str, Union[str, Json]]] = Field( default=None, description="a key-map of URL query parameters that are included in " - "notification messages." + "notification messages.", ) method: str = Field( default=HttpMethods.POST, description="the method to use when sending the notification " - "(default is POST). Only valid HTTP methods are allowed. " - "On specifying an invalid HTTP method, a 400 Bad Request " - "error is returned." + "(default is POST). Only valid HTTP methods are allowed. " + "On specifying an invalid HTTP method, a 400 Bad Request " + "error is returned.", ) payload: Optional[str] = Field( default=None, - description='the payload to be used in notifications. If omitted, the ' - 'default payload (see "Notification Messages" sections) ' - 'is used.' + description="the payload to be used in notifications. If omitted, the " + 'default payload (see "Notification Messages" sections) ' + "is used.", ) json: Optional[Dict[str, Union[str, Json]]] = Field( default=None, - description='get a json as notification. If omitted, the default' - 'payload (see "Notification Messages" sections) is used.' + description="get a json as notification. If omitted, the default" + 'payload (see "Notification Messages" sections) is used.', ) ngsi: Optional[NgsiPayload] = Field( default=None, - description='get an NGSI-v2 normalized entity as notification.If omitted, ' - 'the default payload (see "Notification Messages" sections) is used.' + description="get an NGSI-v2 normalized entity as notification.If omitted, " + 'the default payload (see "Notification Messages" sections) is used.', ) timeout: Optional[int] = Field( default=None, description="Maximum time (in milliseconds) the subscription waits for the " - "response. The maximum value allowed for this parameter is 1800000 " - "(30 minutes). If timeout is defined to 0 or omitted, then the value " - "passed as -httpTimeout CLI parameter is used. See section in the " - "'Command line options' for more details." + "response. The maximum value allowed for this parameter is 1800000 " + "(30 minutes). If timeout is defined to 0 or omitted, then the value " + "passed as -httpTimeout CLI parameter is used. See section in the " + "'Command line options' for more details.", ) - @model_validator(mode='after') + @model_validator(mode="after") def validate_notification_payloads(self): fields = [self.payload, self.json, self.ngsi] filled_fields = [field for field in fields if field is not None] if len(filled_fields) > 1: - raise ValueError("Only one of payload, json or ngsi fields accepted at the " - "same time in httpCustom.") + raise ValueError( + "Only one of payload, json or ngsi fields accepted at the " + "same time in httpCustom." + ) return self @@ -176,31 +179,28 @@ class Mqtt(BaseModel): Model for notifications sent via MQTT https://fiware-orion.readthedocs.io/en/3.8.0/user/mqtt_notifications/index.html """ + url: Union[AnyMqttUrl, str] = Field( - description='to specify the MQTT broker endpoint to use. URL must ' - 'start with mqtt:// and never contains a path (i.e. it ' - 'only includes host and port)') + description="to specify the MQTT broker endpoint to use. URL must " + "start with mqtt:// and never contains a path (i.e. it " + "only includes host and port)" + ) topic: str = Field( - description='to specify the MQTT topic to use', + description="to specify the MQTT topic to use", ) valid_type = field_validator("topic")(validate_mqtt_topic) qos: Optional[int] = Field( default=0, - description='to specify the MQTT QoS value to use in the ' - 'notifications associated to the subscription (0, 1 or 2). ' - 'This is an optional field, if omitted then QoS 0 is used.', + description="to specify the MQTT QoS value to use in the " + "notifications associated to the subscription (0, 1 or 2). " + "This is an optional field, if omitted then QoS 0 is used.", ge=0, - le=2) - user: Optional[str] = Field( - default=None, - description="username if required" - ) - passwd: Optional[str] = Field( - default=None, - description="password if required" + le=2, ) + user: Optional[str] = Field(default=None, description="username if required") + passwd: Optional[str] = Field(default=None, description="password if required") - @field_validator('url') + @field_validator("url") @classmethod def check_url(cls, value): """ @@ -218,27 +218,36 @@ class MqttCustom(Mqtt): Model for custom notification patterns sent via MQTT https://fiware-orion.readthedocs.io/en/3.8.0/user/mqtt_notifications/index.html """ + payload: Optional[str] = Field( default=None, - description='the payload to be used in notifications. If omitted, the ' - 'default payload (see "Notification Messages" sections) ' - 'is used.' + description="the payload to be used in notifications. If omitted, the " + 'default payload (see "Notification Messages" sections) ' + "is used.", ) json: Optional[Dict[str, Any]] = Field( default=None, - description='get a json as notification. If omitted, the default' - 'payload (see "Notification Messages" sections) is used.' + description="get a json as notification. If omitted, the default" + 'payload (see "Notification Messages" sections) is used.', ) ngsi: Optional[NgsiPayload] = Field( default=None, - description='get an NGSI-v2 normalized entity as notification.If omitted, ' - 'the default payload (see "Notification Messages" sections) is used.' + description="get an NGSI-v2 normalized entity as notification.If omitted, " + 'the default payload (see "Notification Messages" sections) is used.', ) - @model_validator(mode='after') + @model_validator(mode="after") def validate_payload_type(self): - assert len([v for k, v in self.model_dump().items() - if ((v is not None) and (k in ['payload', 'ngsi', 'json']))]) <= 1 + assert ( + len( + [ + v + for k, v in self.model_dump().items() + if ((v is not None) and (k in ["payload", "ngsi", "json"])) + ] + ) + <= 1 + ) return self @@ -248,123 +257,144 @@ class Notification(BaseModel): included in the notifications. Otherwise, only the specified ones will be included. """ + model_config = ConfigDict(validate_assignment=True) timesSent: Optional[Any] = Field( default=None, description="Not editable, only present in GET operations. " - "Number of notifications sent due to this subscription." + "Number of notifications sent due to this subscription.", ) http: Optional[Http] = Field( default=None, - description='It is used to convey parameters for notifications ' - 'delivered through the HTTP protocol. Cannot be used ' - 'together with "httpCustom, mqtt, mqttCustom"' + description="It is used to convey parameters for notifications " + "delivered through the HTTP protocol. Cannot be used " + 'together with "httpCustom, mqtt, mqttCustom"', ) httpCustom: Optional[HttpCustom] = Field( default=None, - description='It is used to convey parameters for notifications ' - 'delivered through the HTTP protocol. Cannot be used ' - 'together with "http"' + description="It is used to convey parameters for notifications " + "delivered through the HTTP protocol. Cannot be used " + 'together with "http"', ) mqtt: Optional[Mqtt] = Field( default=None, - description='It is used to convey parameters for notifications ' - 'delivered through the MQTT protocol. Cannot be used ' - 'together with "http, httpCustom, mqttCustom"' + description="It is used to convey parameters for notifications " + "delivered through the MQTT protocol. Cannot be used " + 'together with "http, httpCustom, mqttCustom"', ) mqttCustom: Optional[MqttCustom] = Field( default=None, - description='It is used to convey parameters for notifications ' - 'delivered through the MQTT protocol. Cannot be used ' - 'together with "http, httpCustom, mqtt"' + description="It is used to convey parameters for notifications " + "delivered through the MQTT protocol. Cannot be used " + 'together with "http, httpCustom, mqtt"', ) attrs: Optional[List[str]] = Field( default=None, - description='List of attributes to be included in notification ' - 'messages. It also defines the order in which attributes ' - 'must appear in notifications when attrsFormat value is ' - 'used (see "Notification Messages" section). An empty list ' - 'means that all attributes are to be included in ' - 'notifications. See "Filtering out attributes and ' - 'metadata" section for more detail.' + description="List of attributes to be included in notification " + "messages. It also defines the order in which attributes " + "must appear in notifications when attrsFormat value is " + 'used (see "Notification Messages" section). An empty list ' + "means that all attributes are to be included in " + 'notifications. See "Filtering out attributes and ' + 'metadata" section for more detail.', ) exceptAttrs: Optional[List[str]] = Field( default=None, - description='List of attributes to be excluded from the notification ' - 'message, i.e. a notification message includes all entity ' - 'attributes except the ones listed in this field.' + description="List of attributes to be excluded from the notification " + "message, i.e. a notification message includes all entity " + "attributes except the ones listed in this field.", ) attrsFormat: Optional[AttrsFormat] = Field( default=AttrsFormat.NORMALIZED, - description='specifies how the entities are represented in ' - 'notifications. Accepted values are normalized (default), ' - 'keyValues or values. If attrsFormat takes any value ' - 'different than those, an error is raised. See detail in ' - '"Notification Messages" section.' + description="specifies how the entities are represented in " + "notifications. Accepted values are normalized (default), " + "keyValues or values. If attrsFormat takes any value " + "different than those, an error is raised. See detail in " + '"Notification Messages" section.', ) metadata: Optional[Any] = Field( default=None, - description='List of metadata to be included in notification messages. ' - 'See "Filtering out attributes and metadata" section for ' - 'more detail.' + description="List of metadata to be included in notification messages. " + 'See "Filtering out attributes and metadata" section for ' + "more detail.", ) onlyChangedAttrs: Optional[bool] = Field( default=False, - description='Only supported by Orion Context Broker!' - 'If set to true then notifications associated to the ' - 'subscription include only attributes that changed in the ' - 'triggering update request, in combination with the attrs ' - 'or exceptAttrs field. For instance, if attrs is ' - '[A=1, B=2, C=3] and A=0 is updated. In case ' - 'onlyChangedAttrs=false, CB notifies [A=0, B=2, C=3].' - 'In case onlyChangedAttrs=true, CB notifies ' - '[A=0, B=null, C=null]. This ' + description="Only supported by Orion Context Broker!" + "If set to true then notifications associated to the " + "subscription include only attributes that changed in the " + "triggering update request, in combination with the attrs " + "or exceptAttrs field. For instance, if attrs is " + "[A=1, B=2, C=3] and A=0 is updated. In case " + "onlyChangedAttrs=false, CB notifies [A=0, B=2, C=3]." + "In case onlyChangedAttrs=true, CB notifies " + "[A=0, B=null, C=null]. This ", ) covered: Optional[bool] = Field( default=False, description="A flag to decide whether to include not existing attribute in " - "notifications. It can be useful for those notification endpoints " - "that are not flexible enough for a variable set of attributes and " - "needs always the same set of incoming attributes in every received" - " notification " - "https://fiware-orion.readthedocs.io/en/master/orion-api.html#covered-subscriptions" + "notifications. It can be useful for those notification endpoints " + "that are not flexible enough for a variable set of attributes and " + "needs always the same set of incoming attributes in every received" + " notification " + "https://fiware-orion.readthedocs.io/en/master/orion-api.html#covered-subscriptions", ) - @model_validator(mode='after') + @model_validator(mode="after") def validate_http(self): if self.httpCustom is not None: assert self.http is None return self - @model_validator(mode='after') + @model_validator(mode="after") def validate_attr(self): if self.exceptAttrs is not None: assert self.attrs is None return self - @model_validator(mode='after') + @model_validator(mode="after") def validate_endpoints(self): if self.http is not None: - assert all((v is None for k, v in self.model_dump().items() if k in [ - 'httpCustom', 'mqtt', 'mqttCustom'])) + assert all( + ( + v is None + for k, v in self.model_dump().items() + if k in ["httpCustom", "mqtt", "mqttCustom"] + ) + ) elif self.httpCustom is not None: - assert all((v is None for k, v in self.model_dump().items() if k in [ - 'http', 'mqtt', 'mqttCustom'])) + assert all( + ( + v is None + for k, v in self.model_dump().items() + if k in ["http", "mqtt", "mqttCustom"] + ) + ) elif self.mqtt is not None: - assert all((v is None for k, v in self.model_dump().items() if k in [ - 'http', 'httpCustom', 'mqttCustom'])) + assert all( + ( + v is None + for k, v in self.model_dump().items() + if k in ["http", "httpCustom", "mqttCustom"] + ) + ) else: - assert all((v is None for k, v in self.model_dump().items() if k in [ - 'http', 'httpCustom', 'mqtt'])) + assert all( + ( + v is None + for k, v in self.model_dump().items() + if k in ["http", "httpCustom", "mqtt"] + ) + ) return self - @model_validator(mode='after') + @model_validator(mode="after") def validate_covered_attrs(self): if self.covered is True: if isinstance(self.attrs, list) and len(self.attrs) > 0: return self else: - raise ValueError('Covered notification need an explicit list of attrs.') + raise ValueError("Covered notification need an explicit list of attrs.") return self @@ -372,26 +402,27 @@ class Response(Notification): """ Server response model for notifications """ + timesSent: int = Field( - description='(not editable, only present in GET operations): ' - 'Number of notifications sent due to this subscription.' + description="(not editable, only present in GET operations): " + "Number of notifications sent due to this subscription." ) lastNotification: datetime = Field( - description='(not editable, only present in GET operations): ' - 'Last notification timestamp in ISO8601 format.' + description="(not editable, only present in GET operations): " + "Last notification timestamp in ISO8601 format." ) lastFailure: Optional[datetime] = Field( default=None, - description='(not editable, only present in GET operations): ' - 'Last failure timestamp in ISO8601 format. Not present if ' - 'subscription has never had a problem with notifications.' + description="(not editable, only present in GET operations): " + "Last failure timestamp in ISO8601 format. Not present if " + "subscription has never had a problem with notifications.", ) lastSuccess: Optional[datetime] = Field( default=None, - description='(not editable, only present in GET operations): ' - 'Timestamp in ISO8601 format for last successful ' - 'notification. Not present if subscription has never ' - 'had a successful notification.' + description="(not editable, only present in GET operations): " + "Timestamp in ISO8601 format for last successful " + "notification. Not present if subscription has never " + "had a successful notification.", ) @@ -412,22 +443,21 @@ class Condition(BaseModel): https://github.com/telefonicaid/fiware-orion/blob/3.8.0/doc/manuals/orion-api.md#subscriptions-based-in-alteration-type """ + attrs: Optional[Union[str, List[str]]] = Field( - default=None, - description='array of attribute names' + default=None, description="array of attribute names" ) expression: Optional[Union[str, Expression]] = Field( default=None, - description='an expression composed of q, mq, georel, geometry and ' - 'coords (see "List entities" operation above about this ' - 'field).' + description="an expression composed of q, mq, georel, geometry and " + 'coords (see "List entities" operation above about this ' + "field).", ) alterationTypes: Optional[List[str]] = Field( - default=None, - description='list of alteration types triggering the subscription' + default=None, description="list of alteration types triggering the subscription" ) - @field_validator('attrs') + @field_validator("attrs") def check_attrs(cls, v): if isinstance(v, list): return v @@ -436,7 +466,7 @@ def check_attrs(cls, v): else: raise TypeError() - @field_validator('alterationTypes') + @field_validator("alterationTypes") def check_alteration_types(cls, v): allowed_types = {"entityCreate", "entityDelete", "entityUpdate", "entityChange"} @@ -445,20 +475,22 @@ def check_alteration_types(cls, v): elif isinstance(v, list): for item in v: if item not in allowed_types: - raise ValueError(f'{item} is not a valid alterationType' - f' allowed values are {allowed_types}') + raise ValueError( + f"{item} is not a valid alterationType" + f" allowed values are {allowed_types}" + ) return v else: - raise ValueError('alterationTypes must be a list of strings') + raise ValueError("alterationTypes must be a list of strings") class Subject(BaseModel): """ Model for subscription subject """ + entities: List[EntityPattern] = Field( - description="A list of objects, each one composed of by an Entity " - "Object:" + description="A list of objects, each one composed of by an Entity " "Object:" ) condition: Optional[Condition] = Field( default=None, @@ -470,59 +502,68 @@ class Subscription(BaseModel): Subscription payload validations https://fiware-orion.readthedocs.io/en/master/user/ngsiv2_implementation_notes/index.html#subscription-payload-validations """ + model_config = ConfigDict(validate_assignment=True) id: Optional[str] = Field( default=None, description="Subscription unique identifier. Automatically created at " - "creation time." + "creation time.", ) description: Optional[str] = Field( default=None, - description="A free text used by the client to describe the " - "subscription." + description="A free text used by the client to describe the " "subscription.", ) status: Optional[Status] = Field( default=Status.ACTIVE, description="Either active (for active subscriptions) or inactive " - "(for inactive subscriptions). If this field is not " - "provided at subscription creation time, new subscriptions " - "are created with the active status, which can be changed" - " by clients afterwards. For expired subscriptions, this " - "attribute is set to expired (no matter if the client " - "updates it to active/inactive). Also, for subscriptions " - "experiencing problems with notifications, the status is " - "set to failed. As soon as the notifications start working " - "again, the status is changed back to active." + "(for inactive subscriptions). If this field is not " + "provided at subscription creation time, new subscriptions " + "are created with the active status, which can be changed" + " by clients afterwards. For expired subscriptions, this " + "attribute is set to expired (no matter if the client " + "updates it to active/inactive). Also, for subscriptions " + "experiencing problems with notifications, the status is " + "set to failed. As soon as the notifications start working " + "again, the status is changed back to active.", ) subject: Subject = Field( description="An object that describes the subject of the subscription.", - json_schema_extra={'example':{ - 'entities': [{'idPattern': '.*', 'type': 'Room'}], - 'condition': { - 'attrs': ['temperature'], - 'expression': {'q': 'temperature>40'}, + json_schema_extra={ + "example": { + "entities": [{"idPattern": ".*", "type": "Room"}], + "condition": { + "attrs": ["temperature"], + "expression": {"q": "temperature>40"}, }, - }} + } + }, ) notification: Notification = Field( description="An object that describes the notification to send when " - "the subscription is triggered.", - json_schema_extra={'example':{ - 'http': {'url': 'http://localhost:1234'}, - 'attrs': ['temperature', 'humidity'], - }} + "the subscription is triggered.", + json_schema_extra={ + "example": { + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], + } + }, ) expires: Optional[datetime] = Field( default=None, description="Subscription expiration date in ISO8601 format. " - "Permanent subscriptions must omit this field." + "Permanent subscriptions must omit this field.", ) - throttling: Optional[conint(strict=True, ge=0, )] = Field( + throttling: Optional[ + conint( + strict=True, + ge=0, + ) + ] = Field( default=None, strict=True, description="Minimal period of time in seconds which " - "must elapse between two consecutive notifications. " - "It is optional." + "must elapse between two consecutive notifications. " + "It is optional.", ) diff --git a/filip/models/ngsi_v2/timeseries.py b/filip/models/ngsi_v2/timeseries.py index 528db31b..7c68ea47 100644 --- a/filip/models/ngsi_v2/timeseries.py +++ b/filip/models/ngsi_v2/timeseries.py @@ -1,6 +1,7 @@ """ Data models for interacting with FIWARE's time series-api (aka QuantumLeap) """ + from __future__ import annotations import logging from typing import Any, List, Union @@ -18,18 +19,19 @@ class TimeSeriesBase(BaseModel): """ Base model for other time series api models """ + index: Union[List[datetime], datetime] = Field( default=None, description="Array of the timestamps which are indexes of the response " - "for the requested data. It's a parallel array to 'values'." - " The timestamp will be in the ISO8601 format " - "(e.g. 2010-10-10T07:09:00.792) or in milliseconds since " - "epoch whichever format was used in the input " - "(notification), but ALWAYS in UTC. When using aggregation " - "options, the format of this remains the same, only the " - "semantics will change. For example, if aggrPeriod is day, " - "each index will be a valid timestamp of a moment in the " - "corresponding day." + "for the requested data. It's a parallel array to 'values'." + " The timestamp will be in the ISO8601 format " + "(e.g. 2010-10-10T07:09:00.792) or in milliseconds since " + "epoch whichever format was used in the input " + "(notification), but ALWAYS in UTC. When using aggregation " + "options, the format of this remains the same, only the " + "semantics will change. For example, if aggrPeriod is day, " + "each index will be a valid timestamp of a moment in the " + "corresponding day.", ) @@ -37,34 +39,38 @@ class TimeSeriesHeader(TimeSeriesBase): """ Model to describe an available entity in the time series api """ + model_config = ConfigDict(populate_by_name=True) # aliases are required due to formally inconsistencies in the api-specs - entityId: str = Field(default=None, - alias="id", - description="The entity id the time series api." - "If the id is unique among all entity " - "types, this could be used to uniquely " - "identify the entity instance. Otherwise," - " you will have to use the entityType " - "attribute to resolve ambiguity.") - entityType: str = Field(default=None, - alias="type", - description="The type of an entity") + entityId: str = Field( + default=None, + alias="id", + description="The entity id the time series api." + "If the id is unique among all entity " + "types, this could be used to uniquely " + "identify the entity instance. Otherwise," + " you will have to use the entityType " + "attribute to resolve ambiguity.", + ) + entityType: str = Field( + default=None, alias="type", description="The type of an entity" + ) class IndexedValues(BaseModel): """ Model for time indexed values """ + values: List[Any] = Field( default=None, description="Array of values of the selected attribute, in the same " - "corresponding order of the 'index' array. When using " - "aggregation options, the format of this remains the same, " - "only the semantics will change. For example, if " - "aggrPeriod is day, each value of course may not " - "correspond to original measurements but rather the " - "aggregate of measurements in each day." + "corresponding order of the 'index' array. When using " + "aggregation options, the format of this remains the same, " + "only the semantics will change. For example, if " + "aggrPeriod is day, each value of course may not " + "correspond to original measurements but rather the " + "aggregate of measurements in each day.", ) @@ -72,16 +78,15 @@ class AttributeValues(IndexedValues): """ Model for indexed values that contain attribute name """ - attrName: str = Field( - title="Attribute name", - description="" - ) + + attrName: str = Field(title="Attribute name", description="") class TimeSeries(TimeSeriesHeader): """ Model for time series data """ + model_config = ConfigDict(populate_by_name=True) attributes: List[AttributeValues] = None @@ -115,12 +120,13 @@ def to_pandas(self) -> pd.DataFrame: Returns: pandas.DataFrame """ - index = pd.Index(data=self.index, name='datetime') + index = pd.Index(data=self.index, name="datetime") attr_names = [attr.attrName for attr in self.attributes] values = np.array([attr.values for attr in self.attributes]).transpose() columns = pd.MultiIndex.from_product( [[self.entityId], [self.entityType], attr_names], - names=['entityId', 'entityType', 'attribute']) + names=["entityId", "entityType", "attribute"], + ) return pd.DataFrame(data=values, index=index, columns=columns) @@ -129,7 +135,8 @@ class AggrMethod(str, Enum): """ Aggregation Methods """ - _init_ = 'value __doc__' + + _init_ = "value __doc__" COUNT = "count", "Number of Entries" SUM = "sum", "Sum" AVG = "avg", "Average" @@ -141,7 +148,8 @@ class AggrPeriod(str, Enum): """ Aggregation Periods """ - _init_ = 'value __doc__' + + _init_ = "value __doc__" YEAR = "year", "year" MONTH = "month", "month" DAY = "day", "day" @@ -157,6 +165,7 @@ class AggrScope(str, Enum): multiple entities instances, you can define the aggregation method to be applied for each entity instance [entity] or across them [global]. """ - _init_ = 'value __doc__' + + _init_ = "value __doc__" ENTITY = "entity", "Entity (default)" GLOBAL = "global", "Global" diff --git a/filip/models/ngsi_v2/units.py b/filip/models/ngsi_v2/units.py index 25b14f25..d0db73ce 100644 --- a/filip/models/ngsi_v2/units.py +++ b/filip/models/ngsi_v2/units.py @@ -9,6 +9,7 @@ https://unece.org/trade/cefact/UNLOCODE-Download https://unece.org/trade/uncefact/cl-recommendations """ + import json import logging import pandas as pd @@ -33,12 +34,13 @@ def load_units() -> pd.DataFrame: Cleaned dataset containing all unit data """ units = load_datapackage( - url="https://github.com/datasets/unece-units-of-measure", - package_name="unece-units")["units_of_measure"] + url="https://github.com/datasets/unece-units-of-measure", + package_name="unece-units", + )["units_of_measure"] # remove deprecated entries units = units.loc[ - ((units.Status.str.casefold() != 'x') & - (units.Status.str.casefold() != 'd'))] + ((units.Status.str.casefold() != "x") & (units.Status.str.casefold() != "d")) + ] return units @@ -52,16 +54,21 @@ class UnitCode(BaseModel): Note: Currently we only support the UN/CEFACT Common Codes """ - type: DataType = Field(default=DataType.TEXT, - # const=True, - description="Data type") - value: str = Field(..., - title="Code of unit ", - description="UN/CEFACT Common Code (3 characters)", - min_length=2, - max_length=3) - - @field_validator('value') + + type: DataType = Field( + default=DataType.TEXT, + # const=True, + description="Data type", + ) + value: str = Field( + ..., + title="Code of unit ", + description="UN/CEFACT Common Code (3 characters)", + min_length=2, + max_length=3, + ) + + @field_validator("value") @classmethod def validate_code(cls, value): units = load_units() @@ -79,16 +86,21 @@ class UnitText(BaseModel): Note: We use the names of units of measurements from UN/CEFACT for validation """ - type: DataType = Field(default=DataType.TEXT, - # const=True, - description="Data type") - value: str = Field(..., - title="Name of unit of measurement", - description="Verbose name of a unit using British " - "spelling in singular form, " - "e.g. 'newton second per metre'") - - @field_validator('value') + + type: DataType = Field( + default=DataType.TEXT, + # const=True, + description="Data type", + ) + value: str = Field( + ..., + title="Name of unit of measurement", + description="Verbose name of a unit using British " + "spelling in singular form, " + "e.g. 'newton second per metre'", + ) + + @field_validator("value") @classmethod def validate_text(cls, value): units = load_units() @@ -96,46 +108,55 @@ def validate_text(cls, value): if len(units.loc[(units.Name.str.casefold() == value.casefold())]) >= 1: return value names = units.Name.tolist() - suggestions = [item[0] for item in process.extract( - query=value.casefold(), - choices=names, - score_cutoff=50, - limit=5)] - raise ValueError(f"Invalid 'name' for unit! '{value}' \n " - f"Did you mean one of the following? \n " - f"{suggestions}") + suggestions = [ + item[0] + for item in process.extract( + query=value.casefold(), choices=names, score_cutoff=50, limit=5 + ) + ] + raise ValueError( + f"Invalid 'name' for unit! '{value}' \n " + f"Did you mean one of the following? \n " + f"{suggestions}" + ) class Unit(BaseModel): """ Model for a unit definition """ - model_config = ConfigDict(extra='ignore', populate_by_name=True) + + model_config = ConfigDict(extra="ignore", populate_by_name=True) _ngsi_version: Literal[NgsiVersion.v2] = NgsiVersion.v2 name: Optional[Union[str, UnitText]] = Field( alias="unitText", default=None, - description="A string or text indicating the unit of measurement") + description="A string or text indicating the unit of measurement", + ) code: Optional[Union[str, UnitCode]] = Field( alias="unitCode", default=None, description="The unit of measurement given using the UN/CEFACT " - "Common Code (3 characters)") + "Common Code (3 characters)", + ) description: Optional[str] = Field( default=None, alias="unitDescription", description="Verbose description of unit", - max_length=350) + max_length=350, + ) symbol: Optional[str] = Field( default=None, alias="unitSymbol", description="The symbol used to represent the unit of measure as " - "in ISO 31 / 80000.") + "in ISO 31 / 80000.", + ) conversion_factor: Optional[str] = Field( default=None, alias="unitConversionFactor", description="The value used to convert units to the equivalent SI " - "unit when applicable.") + "unit when applicable.", + ) @model_validator(mode="before") @classmethod @@ -160,11 +181,11 @@ def check_consistency(cls, values): name = name.value if code and name: - idx = units.index[((units.CommonCode == code) & - (units.Name == name))] + idx = units.index[((units.CommonCode == code) & (units.Name == name))] if idx.empty: - raise ValueError("Invalid combination of 'code' and 'name': ", - code, name) + raise ValueError( + "Invalid combination of 'code' and 'name': ", code, name + ) elif code: idx = units.index[(units.CommonCode == code)] if idx.empty: @@ -173,15 +194,18 @@ def check_consistency(cls, values): idx = units.index[(units.Name == name)] if idx.empty: names = units.Name.tolist() - suggestions = [item[0] for item in process.extract( - query=name.casefold(), - choices=names, - score_cutoff=50, - limit=5)] - - raise ValueError(f"Invalid 'name' for unit! '{name}' \n " - f"Did you mean one of the following? \n " - f"{suggestions}") + suggestions = [ + item[0] + for item in process.extract( + query=name.casefold(), choices=names, score_cutoff=50, limit=5 + ) + ] + + raise ValueError( + f"Invalid 'name' for unit! '{name}' \n " + f"Did you mean one of the following? \n " + f"{suggestions}" + ) else: raise AssertionError("'name' or 'code' must be provided!") @@ -199,6 +223,7 @@ class Units: Class for easy accessing the data set of UNECE units from here. "https://github.com/datasets/unece-units-of-measure" """ + units = load_units() def __getattr__(self, item): @@ -212,7 +237,7 @@ def __getattr__(self, item): Returns: Unit """ - item = item.casefold().replace('_', ' ') + item = item.casefold().replace("_", " ") return self.__getitem__(item) @property @@ -222,8 +247,10 @@ def quantities(self): Returns: list of units ordered by measured quantities """ - raise NotImplementedError("The used dataset does currently not " - "contain the information about quantity") + raise NotImplementedError( + "The used dataset does currently not " + "contain the information about quantity" + ) def __getitem__(self, item: str) -> Unit: """ @@ -235,18 +262,25 @@ def __getitem__(self, item: str) -> Unit: Returns: Unit """ - idx = self.units.index[((self.units.CommonCode == item.upper()) | - (self.units.Name.str.casefold() == item.casefold()))] + idx = self.units.index[ + ( + (self.units.CommonCode == item.upper()) + | (self.units.Name.str.casefold() == item.casefold()) + ) + ] if idx.empty: names = self.units.Name.tolist() - suggestions = [item[0] for item in process.extract( - query=item.casefold(), - choices=names, - score_cutoff=50, - limit=5)] - raise ValueError(f"Invalid 'name' for unit! '{item}' \n " - f"Did you mean one of the following? \n " - f"{suggestions}") + suggestions = [ + item[0] + for item in process.extract( + query=item.casefold(), choices=names, score_cutoff=50, limit=5 + ) + ] + raise ValueError( + f"Invalid 'name' for unit! '{item}' \n " + f"Did you mean one of the following? \n " + f"{suggestions}" + ) return Unit(code=self.units.CommonCode[idx[0]]) @@ -321,13 +355,11 @@ def validate_unit_data(data: Dict) -> Dict: Returns: Validated dictionary of metadata """ - _unit_models = {'unit': Unit, - "unitText": UnitText, - "unitCode": UnitCode} + _unit_models = {"unit": Unit, "unitText": UnitText, "unitCode": UnitCode} for modelname, model in _unit_models.items(): if data.get("name", "").casefold() == modelname.casefold(): - if data.get("name", "").casefold() == 'unit': - data["type"] = 'Unit' + if data.get("name", "").casefold() == "unit": + data["type"] = "Unit" data["value"] = model.model_validate(data["value"]) # data["value"] = model.parse_obj(data["value"]) return data @@ -335,5 +367,4 @@ def validate_unit_data(data: Dict) -> Dict: data.update(model.model_validate(data).model_dump()) # data.update(model.parse_obj(data).dict()) return data - raise ValueError(f"Invalid unit data found: \n " - f"{json.dumps(data, indent=2)}") + raise ValueError(f"Invalid unit data found: \n " f"{json.dumps(data, indent=2)}") diff --git a/filip/semantics/ontology_parser/post_processer.py b/filip/semantics/ontology_parser/post_processer.py index 3791d809..4f77e84e 100644 --- a/filip/semantics/ontology_parser/post_processer.py +++ b/filip/semantics/ontology_parser/post_processer.py @@ -13,18 +13,28 @@ import stringcase from filip.semantics.ontology_parser.vocabulary_builder import VocabularyBuilder -from filip.semantics.vocabulary import Source, IdType, Vocabulary, \ - DatatypeType, Datatype, Class -from filip.semantics.vocabulary import CombinedDataRelation, \ - CombinedObjectRelation, CombinedRelation +from filip.semantics.vocabulary import ( + Source, + IdType, + Vocabulary, + DatatypeType, + Datatype, + Class, +) +from filip.semantics.vocabulary import ( + CombinedDataRelation, + CombinedObjectRelation, + CombinedRelation, +) class PostProcessor: """Class offering postprocessing as cls-methods for a vocabulary""" @classmethod - def post_process_vocabulary(cls, vocabulary: Vocabulary, - old_vocabulary: Optional[Vocabulary] = None): + def post_process_vocabulary( + cls, vocabulary: Vocabulary, old_vocabulary: Optional[Vocabulary] = None + ): """Main methode to be called for post processing Args: @@ -52,8 +62,9 @@ def post_process_vocabulary(cls, vocabulary: Vocabulary, cls._combine_relations(voc_builder) if old_vocabulary is not None: - cls.transfer_settings(new_vocabulary=vocabulary, - old_vocabulary=old_vocabulary) + cls.transfer_settings( + new_vocabulary=vocabulary, old_vocabulary=old_vocabulary + ) cls._apply_vocabulary_settings(voc_builder) cls._ensure_parent_class(voc_builder) @@ -65,7 +76,7 @@ def post_process_vocabulary(cls, vocabulary: Vocabulary, @classmethod def _set_labels(cls, voc_builder: VocabularyBuilder): - """ If entities have no label, extract their label from the iri + """If entities have no label, extract their label from the iri Args: voc_builder: Builder object for Vocabulary @@ -78,7 +89,7 @@ def _set_labels(cls, voc_builder: VocabularyBuilder): @classmethod def _add_predefined_source(cls, voc_builder: VocabularyBuilder): - """ Add a special source to the vocabulary: PREDEFINED + """Add a special source to the vocabulary: PREDEFINED Args: voc_builder: Builder object for Vocabulary @@ -87,8 +98,11 @@ def _add_predefined_source(cls, voc_builder: VocabularyBuilder): None """ if "PREDEFINED" not in voc_builder.vocabulary.sources: - source = Source(source_name="Predefined", - timestamp=datetime.datetime.now(), predefined=True) + source = Source( + source_name="Predefined", + timestamp=datetime.datetime.now(), + predefined=True, + ) voc_builder.add_source(source, "PREDEFINED") @classmethod @@ -122,203 +136,309 @@ def _add_predefined_datatypes(cls, voc_builder: VocabularyBuilder): None """ # Test if datatype_catalogue were already added, if yes skip - if 'http://www.w3.org/2002/07/owl#rational' in \ - voc_builder.vocabulary.datatypes.keys(): + if ( + "http://www.w3.org/2002/07/owl#rational" + in voc_builder.vocabulary.datatypes.keys() + ): return voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2002/07/owl#rational", - comment="All numbers allowed", - type=DatatypeType.number, - number_decimal_allowed=True)) + Datatype( + iri="http://www.w3.org/2002/07/owl#rational", + comment="All numbers allowed", + type=DatatypeType.number, + number_decimal_allowed=True, + ) + ) voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2002/07/owl#real", - comment="All whole numbers allowed", - type=DatatypeType.number, - number_decimal_allowed=False)) + Datatype( + iri="http://www.w3.org/2002/07/owl#real", + comment="All whole numbers allowed", + type=DatatypeType.number, + number_decimal_allowed=False, + ) + ) voc_builder.add_predefined_datatype( Datatype( iri="http://www.w3.org/1999/02/22-rdf-syntax-ns#PlainLiteral", comment="All strings allowed", - type=DatatypeType.string)) + type=DatatypeType.string, + ) + ) voc_builder.add_predefined_datatype( Datatype( iri="http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral", comment="XML Syntax required", - type=DatatypeType.string)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2000/01/rdf-schema#Literal", - comment="All strings allowed", - type=DatatypeType.string)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#anyURI", - comment="Needs to start with http://", - type=DatatypeType.string)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#base64Binary", - comment="Base64Binary", - type=DatatypeType.string)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#boolean", - comment="True or False", - type=DatatypeType.enum, - enum_values=["True", "False"])) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#byte", - comment="Byte Number", - type=DatatypeType.number, - number_has_range=True, - number_range_min=-128, - number_range_max=127)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#dateTime", - comment="Date with possible timezone", - type=DatatypeType.date)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#dateTimeStamp", - comment="Date", - type=DatatypeType.date)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#decimal", - comment="All decimal numbers", - type=DatatypeType.number, - number_decimal_allowed=True)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#double", - comment="64 bit decimal", - type=DatatypeType.number, - number_decimal_allowed=True)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#float", - comment="32 bit decimal", - type=DatatypeType.number, - number_decimal_allowed=True)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#hexBinary", - comment="Hexadecimal", - type=DatatypeType.string, - allowed_chars=["0", "1", "2", "3", "4", "5", "6", "7", "8", - "9", "A", "B", "C", "D", "E", "F"])) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#int", - comment="Signed 32 bit number", - type=DatatypeType.number, - number_has_range=True, - number_range_min=-2147483648, - number_range_max=2147483647)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#integer", - comment="All whole numbers", - type=DatatypeType.number, - number_decimal_allowed=False)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#language", - comment="Language code, e.g: en, en-US, fr, or fr-FR", - type=DatatypeType.string)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#long", - comment="Signed 64 bit integer", - type=DatatypeType.number, - number_has_range=True, - number_range_min=-9223372036854775808, - number_range_max=9223372036854775807, - number_decimal_allowed=False)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#Name", - comment="Name string (dont start with number)", - type=DatatypeType.string)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#NCName", - comment="Name string : forbidden", - type=DatatypeType.string, - forbidden_chars=[":"])) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#negativeInteger", - comment="All negative whole numbers", - type=DatatypeType.number, - number_has_range=True, - number_range_max=-1 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#NMTOKEN", - comment="Token string", - type=DatatypeType.string)) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#nonNegativeInteger", - comment="All positive whole numbers", - type=DatatypeType.number, - number_has_range=True, - number_range_min=0 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#nonPositiveInteger", - comment="All negative whole numbers", - type=DatatypeType.number, - number_has_range=True, - number_range_max=-1 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#normalizedString", - comment="normalized String", - type=DatatypeType.string - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#positiveInteger", - comment="All positive whole numbers", - type=DatatypeType.number, - number_has_range=True, - number_range_min=0 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#short", - comment="signed 16 bit number", - type=DatatypeType.number, - number_has_range=True, - number_range_min=-32768, - number_range_max=32767 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#string", - comment="String", - type=DatatypeType.string - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#token", - comment="String", - type=DatatypeType.string - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#unsignedByte", - comment="unsigned 8 bit number", - type=DatatypeType.number, - number_has_range=True, - number_range_min=0, - number_range_max=255 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#unsignedInt", - comment="unsigned 32 bit number", - type=DatatypeType.number, - number_has_range=True, - number_range_min=0, - number_range_max=4294967295 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#unsignedLong", - comment="unsigned 64 bit number", - type=DatatypeType.number, - number_has_range=True, - number_range_min=0, - number_range_max=18446744073709551615 - )) - voc_builder.add_predefined_datatype( - Datatype(iri="http://www.w3.org/2001/XMLSchema#unsignedShort", - comment="unsigned 16 bit number", - type=DatatypeType.number, - number_has_range=True, - number_range_min=0, - number_range_max=65535 - )) + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2000/01/rdf-schema#Literal", + comment="All strings allowed", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#anyURI", + comment="Needs to start with http://", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#base64Binary", + comment="Base64Binary", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#boolean", + comment="True or False", + type=DatatypeType.enum, + enum_values=["True", "False"], + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#byte", + comment="Byte Number", + type=DatatypeType.number, + number_has_range=True, + number_range_min=-128, + number_range_max=127, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#dateTime", + comment="Date with possible timezone", + type=DatatypeType.date, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#dateTimeStamp", + comment="Date", + type=DatatypeType.date, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#decimal", + comment="All decimal numbers", + type=DatatypeType.number, + number_decimal_allowed=True, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#double", + comment="64 bit decimal", + type=DatatypeType.number, + number_decimal_allowed=True, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#float", + comment="32 bit decimal", + type=DatatypeType.number, + number_decimal_allowed=True, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#hexBinary", + comment="Hexadecimal", + type=DatatypeType.string, + allowed_chars=[ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + ], + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#int", + comment="Signed 32 bit number", + type=DatatypeType.number, + number_has_range=True, + number_range_min=-2147483648, + number_range_max=2147483647, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#integer", + comment="All whole numbers", + type=DatatypeType.number, + number_decimal_allowed=False, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#language", + comment="Language code, e.g: en, en-US, fr, or fr-FR", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#long", + comment="Signed 64 bit integer", + type=DatatypeType.number, + number_has_range=True, + number_range_min=-9223372036854775808, + number_range_max=9223372036854775807, + number_decimal_allowed=False, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#Name", + comment="Name string (dont start with number)", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#NCName", + comment="Name string : forbidden", + type=DatatypeType.string, + forbidden_chars=[":"], + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#negativeInteger", + comment="All negative whole numbers", + type=DatatypeType.number, + number_has_range=True, + number_range_max=-1, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#NMTOKEN", + comment="Token string", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#nonNegativeInteger", + comment="All positive whole numbers", + type=DatatypeType.number, + number_has_range=True, + number_range_min=0, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#nonPositiveInteger", + comment="All negative whole numbers", + type=DatatypeType.number, + number_has_range=True, + number_range_max=-1, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#normalizedString", + comment="normalized String", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#positiveInteger", + comment="All positive whole numbers", + type=DatatypeType.number, + number_has_range=True, + number_range_min=0, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#short", + comment="signed 16 bit number", + type=DatatypeType.number, + number_has_range=True, + number_range_min=-32768, + number_range_max=32767, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#string", + comment="String", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#token", + comment="String", + type=DatatypeType.string, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#unsignedByte", + comment="unsigned 8 bit number", + type=DatatypeType.number, + number_has_range=True, + number_range_min=0, + number_range_max=255, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#unsignedInt", + comment="unsigned 32 bit number", + type=DatatypeType.number, + number_has_range=True, + number_range_min=0, + number_range_max=4294967295, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#unsignedLong", + comment="unsigned 64 bit number", + type=DatatypeType.number, + number_has_range=True, + number_range_min=0, + number_range_max=18446744073709551615, + ) + ) + voc_builder.add_predefined_datatype( + Datatype( + iri="http://www.w3.org/2001/XMLSchema#unsignedShort", + comment="unsigned 16 bit number", + type=DatatypeType.number, + number_has_range=True, + number_range_min=0, + number_range_max=65535, + ) + ) @classmethod def _add_owl_thing(cls, voc_builder: VocabularyBuilder): @@ -333,10 +453,12 @@ def _add_owl_thing(cls, voc_builder: VocabularyBuilder): Returns: None """ - root_class = Class(iri="http://www.w3.org/2002/07/owl#Thing", - comment="Predefined root_class", - label="Thing", - predefined=True) + root_class = Class( + iri="http://www.w3.org/2002/07/owl#Thing", + comment="Predefined root_class", + label="Thing", + predefined=True, + ) # as it is the root object it is only a parent of classes which have no # parents yet @@ -376,7 +498,8 @@ def _ensure_parent_class(cls, voc_builder: VocabularyBuilder): if not class_.iri == "http://www.w3.org/2002/07/owl#Thing": if len(class_.parent_class_iris) == 0: class_.parent_class_iris.append( - "http://www.w3.org/2002/07/owl#Thing") + "http://www.w3.org/2002/07/owl#Thing" + ) @classmethod def _apply_vocabulary_settings(cls, voc_builder: VocabularyBuilder): @@ -393,8 +516,12 @@ def _apply_vocabulary_settings(cls, voc_builder: VocabularyBuilder): settings = vocabulary.settings def to_pascal_case(string: str) -> str: - return stringcase.pascalcase(string).replace("_", "").\ - replace(" ", "").replace("-", "") + return ( + stringcase.pascalcase(string) + .replace("_", "") + .replace(" ", "") + .replace("-", "") + ) def to_camel_case(string: str) -> str: camel_string = stringcase.camelcase(string) @@ -402,7 +529,7 @@ def to_camel_case(string: str) -> str: def to_snake_case(string: str) -> str: camel_string = to_pascal_case(string) - return re.sub(r'(? List[str]: + cls, combined_relations: List[CombinedRelation], vocabulary: Vocabulary + ) -> List[str]: """sort given CombinedRelations according to their labels Args: @@ -659,8 +793,7 @@ def _mirror_object_property_inverses(cls, voc_builder: VocabularyBuilder): inverse_prop.add_inverse_property_iri(obj_prop_iri) @classmethod - def transfer_settings(cls, new_vocabulary: Vocabulary, - old_vocabulary: Vocabulary): + def transfer_settings(cls, new_vocabulary: Vocabulary, old_vocabulary: Vocabulary): """ Transfer all the user made settings (labels, ..) from an old vocabulary to a new vocabulary @@ -686,4 +819,3 @@ def transfer_settings(cls, new_vocabulary: Vocabulary, if iri in new_vocabulary.data_properties: new_data_property = new_vocabulary.data_properties[iri] new_data_property.field_type = data_property.field_type - diff --git a/filip/semantics/ontology_parser/rdfparser.py b/filip/semantics/ontology_parser/rdfparser.py index 5666b702..fce34041 100644 --- a/filip/semantics/ontology_parser/rdfparser.py +++ b/filip/semantics/ontology_parser/rdfparser.py @@ -9,16 +9,30 @@ from filip.models.base import LogLevel from filip.semantics.ontology_parser.vocabulary_builder import VocabularyBuilder -from filip.semantics.vocabulary import Source, IdType, \ - Vocabulary,RestrictionType, ObjectProperty, DataProperty, Relation, \ - TargetStatement, StatementType, DatatypeType, Datatype, Class, Individual - - -specifier_base_iris = ["http://www.w3.org/2002/07/owl", - "http://www.w3.org/1999/02/22-rdf-syntax-ns", - "http://www.w3.org/XML/1998/namespace", - "http://www.w3.org/2001/XMLSchema", - "http://www.w3.org/2000/01/rdf-schema"] +from filip.semantics.vocabulary import ( + Source, + IdType, + Vocabulary, + RestrictionType, + ObjectProperty, + DataProperty, + Relation, + TargetStatement, + StatementType, + DatatypeType, + Datatype, + Class, + Individual, +) + + +specifier_base_iris = [ + "http://www.w3.org/2002/07/owl", + "http://www.w3.org/1999/02/22-rdf-syntax-ns", + "http://www.w3.org/XML/1998/namespace", + "http://www.w3.org/2001/XMLSchema", + "http://www.w3.org/2000/01/rdf-schema", +] """ Defines a set of base iris, that describe elements that belong to the description language not the ontology itself @@ -30,13 +44,14 @@ class Tags(str, Enum): Collection of tags used as structures in ontologies, that were used more than once in the rdfparser code """ - rdf_type = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', - owl_intersection = 'http://www.w3.org/2002/07/owl#intersectionOf', - owl_union = 'http://www.w3.org/2002/07/owl#unionOf', - owl_one_of = 'http://www.w3.org/2002/07/owl#oneOf', - owl_individual = 'http://www.w3.org/2002/07/owl#NamedIndividual', - owl_on_class = 'http://www.w3.org/2002/07/owl#onClass', - owl_on_data_range = 'http://www.w3.org/2002/07/owl#onDataRange' + + rdf_type = ("http://www.w3.org/1999/02/22-rdf-syntax-ns#type",) + owl_intersection = ("http://www.w3.org/2002/07/owl#intersectionOf",) + owl_union = ("http://www.w3.org/2002/07/owl#unionOf",) + owl_one_of = ("http://www.w3.org/2002/07/owl#oneOf",) + owl_individual = ("http://www.w3.org/2002/07/owl#NamedIndividual",) + owl_on_class = ("http://www.w3.org/2002/07/owl#onClass",) + owl_on_data_range = "http://www.w3.org/2002/07/owl#onDataRange" def get_iri_from_uriref(uriref: rdflib.URIRef) -> str: @@ -54,12 +69,12 @@ def get_iri_from_uriref(uriref: rdflib.URIRef) -> str: def get_base_out_of_iri(iri: str) -> str: """Give an iri, returns an the ontology base name - Args: - iri + Args: + iri - Returns: - str - """ + Returns: + str + """ if "#" in iri: index = iri.find("#") return iri[:index] @@ -74,15 +89,16 @@ class RdfParser: """ Class that parses a given source into a vocabulary. """ + def __init__(self): self.current_source = None """Current source which is parsed, used for Log entries""" self.current_class_iri = None """Iri of class which is currently parsed, used for Log entries""" - def _add_logging_information(self, level: LogLevel, - entity_type: IdType, entity_iri: str, - msg: str): + def _add_logging_information( + self, level: LogLevel, entity_type: IdType, entity_iri: str, msg: str + ): """Add an entry to the parsing log Args: @@ -95,12 +111,14 @@ def _add_logging_information(self, level: LogLevel, None """ if self.current_source is not None: - self.current_source.add_parsing_log_entry(level, entity_type, - entity_iri, msg) - - def parse_source_into_vocabulary(self, source: Source, - vocabulary: Vocabulary) -> bool: - """ Parse a Source into the given vocabulary + self.current_source.add_parsing_log_entry( + level, entity_type, entity_iri, msg + ) + + def parse_source_into_vocabulary( + self, source: Source, vocabulary: Vocabulary + ) -> bool: + """Parse a Source into the given vocabulary Args: source (Source) vocabulary (Vocabulary) @@ -123,9 +141,12 @@ def parse_source_into_vocabulary(self, source: Source, g.parse(data=source.content, format="turtle") - ontology_nodes = list(g.subjects( - object=rdflib.term.URIRef("http://www.w3.org/2002/07/owl#Ontology"), - predicate=rdflib.term.URIRef(Tags.rdf_type.value))) + ontology_nodes = list( + g.subjects( + object=rdflib.term.URIRef("http://www.w3.org/2002/07/owl#Ontology"), + predicate=rdflib.term.URIRef(Tags.rdf_type.value), + ) + ) # a source may have no ontology iri defined # if wanted on this place more info about the ontology can be extracted @@ -138,9 +159,10 @@ def parse_source_into_vocabulary(self, source: Source, return True - def _is_object_defined_by_other_source(self, a: rdflib.term, - graph: rdflib.Graph) -> bool: - """ Test if the term is defined outside the current source + def _is_object_defined_by_other_source( + self, a: rdflib.term, graph: rdflib.Graph + ) -> bool: + """Test if the term is defined outside the current source Args: a (rdflib.term): Term to check @@ -152,13 +174,17 @@ def _is_object_defined_by_other_source(self, a: rdflib.term, # if an object is defined by an other source it carries the predicate # ("isDefinedBy"). Then don't parse the object - defined_tags = list(graph.objects( - subject=a, predicate=rdflib.term.URIRef( - "http://www.w3.org/2000/01/rdf-schema#isDefinedBy"))) + defined_tags = list( + graph.objects( + subject=a, + predicate=rdflib.term.URIRef( + "http://www.w3.org/2000/01/rdf-schema#isDefinedBy" + ), + ) + ) return len(defined_tags) > 0 - def _parse_to_vocabulary(self, graph: rdflib.Graph, - voc_builder: VocabularyBuilder): + def _parse_to_vocabulary(self, graph: rdflib.Graph, voc_builder: VocabularyBuilder): """Parse an graph that was extracted from a TTL file into the vocabulary Args: @@ -172,9 +198,9 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, # OWLClasses for a in graph.subjects( - object=rdflib.term.URIRef( - "http://www.w3.org/2002/07/owl#Class"), - predicate=rdflib.term.URIRef(Tags.rdf_type.value)): + object=rdflib.term.URIRef("http://www.w3.org/2002/07/owl#Class"), + predicate=rdflib.term.URIRef(Tags.rdf_type.value), + ): if isinstance(a, rdflib.term.BNode): pass @@ -193,8 +219,10 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, # Class properties found_class_iris = set() for class_node in graph.subjects( - predicate=rdflib.term.URIRef( - "http://www.w3.org/2000/01/rdf-schema#subClassOf")): + predicate=rdflib.term.URIRef( + "http://www.w3.org/2000/01/rdf-schema#subClassOf" + ) + ): class_iri = get_iri_from_uriref(class_node) found_class_iris.add(class_iri) @@ -202,26 +230,29 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, for class_iri in found_class_iris: # parent class / relation parsing for sub in graph.objects( - subject=rdflib.term.URIRef(class_iri), - predicate=rdflib.term.URIRef - ('http://www.w3.org/2000/01/rdf-schema#subClassOf')): + subject=rdflib.term.URIRef(class_iri), + predicate=rdflib.term.URIRef( + "http://www.w3.org/2000/01/rdf-schema#subClassOf" + ), + ): self.current_class_iri = class_iri # used only for logging - self._parse_subclass_term(graph=graph, - voc_builder=voc_builder, - node=sub, - class_iri=class_iri) + self._parse_subclass_term( + graph=graph, voc_builder=voc_builder, node=sub, class_iri=class_iri + ) # OWlObjectProperties for a in graph.subjects( - object=rdflib.term.URIRef( - "http://www.w3.org/2002/07/owl#ObjectProperty"), - predicate=rdflib.term.URIRef(Tags.rdf_type.value)): + object=rdflib.term.URIRef("http://www.w3.org/2002/07/owl#ObjectProperty"), + predicate=rdflib.term.URIRef(Tags.rdf_type.value), + ): if isinstance(a, rdflib.term.BNode): - self._add_logging_information(LogLevel.WARNING, - IdType.object_property, - "unknown", - "Found unparseable statement") + self._add_logging_information( + LogLevel.WARNING, + IdType.object_property, + "unknown", + "Found unparseable statement", + ) else: # defined in other source -> ignore @@ -234,27 +265,36 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, voc_builder.add_object_property(obj_prop) # extract inverse properties, it can be multiple but only # URIRefs allowed no union/intersection - for inverse_iri_node in graph.objects(subject=a, - predicate=rdflib.term.URIRef( - 'http://www.w3.org/2002/07/owl#inverseOf')): + for inverse_iri_node in graph.objects( + subject=a, + predicate=rdflib.term.URIRef( + "http://www.w3.org/2002/07/owl#inverseOf" + ), + ): if isinstance(inverse_iri_node, rdflib.term.BNode): self._add_logging_information( - LogLevel.CRITICAL, IdType.object_property, iri, - "Complex inverseProperty statements aren't allowed") + LogLevel.CRITICAL, + IdType.object_property, + iri, + "Complex inverseProperty statements aren't allowed", + ) else: inverse_iri = get_iri_from_uriref(inverse_iri_node) obj_prop.add_inverse_property_iri(inverse_iri) # OWlDataProperties for a in graph.subjects( - object=rdflib.term.URIRef( - "http://www.w3.org/2002/07/owl#DatatypeProperty"), - predicate=rdflib.term.URIRef(Tags.rdf_type.value)): + object=rdflib.term.URIRef("http://www.w3.org/2002/07/owl#DatatypeProperty"), + predicate=rdflib.term.URIRef(Tags.rdf_type.value), + ): if isinstance(a, rdflib.term.BNode): - self._add_logging_information(LogLevel.WARNING, - IdType.data_property, "unknown", - "Found unparseable statement") + self._add_logging_information( + LogLevel.WARNING, + IdType.data_property, + "unknown", + "Found unparseable statement", + ) else: # defined in other source -> ignore @@ -271,16 +311,16 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, # the predefined are automatically added at the start # of post processing for a in graph.subjects( - object=rdflib.term.URIRef( - "http://www.w3.org/2000/01/rdf-schema#Datatype"), - predicate=rdflib.term.URIRef(Tags.rdf_type.value)): + object=rdflib.term.URIRef("http://www.w3.org/2000/01/rdf-schema#Datatype"), + predicate=rdflib.term.URIRef(Tags.rdf_type.value), + ): if isinstance(a, rdflib.term.BNode): # self._add_logging_information(LogLevel.WARNING, # IdType.datatype, "unknown", # "Found unparseable statement") pass - #e.g: : + # e.g: : # customDataType4 rdf:type rdfs:Datatype ; # owl:equivalentClass [ rdf:type rdfs:Datatype ;.... # the second Datatype triggers this if condition, @@ -307,19 +347,24 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, enum_values = [] for equivalent_class in graph.objects( - subject=a, - predicate=rdflib.term.URIRef( - "http://www.w3.org/2002/07/owl#equivalentClass")): + subject=a, + predicate=rdflib.term.URIRef( + "http://www.w3.org/2002/07/owl#equivalentClass" + ), + ): if isinstance(equivalent_class, rdflib.term.URIRef): # points to an other defined datatype, ignore pass else: # is a bNode and points to owl:oneOf - enum_literals = self.\ - _extract_objects_out_of_single_combination( - graph, equivalent_class, accept_and=False, - accept_or=False, accept_one_of=True) + enum_literals = self._extract_objects_out_of_single_combination( + graph, + equivalent_class, + accept_and=False, + accept_or=False, + accept_one_of=True, + ) for literal in enum_literals: enum_values.append(str(literal)) datatype.enum_values = enum_values @@ -331,13 +376,17 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, # OWLIndividuals for a in graph.subjects( - object=rdflib.term.URIRef(Tags.owl_individual.value), - predicate=rdflib.term.URIRef(Tags.rdf_type.value)): + object=rdflib.term.URIRef(Tags.owl_individual.value), + predicate=rdflib.term.URIRef(Tags.rdf_type.value), + ): if isinstance(a, rdflib.term.BNode): - self._add_logging_information(LogLevel.WARNING, - IdType.individual, "unknown", - "Found unparseable statement") + self._add_logging_information( + LogLevel.WARNING, + IdType.individual, + "unknown", + "Found unparseable statement", + ) else: # defined in other source -> ignore @@ -345,22 +394,22 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, continue iri, label, comment = self._extract_annotations(graph, a) - objects = graph.objects(subject=a, - predicate= - rdflib.term.URIRef(Tags.rdf_type.value)) + objects = graph.objects( + subject=a, predicate=rdflib.term.URIRef(Tags.rdf_type.value) + ) # superclasses = types types = [] for object in objects: - if not object == \ - rdflib.term.URIRef(Tags.owl_individual.value): - types.extend(self. - _extract_objects_out_of_layered_combination( - graph, object, True, False)) + if not object == rdflib.term.URIRef(Tags.owl_individual.value): + types.extend( + self._extract_objects_out_of_layered_combination( + graph, object, True, False + ) + ) individual = Individual(iri=iri, label=label, comment=comment) for type in types: - individual.parent_class_iris.append( - get_iri_from_uriref(type)) + individual.parent_class_iris.append(get_iri_from_uriref(type)) voc_builder.add_individual(individual=individual) # As seen for example in the bricks ontology an individual can be @@ -370,11 +419,10 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, # as we may not have loaded all dependencies we can not simply look it # up in vocabulary # -> getbase uri of statement and filter all known specifier uris - for sub in graph.subjects( - predicate=rdflib.term.URIRef(Tags.rdf_type.value)): - for obj in graph.objects(subject=sub, - predicate= - rdflib.term.URIRef(Tags.rdf_type.value)): + for sub in graph.subjects(predicate=rdflib.term.URIRef(Tags.rdf_type.value)): + for obj in graph.objects( + subject=sub, predicate=rdflib.term.URIRef(Tags.rdf_type.value) + ): if isinstance(obj, rdflib.term.BNode): continue @@ -382,20 +430,17 @@ def _parse_to_vocabulary(self, graph: rdflib.Graph, obj_base_iri = get_base_out_of_iri(iri=obj_iri) if obj_base_iri not in specifier_base_iris: - iri, label, comment = \ - self._extract_annotations(graph, sub) + iri, label, comment = self._extract_annotations(graph, sub) if not voc_builder.entity_is_known(iri): - iri, label, comment = \ - self._extract_annotations(graph, sub) - individual = Individual(iri=iri, - label=label, - comment=comment) + iri, label, comment = self._extract_annotations(graph, sub) + individual = Individual(iri=iri, label=label, comment=comment) individual.parent_class_iris.append(obj_iri) voc_builder.add_individual(individual) - def _extract_annotations(self, graph: rdflib.Graph, - node: rdflib.term.URIRef) -> Tuple[str, str, str]: - """ Extract out of a node term the owl annotations (iri, label, comment) + def _extract_annotations( + self, graph: rdflib.Graph, node: rdflib.term.URIRef + ) -> Tuple[str, str, str]: + """Extract out of a node term the owl annotations (iri, label, comment) Args: graph (rdflib.graph): Graph describing ontology @@ -410,9 +455,13 @@ def _extract_annotations(self, graph: rdflib.Graph, return iri, label, comment - def _parse_subclass_term(self, graph: rdflib.Graph, - voc_builder: VocabularyBuilder, - node: rdflib.term, class_iri: str): + def _parse_subclass_term( + self, + graph: rdflib.Graph, + voc_builder: VocabularyBuilder, + node: rdflib.term, + class_iri: str, + ): """Parse a subclass term of the given node and class_iri Args: @@ -461,21 +510,31 @@ def _parse_subclass_term(self, graph: rdflib.Graph, # Combination of statements if rdflib.term.URIRef(Tags.owl_intersection.value) in predicates: objects = self._extract_objects_out_of_single_combination( - graph, node, True, False) + graph, node, True, False + ) for object in objects: - self._parse_subclass_term(graph=graph, - voc_builder=voc_builder, - node=object, class_iri=class_iri) + self._parse_subclass_term( + graph=graph, + voc_builder=voc_builder, + node=object, + class_iri=class_iri, + ) elif rdflib.term.URIRef(Tags.owl_union.value) in predicates: self._add_logging_information( - LogLevel.CRITICAL, IdType.class_, class_iri, - "Relation statements combined with or") + LogLevel.CRITICAL, + IdType.class_, + class_iri, + "Relation statements combined with or", + ) elif rdflib.term.URIRef(Tags.owl_one_of.value) in predicates: self._add_logging_information( - LogLevel.CRITICAL, IdType.class_, class_iri, - "Relation statements combined with oneOf") + LogLevel.CRITICAL, + IdType.class_, + class_iri, + "Relation statements combined with oneOf", + ) # Relation statement else: @@ -488,23 +547,31 @@ def _parse_subclass_term(self, graph: rdflib.Graph, if predicates[i] == rdflib.term.URIRef(Tags.rdf_type.value): rdf_type = get_iri_from_uriref(objects[i]) elif predicates[i] == rdflib.term.URIRef( - "http://www.w3.org/2002/07/owl#onProperty"): + "http://www.w3.org/2002/07/owl#onProperty" + ): owl_on_property = get_iri_from_uriref(objects[i]) else: - additional_statements[ - get_iri_from_uriref(predicates[i])] = objects[i] + additional_statements[get_iri_from_uriref(predicates[i])] = ( + objects[i] + ) relation_is_ok = True if not rdf_type == "http://www.w3.org/2002/07/owl#Restriction": self._add_logging_information( - LogLevel.CRITICAL, IdType.class_, class_iri, - "Class has an unknown subClass statement") + LogLevel.CRITICAL, + IdType.class_, + class_iri, + "Class has an unknown subClass statement", + ) relation_is_ok = False if owl_on_property == "": self._add_logging_information( - LogLevel.CRITICAL, IdType.class_, class_iri, - "Class has a relation without a property") + LogLevel.CRITICAL, + IdType.class_, + class_iri, + "Class has a relation without a property", + ) relation_is_ok = False # object or data relation? @@ -520,34 +587,38 @@ def _parse_subclass_term(self, graph: rdflib.Graph, # go through the additional statement to figure out the # targetIRI and the restrictionType/cardinality - self._parse_relation_type(graph, relation, - additional_statements) + self._parse_relation_type(graph, relation, additional_statements) # parent-class statement or empty list element else: # owlThing is the root object, but it is not declared as a class # in the file to prevent None pointer when looking up parents, # a class that has a parent owlThing simply has no parents - if not get_iri_from_uriref(node) == \ - "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil": + if ( + not get_iri_from_uriref(node) + == "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil" + ): # ignore empty lists - if not get_iri_from_uriref(node) == \ - "http://www.w3.org/2002/07/owl#Thing": - voc_builder.vocabulary.\ - get_class_by_iri(class_iri).parent_class_iris.\ - append(get_iri_from_uriref(node)) - - def _parse_relation_type(self, graph: rdflib.Graph, - relation: Relation, statements: {}): + if ( + not get_iri_from_uriref(node) + == "http://www.w3.org/2002/07/owl#Thing" + ): + voc_builder.vocabulary.get_class_by_iri( + class_iri + ).parent_class_iris.append(get_iri_from_uriref(node)) + + def _parse_relation_type( + self, graph: rdflib.Graph, relation: Relation, statements: {} + ): """ Parse the relation type and depending on the result the cardinality or value of relation - + Args: graph: underlying ontology graph relation: Relation object into which the information are saved statements: Ontology statements concerning the relation - + Returns: None """ @@ -555,57 +626,65 @@ def _parse_relation_type(self, graph: rdflib.Graph, for statement in statements: if statement == "http://www.w3.org/2002/07/owl#someValuesFrom": relation.restriction_type = RestrictionType.some - self._parse_relation_values(graph, relation, - statements[statement]) + self._parse_relation_values(graph, relation, statements[statement]) elif statement == "http://www.w3.org/2002/07/owl#allValuesFrom": relation.restriction_type = RestrictionType.only - self._parse_relation_values(graph, relation, - statements[statement]) + self._parse_relation_values(graph, relation, statements[statement]) elif statement == "http://www.w3.org/2002/07/owl#hasValue": relation.restriction_type = RestrictionType.value # has Value can only point to a single value - self._parse_has_value(graph, relation, - statements[statement]) + self._parse_has_value(graph, relation, statements[statement]) elif statement == "http://www.w3.org/2002/07/owl#maxCardinality": relation.restriction_type = RestrictionType.max - self._parse_cardinality(graph, relation, statement, - statements, treated_statements) + self._parse_cardinality( + graph, relation, statement, statements, treated_statements + ) elif statement == "http://www.w3.org/2002/07/owl#minCardinality": relation.restriction_type = RestrictionType.min - self._parse_cardinality(graph, relation, statement, - statements, treated_statements) + self._parse_cardinality( + graph, relation, statement, statements, treated_statements + ) elif statement == "http://www.w3.org/2002/07/owl#cardinality": relation.restriction_type = RestrictionType.exactly - self._parse_cardinality(graph, relation, statement, - statements, treated_statements) - elif statement == \ - "http://www.w3.org/2002/07/owl#maxQualifiedCardinality": + self._parse_cardinality( + graph, relation, statement, statements, treated_statements + ) + elif statement == "http://www.w3.org/2002/07/owl#maxQualifiedCardinality": relation.restriction_type = RestrictionType.max - self._parse_cardinality(graph, relation, statement, - statements, treated_statements) - elif statement == \ - "http://www.w3.org/2002/07/owl#minQualifiedCardinality": + self._parse_cardinality( + graph, relation, statement, statements, treated_statements + ) + elif statement == "http://www.w3.org/2002/07/owl#minQualifiedCardinality": relation.restriction_type = RestrictionType.min - self._parse_cardinality(graph, relation, statement, - statements, treated_statements) - elif statement == \ - "http://www.w3.org/2002/07/owl#qualifiedCardinality": + self._parse_cardinality( + graph, relation, statement, statements, treated_statements + ) + elif statement == "http://www.w3.org/2002/07/owl#qualifiedCardinality": relation.restriction_type = RestrictionType.exactly - self._parse_cardinality(graph, relation, statement, - statements, treated_statements) + self._parse_cardinality( + graph, relation, statement, statements, treated_statements + ) treated_statements.append(statement) for statement in statements: if statement not in treated_statements: self._add_logging_information( - LogLevel.CRITICAL, IdType.class_, self.current_class_iri, - "Relation with property {} has an untreated restriction " - "{}".format(relation.property_iri, statement)) - - def _parse_cardinality(self, graph: rdflib.Graph, - relation: Relation, statement, statements, - treated_statements): + LogLevel.CRITICAL, + IdType.class_, + self.current_class_iri, + "Relation with property {} has an untreated restriction " + "{}".format(relation.property_iri, statement), + ) + + def _parse_cardinality( + self, + graph: rdflib.Graph, + relation: Relation, + statement, + statements, + treated_statements, + ): """Parse the cardinality of a relation Args: @@ -641,19 +720,21 @@ def _parse_cardinality(self, graph: rdflib.Graph, relation.restriction_cardinality = statements[statement].value datatype = "http://www.w3.org/2001/XMLSchema#string" - target_statement = TargetStatement(type=StatementType.LEAF, - target_iri=datatype) + target_statement = TargetStatement( + type=StatementType.LEAF, target_iri=datatype + ) relation.target_statement = target_statement - def _parse_has_value(self, graph: rdflib.Graph, relation: Relation, - node: rdflib.term): + def _parse_has_value( + self, graph: rdflib.Graph, relation: Relation, node: rdflib.term + ): """Parse the value of a relation Args: graph: underlying ontology graph relation: Relation object into which the information are saved node: (complex) Graph node containing the value - + Returns: None """ @@ -665,10 +746,12 @@ def _parse_has_value(self, graph: rdflib.Graph, relation: Relation, IdType.class_, self.current_class_iri, f"In hasValue relation with property {relation.property_iri} " - f"target is a complex expression") + f"target is a complex expression", + ) - def _parse_relation_values(self, graph: rdflib.Graph, - relation: Relation, node: rdflib.term): + def _parse_relation_values( + self, graph: rdflib.Graph, relation: Relation, node: rdflib.term + ): """ Parse the value of a relation out of a node that can be complex; consisting out of a combination of multiple other nodes @@ -692,23 +775,27 @@ def _parse_relation_values(self, graph: rdflib.Graph, current_statement.set_target(target_iri=target_iri) else: - if rdflib.term.URIRef(Tags.owl_intersection.value) in \ - graph.predicates(subject=current_term): + if rdflib.term.URIRef(Tags.owl_intersection.value) in graph.predicates( + subject=current_term + ): current_statement.type = StatementType.AND - elif rdflib.term.URIRef(Tags.owl_union.value) in \ - graph.predicates(subject=current_term): + elif rdflib.term.URIRef(Tags.owl_union.value) in graph.predicates( + subject=current_term + ): current_statement.type = StatementType.OR else: current_statement.set_target( target_iri="Target statement has no iri", - target_data_value=str(current_term)) + target_data_value=str(current_term), + ) continue child_nodes = self._extract_objects_out_of_single_combination( - graph, current_term, True, True) + graph, current_term, True, True + ) for child_node in child_nodes: new_statement = TargetStatement() current_statement.target_statements.append(new_statement) @@ -721,11 +808,14 @@ def _parse_relation_values(self, graph: rdflib.Graph, # this methode extracts all objects of a single layered intersection, # if the intersection contains further intersections these are contained in # the result list as BNode - def _extract_objects_out_of_single_combination(self, graph: rdflib.Graph, - node: rdflib.term.BNode, - accept_and: bool, - accept_or: bool, - accept_one_of: bool = False): + def _extract_objects_out_of_single_combination( + self, + graph: rdflib.Graph, + node: rdflib.term.BNode, + accept_and: bool, + accept_or: bool, + accept_one_of: bool = False, + ): """ An intersection/union is a basic list, it consits out of a chain of bnode,where each bnode has the "first"and "rest" predicate, @@ -753,28 +843,36 @@ def _extract_objects_out_of_single_combination(self, graph: rdflib.Graph, # the passed startnode needs to contain an intersection or a union # both at the same time should not be possible start_node = None - if rdflib.term.URIRef(Tags.owl_intersection.value) \ - in predicates: + if rdflib.term.URIRef(Tags.owl_intersection.value) in predicates: if accept_and: - start_node = next(graph.objects( - subject=node, - predicate=rdflib.term.URIRef(Tags.owl_intersection.value))) - elif rdflib.term.URIRef(Tags.owl_union.value) \ - in predicates: + start_node = next( + graph.objects( + subject=node, + predicate=rdflib.term.URIRef(Tags.owl_intersection.value), + ) + ) + elif rdflib.term.URIRef(Tags.owl_union.value) in predicates: if accept_or: - start_node = next(graph.objects( - subject=node, - predicate=rdflib.term.URIRef(Tags.owl_union.value))) - elif rdflib.term.URIRef(Tags.owl_one_of.value) \ - in predicates: + start_node = next( + graph.objects( + subject=node, predicate=rdflib.term.URIRef(Tags.owl_union.value) + ) + ) + elif rdflib.term.URIRef(Tags.owl_one_of.value) in predicates: if accept_one_of: - start_node = next(graph.objects( - subject=node, - predicate=rdflib.term.URIRef(Tags.owl_one_of.value))) + start_node = next( + graph.objects( + subject=node, + predicate=rdflib.term.URIRef(Tags.owl_one_of.value), + ) + ) else: self._add_logging_information( - LogLevel.CRITICAL, IdType.class_, self.current_class_iri, - f"Intern Error - invalid {node} passed to list extraction") + LogLevel.CRITICAL, + IdType.class_, + self.current_class_iri, + f"Intern Error - invalid {node} passed to list extraction", + ) result = [] rest = start_node @@ -782,21 +880,36 @@ def _extract_objects_out_of_single_combination(self, graph: rdflib.Graph, return [] while not rest == rdflib.term.URIRef( - 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'): - - first = next(graph.objects(predicate=rdflib.term.URIRef( - 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first'), - subject=rest)) + "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil" + ): + + first = next( + graph.objects( + predicate=rdflib.term.URIRef( + "http://www.w3.org/1999/02/22-rdf-syntax-ns#first" + ), + subject=rest, + ) + ) result.append(first) - rest = next(graph.objects(predicate=rdflib.term.URIRef( - 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'), - subject=rest)) + rest = next( + graph.objects( + predicate=rdflib.term.URIRef( + "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest" + ), + subject=rest, + ) + ) return result def _extract_objects_out_of_layered_combination( - self, graph: rdflib.Graph, node: rdflib.term.BNode, - accept_and: bool, accept_or: bool) -> List[rdflib.term.URIRef]: + self, + graph: rdflib.Graph, + node: rdflib.term.BNode, + accept_and: bool, + accept_or: bool, + ) -> List[rdflib.term.URIRef]: """Extract all nodes out of a complex combination Args: @@ -818,7 +931,9 @@ def _extract_objects_out_of_layered_combination( if isinstance(node, rdflib.term.URIRef): result.append(node) else: - queue.extend(self._extract_objects_out_of_single_combination - (graph, node, accept_and, accept_or)) + queue.extend( + self._extract_objects_out_of_single_combination( + graph, node, accept_and, accept_or + ) + ) return result - diff --git a/filip/semantics/ontology_parser/vocabulary_builder.py b/filip/semantics/ontology_parser/vocabulary_builder.py index 67dfb8f8..a070578b 100644 --- a/filip/semantics/ontology_parser/vocabulary_builder.py +++ b/filip/semantics/ontology_parser/vocabulary_builder.py @@ -1,5 +1,6 @@ """Wrapper module to provide manipulation functions for vocabulary that should later be hidden from the user""" + import uuid from enum import Enum @@ -12,28 +13,27 @@ class IdType(str, Enum): - class_ = 'Class' - object_property = 'Object Property' - data_property = 'Data Property' - datatype = 'Datatype' - relation = 'Relation' - combined_relation = 'Combined Relation' - individual = 'Individual' - source = 'Source' + class_ = "Class" + object_property = "Object Property" + data_property = "Data Property" + datatype = "Datatype" + relation = "Relation" + combined_relation = "Combined Relation" + individual = "Individual" + source = "Source" class VocabularyBuilder(BaseModel): """Wrapper class to provide manipulation functions for vocabulary that should later be hidden from the user""" - vocabulary: Vocabulary = Field( - description="Vocabulary to manipulate" - ) + vocabulary: Vocabulary = Field(description="Vocabulary to manipulate") current_source: Source = Field( default=None, description="Current source to which entities are added," - "needed while parsing") + "needed while parsing", + ) def clear(self): """Clear all objects form the vocabulary @@ -62,9 +62,7 @@ def add_class(self, class_: Class): Returns: None """ - self._add_and_merge_entity(class_, - self.vocabulary.classes, - IdType.class_) + self._add_and_merge_entity(class_, self.vocabulary.classes, IdType.class_) def add_object_property(self, obj_prop: ObjectProperty): """Add an ObjectProperty to the vocabulary @@ -76,7 +74,8 @@ def add_object_property(self, obj_prop: ObjectProperty): None """ self._add_and_merge_entity( - obj_prop, self.vocabulary.object_properties, IdType.object_property) + obj_prop, self.vocabulary.object_properties, IdType.object_property + ) def add_data_property(self, data_prop: DataProperty): """Add an DataProperty to the vocabulary @@ -88,7 +87,8 @@ def add_data_property(self, data_prop: DataProperty): None """ self._add_and_merge_entity( - data_prop, self.vocabulary.data_properties, IdType.data_property) + data_prop, self.vocabulary.data_properties, IdType.data_property + ) def add_datatype(self, datatype: Datatype): """Add a DataType to the vocabulary @@ -99,8 +99,7 @@ def add_datatype(self, datatype: Datatype): Returns: None """ - self._add_and_merge_entity( - datatype, self.vocabulary.datatypes, IdType.datatype) + self._add_and_merge_entity(datatype, self.vocabulary.datatypes, IdType.datatype) def add_predefined_datatype(self, datatype: Datatype): """Add a DataType to the vocabulary, that belongs to the source: @@ -126,9 +125,9 @@ def add_individual(self, individual: Individual): Returns: None """ - self._add_and_merge_entity(individual, - self.vocabulary.individuals, - IdType.individual) + self._add_and_merge_entity( + individual, self.vocabulary.individuals, IdType.individual + ) def add_relation_for_class(self, class_iri: str, rel: Relation): """Add a relation object to a class @@ -152,8 +151,9 @@ def add_relation_for_class(self, class_iri: str, rel: Relation): class_.relation_ids.append(rel.id) self.vocabulary.id_types[rel.id] = IdType.relation - def add_combined_object_relation_for_class(self, class_iri: str, - crel: CombinedObjectRelation): + def add_combined_object_relation_for_class( + self, class_iri: str, crel: CombinedObjectRelation + ): """Add a combined object relation object to a class Args: @@ -164,12 +164,14 @@ def add_combined_object_relation_for_class(self, class_iri: str, None """ self.vocabulary.combined_object_relations[crel.id] = crel - self.vocabulary.get_class_by_iri(class_iri).\ - combined_object_relation_ids.append(crel.id) + self.vocabulary.get_class_by_iri(class_iri).combined_object_relation_ids.append( + crel.id + ) self.vocabulary.id_types[crel.id] = IdType.combined_relation - def add_combined_data_relation_for_class(self, class_iri: str, - cdata: CombinedDataRelation): + def add_combined_data_relation_for_class( + self, class_iri: str, cdata: CombinedDataRelation + ): """Add a combined data relation object to a class Args: @@ -180,8 +182,9 @@ def add_combined_data_relation_for_class(self, class_iri: str, None """ self.vocabulary.combined_data_relations[cdata.id] = cdata - self.vocabulary.get_class_by_iri(class_iri).\ - combined_data_relation_ids.append(cdata.id) + self.vocabulary.get_class_by_iri(class_iri).combined_data_relation_ids.append( + cdata.id + ) self.vocabulary.id_types[cdata.id] = IdType.combined_relation def add_source(self, source: Source, id: str = None): @@ -214,10 +217,9 @@ def set_current_source(self, source_id: str): assert source_id in self.vocabulary.sources self.current_source = self.vocabulary.sources[source_id] - def _add_and_merge_entity(self, - entity: Entity, - entity_dict: Dict[str, Entity], - id_type: IdType): + def _add_and_merge_entity( + self, entity: Entity, entity_dict: Dict[str, Entity], id_type: IdType + ): """Adds an entity to the vocabulary. If an entity with teh same iri already exists the label and comment are "merged" and both sources are noted @@ -237,12 +239,15 @@ def _add_and_merge_entity(self, if entity.iri in self.vocabulary.id_types: if not id_type == self.vocabulary.id_types[entity.iri]: self.current_source.add_parsing_log_entry( - LogLevel.CRITICAL, id_type, entity.iri, + LogLevel.CRITICAL, + id_type, + entity.iri, f"{entity.iri} from source " f"{self.current_source.get_name()} " f"exists multiple times in different catagories. It was " f"only added for the category " - f"{self.vocabulary.id_types[entity.iri].value}") + f"{self.vocabulary.id_types[entity.iri].value}", + ) return old_entity = entity_dict[entity.iri] @@ -258,15 +263,17 @@ def select_from(old: str, new: str, property: str) -> str: return "" else: self.current_source.add_parsing_log_entry( - LogLevel.WARNING, id_type, entity.iri, + LogLevel.WARNING, + id_type, + entity.iri, f"{property} from source " f"{old_entity.get_source_names(self.vocabulary)} " - f"was overwritten") + f"was overwritten", + ) return new entity.label = select_from(old_entity.label, entity.label, "label") - entity.comment = select_from(old_entity.comment, entity.comment, - "comment") + entity.comment = select_from(old_entity.comment, entity.comment, "comment") self.vocabulary.id_types[entity.iri] = id_type entity.source_ids.add(self.current_source.id) diff --git a/filip/semantics/semantics_manager.py b/filip/semantics/semantics_manager.py index c31e6fa2..b20808c5 100644 --- a/filip/semantics/semantics_manager.py +++ b/filip/semantics/semantics_manager.py @@ -18,14 +18,24 @@ from filip.models.ngsi_v2.context import ContextEntity from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient from filip.models import FiwareHeader -from filip.semantics.semantics_models import \ - InstanceIdentifier, SemanticClass, InstanceHeader, Datatype, DataField, \ - RelationField, SemanticIndividual, SemanticDeviceClass, CommandField, \ - Command, DeviceAttributeField, DeviceAttribute +from filip.semantics.semantics_models import ( + InstanceIdentifier, + SemanticClass, + InstanceHeader, + Datatype, + DataField, + RelationField, + SemanticIndividual, + SemanticDeviceClass, + CommandField, + Command, + DeviceAttributeField, + DeviceAttribute, +) from filip.utils.simple_ql import QueryString -logger = logging.getLogger('semantics') +logger = logging.getLogger("semantics") class InstanceRegistry(BaseModel): @@ -34,14 +44,15 @@ class InstanceRegistry(BaseModel): The instance registry is a global object, that is directly inject in the SemanticClass constructor over the SemanticsManager """ - _registry: Dict[InstanceIdentifier, 'SemanticClass'] = {} + + _registry: Dict[InstanceIdentifier, "SemanticClass"] = {} """ Dict of the references to the local SemanticClass instances. Instances are saved with their identifier as key """ _deleted_identifiers: List[InstanceIdentifier] = [] """List of all identifiers that were deleted""" - def delete(self, instance: 'SemanticClass'): + def delete(self, instance: "SemanticClass"): """Delete an instance from the registry Args: @@ -55,8 +66,7 @@ def delete(self, instance: 'SemanticClass'): """ identifier = instance.get_identifier() if not self.contains(identifier): - raise KeyError(f"Identifier {identifier} unknown, " - f"can not delete") + raise KeyError(f"Identifier {identifier} unknown, " f"can not delete") # If instance was loaded from Fiware it has an old_state. # if that is the case, we need to note that we have deleted the instance @@ -79,7 +89,7 @@ def instance_was_deleted(self, identifier: InstanceIdentifier) -> bool: """ return identifier in self._deleted_identifiers - def register(self, instance: 'SemanticClass'): + def register(self, instance: "SemanticClass"): """ Register a new instance of a SemanticClass in the registry @@ -91,11 +101,11 @@ def register(self, instance: 'SemanticClass'): identifier = instance.get_identifier() if identifier in self._registry: - raise AttributeError('Instance already exists') + raise AttributeError("Instance already exists") else: self._registry[identifier] = instance - def get(self, identifier: InstanceIdentifier) -> 'SemanticClass': + def get(self, identifier: InstanceIdentifier) -> "SemanticClass": """Retrieve an registered instance with its identifier Args: @@ -115,7 +125,7 @@ def contains(self, identifier: InstanceIdentifier) -> bool: """ return identifier in self._registry - def get_all(self) -> List['SemanticClass']: + def get_all(self) -> List["SemanticClass"]: """Get all registered instances Returns: @@ -123,7 +133,7 @@ def get_all(self) -> List['SemanticClass']: """ return list(self._registry.values()) - def get_all_deleted_identifiers(self) -> List['InstanceIdentifier']: + def get_all_deleted_identifiers(self) -> List["InstanceIdentifier"]: """ Get all identifiers that were deleted by the user @@ -139,7 +149,7 @@ def save(self) -> str: Returns: str, json string of registry state """ - res = {'instances': [], 'deleted_identifiers': []} + res = {"instances": [], "deleted_identifiers": []} for identifier, instance in self._registry.items(): old_state = None @@ -148,12 +158,12 @@ def save(self) -> str: instance_dict = { "entity": instance.build_context_entity().model_dump_json(), "header": instance.header.model_dump_json(), - "old_state": old_state + "old_state": old_state, } - res['instances'].append(instance_dict) + res["instances"].append(instance_dict) for identifier in self._deleted_identifiers: - res['deleted_identifiers'].append(identifier.model_dump_json()) + res["deleted_identifiers"].append(identifier.model_dump_json()) return json.dumps(res, indent=4) @@ -162,7 +172,7 @@ def clear(self): self._registry.clear() self._deleted_identifiers.clear() - def load(self, json_string: str, semantic_manager: 'SemanticsManager'): + def load(self, json_string: str, semantic_manager: "SemanticsManager"): """ Load the state of the registry out of a json string. The current state will be discarded @@ -177,30 +187,32 @@ def load(self, json_string: str, semantic_manager: 'SemanticsManager'): self.clear() save = json.loads(json_string) - for instance_dict in save['instances']: - entity_json = instance_dict['entity'] - header = InstanceHeader.model_validate(instance_dict['header']) + for instance_dict in save["instances"]: + entity_json = instance_dict["entity"] + header = InstanceHeader.model_validate(instance_dict["header"]) context_entity = ContextEntity.model_validate(entity_json) instance = semantic_manager._context_entity_to_semantic_class( - context_entity, header) + context_entity, header + ) - if instance_dict['old_state'] is not None: - instance.old_state.state = \ - ContextEntity.model_validate(instance_dict['old_state']) + if instance_dict["old_state"] is not None: + instance.old_state.state = ContextEntity.model_validate( + instance_dict["old_state"] + ) self._registry[instance.get_identifier()] = instance - for identifier in save['deleted_identifiers']: + for identifier in save["deleted_identifiers"]: self._deleted_identifiers.append( - InstanceIdentifier.model_validate(identifier)) + InstanceIdentifier.model_validate(identifier) + ) def __hash__(self): values = (hash(value) for value in self._registry.values()) - return hash((frozenset(values), - frozenset(self._deleted_identifiers))) + return hash((frozenset(values), frozenset(self._deleted_identifiers))) class SemanticsManager(BaseModel): @@ -215,28 +227,24 @@ class SemanticsManager(BaseModel): description="Registry managing the local state" ) class_catalogue: Dict[str, Type[SemanticClass]] = Field( - default={}, - description="Register of class names to classes" + default={}, description="Register of class names to classes" ) datatype_catalogue: Dict[str, Dict[str, str]] = Field( default={}, - description="Register of datatype names to Dict representation of " - "datatypes" + description="Register of datatype names to Dict representation of " "datatypes", ) individual_catalogue: Dict[str, type] = Field( - default={}, - description="Register of individual names to their classes" + default={}, description="Register of individual names to their classes" ) default_header: InstanceHeader = Field( default=InstanceHeader(), description="Default header that each new instance receives if it " - "does not specify an own header" + "does not specify an own header", ) @staticmethod - def get_client(instance_header: InstanceHeader) \ - -> ContextBrokerClient: + def get_client(instance_header: InstanceHeader) -> ContextBrokerClient: """Get the correct ContextBrokerClient to be used with the given header Args: @@ -247,7 +255,8 @@ def get_client(instance_header: InstanceHeader) \ if instance_header.ngsi_version == NgsiVersion.v2: return ContextBrokerClient( url=instance_header.cb_url, - fiware_header=instance_header.get_fiware_header()) + fiware_header=instance_header.get_fiware_header(), + ) else: # todo LD raise Exception("FiwareVersion not yet supported") @@ -264,16 +273,15 @@ def get_iota_client(instance_header: InstanceHeader) -> IoTAClient: if instance_header.ngsi_version == NgsiVersion.v2: return IoTAClient( url=instance_header.iota_url, - fiware_header=instance_header.get_fiware_header()) + fiware_header=instance_header.get_fiware_header(), + ) else: # todo LD raise Exception("FiwareVersion not yet supported") def _context_entity_to_semantic_class( - self, - entity: ContextEntity, - header: InstanceHeader) -> SemanticClass: - + self, entity: ContextEntity, header: InstanceHeader + ) -> SemanticClass: """Converts a ContextEntity to a SemanticClass Args: @@ -290,13 +298,13 @@ def _context_entity_to_semantic_class( if not self.is_class_name_an_device_class(class_name): - loaded_class: SemanticClass = class_(id=entity.id, - header=header, - enforce_new=True) + loaded_class: SemanticClass = class_( + id=entity.id, header=header, enforce_new=True + ) else: - loaded_class: SemanticDeviceClass = class_(id=entity.id, - header=header, - enforce_new=True) + loaded_class: SemanticDeviceClass = class_( + id=entity.id, header=header, enforce_new=True + ) loaded_class.old_state.state = entity @@ -311,7 +319,8 @@ def _context_entity_to_semantic_class( f"in Fiware misses a field that " f"is required by the class_model: {field_name}. The " f"fiware state and the used vocabulary models are not " - f"compatible") + f"compatible" + ) entity_field_value = entity.get_attribute(field_name).value @@ -321,8 +330,7 @@ def _context_entity_to_semantic_class( values = [entity_field_value] for value in values: - converted_value = self._convert_value_fitting_for_field( - field, value) + converted_value = self._convert_value_fitting_for_field(field, value) if isinstance(field, RelationField): # we need to bypass the main setter, as it expects an # instance and we do not want to load the instance if it @@ -338,13 +346,16 @@ def _context_entity_to_semantic_class( for identifier_str, prop_list in references.items(): for prop in prop_list: loaded_class.add_reference( - InstanceIdentifier.model_validate_json(identifier_str.replace( - "---", ".")), prop) + InstanceIdentifier.model_validate_json( + identifier_str.replace("---", ".") + ), + prop, + ) # load metadata metadata_dict = entity.get_attribute("metadata").value - loaded_class.metadata.name = metadata_dict['name'] - loaded_class.metadata.comment = metadata_dict['comment'] + loaded_class.metadata.name = metadata_dict["name"] + loaded_class.metadata.comment = metadata_dict["comment"] # load device_settings into instance, if instance is a device if isinstance(loaded_class, SemanticDeviceClass): @@ -381,7 +392,8 @@ def _convert_value_fitting_for_field(field, value): # we need to replace back --- with . that we switched, # as a . is not allowed in the dic in Fiware return InstanceIdentifier.model_validate_json( - str(value).replace("---", ".").replace("'", '"')) + str(value).replace("---", ".").replace("'", '"') + ) elif isinstance(field, CommandField): if isinstance(value, Command): @@ -391,7 +403,7 @@ def _convert_value_fitting_for_field(field, value): if not isinstance(value, dict): value = json.loads(value.replace("'", '"')) - return Command(name=value['name']) + return Command(name=value["name"]) elif isinstance(field, DeviceAttributeField): # if loading local state, the wrong string delimters are used, @@ -403,9 +415,7 @@ def _convert_value_fitting_for_field(field, value): value = json.loads(value.replace("'", '"')) return DeviceAttribute( - name=value['name'], - attribute_type=value[ - "attribute_type"] + name=value["name"], attribute_type=value["attribute_type"] ) def get_class_by_name(self, class_name: str) -> Type[SemanticClass]: @@ -455,8 +465,8 @@ def is_local_state_valid(self, validate_rules: bool = True) -> (bool, str): return ( False, f"SemanticEntity {instance.id} of type" - f"{instance.get_type()} has unfulfilled fields " - f"{[f.name for f in instance.get_invalid_rule_fields()]}." + f"{instance.get_type()} has unfulfilled fields " + f"{[f.name for f in instance.get_invalid_rule_fields()]}.", ) for instance in self.instance_registry.get_all(): @@ -464,8 +474,8 @@ def is_local_state_valid(self, validate_rules: bool = True) -> (bool, str): if instance.device_settings.transport is None: return ( False, - f"Device {instance.id} of type {instance.get_type()} " - f"needs to be given an transport setting." + f"Device {instance.id} of type {instance.get_type()} " + f"needs to be given an transport setting.", ) return True, "State is valid" @@ -496,14 +506,14 @@ def save_state(self, assert_validity: bool = True): # clients client = self.get_client(instance_header=identifier.header) - iota_client = self.get_iota_client( - instance_header=identifier.header) + iota_client = self.get_iota_client(instance_header=identifier.header) try: client.delete_entity( entity_id=identifier.id, entity_type=identifier.type, delete_devices=True, - iota_client=iota_client) + iota_client=iota_client, + ) except requests.RequestException: raise @@ -520,15 +530,16 @@ def save_state(self, assert_validity: bool = True): # it is important that we patch the values else the # references field would reach an invalid state if we worked # in parallel on an instance - cb_client.patch_entity(instance.build_context_entity(), - instance.old_state.state) + cb_client.patch_entity( + instance.build_context_entity(), instance.old_state.state + ) else: - iota_client = self.get_iota_client( - instance_header=instance.header) + iota_client = self.get_iota_client(instance_header=instance.header) iota_client.patch_device( device=instance.build_context_device(), patch_entity=True, - cb_client=cb_client) + cb_client=cb_client, + ) iota_client.close() cb_client.close() # update old_state @@ -552,17 +563,20 @@ def load_instance(self, identifier: InstanceIdentifier) -> SemanticClass: else: client = self.get_client(identifier.header) - entity = client.get_entity(entity_id=identifier.id, - entity_type=identifier.type) + entity = client.get_entity( + entity_id=identifier.id, entity_type=identifier.type + ) client.close() - logger.info(f"Instance ({identifier.id}, {identifier.type}) " - f"loaded from Fiware({identifier.header.cb_url}" - f", {identifier.header.service}" - f"{identifier.header.service_path})") + logger.info( + f"Instance ({identifier.id}, {identifier.type}) " + f"loaded from Fiware({identifier.header.cb_url}" + f", {identifier.header.service}" + f"{identifier.header.service_path})" + ) return self._context_entity_to_semantic_class( - entity=entity, - header=identifier.header) + entity=entity, header=identifier.header + ) def does_instance_exists(self, identifier: InstanceIdentifier) -> bool: """ @@ -582,8 +596,9 @@ def does_instance_exists(self, identifier: InstanceIdentifier) -> bool: return False else: client = self.get_client(identifier.header) - return client.does_entity_exist(entity_id=identifier.id, - entity_type=identifier.type) + return client.does_entity_exist( + entity_id=identifier.id, entity_type=identifier.type + ) def was_instance_deleted(self, identifier: InstanceIdentifier) -> bool: """ @@ -619,11 +634,12 @@ def get_all_local_instances(self) -> List[SemanticClass]: """ return self.instance_registry.get_all() - def get_all_local_instances_of_class(self, - class_: Optional[type] = None, - class_name: Optional[str] = None, - get_subclasses: bool = True) \ - -> List[SemanticClass]: + def get_all_local_instances_of_class( + self, + class_: Optional[type] = None, + class_name: Optional[str] = None, + get_subclasses: bool = True, + ) -> List[SemanticClass]: """ Retrieve all instances of a SemanitcClass from Local Storage @@ -643,10 +659,8 @@ def get_all_local_instances_of_class(self, List[SemanticClass] """ - assert class_ is None or class_name is None, \ - "Only one parameter is allowed" - assert class_ is not None or class_name is not None, \ - "One parameter is required" + assert class_ is None or class_name is None, "Only one parameter is allowed" + assert class_ is not None or class_name is not None, "One parameter is required" if class_ is not None: class_name = class_.__name__ @@ -664,17 +678,17 @@ def get_all_local_instances_of_class(self, return res def load_instances_from_fiware( - self, - fiware_header: FiwareHeader, - fiware_version: NgsiVersion, - cb_url: str, - iota_url: str, - entity_ids: Optional[List[str]] = None, - entity_types: Optional[List[str]] = None, - id_pattern: str = None, - type_pattern: str = None, - q: Union[str, QueryString] = None, - limit: int = inf, + self, + fiware_header: FiwareHeader, + fiware_version: NgsiVersion, + cb_url: str, + iota_url: str, + entity_ids: Optional[List[str]] = None, + entity_types: Optional[List[str]] = None, + id_pattern: str = None, + type_pattern: str = None, + q: Union[str, QueryString] = None, + limit: int = inf, ) -> List[SemanticClass]: """ Loads the instances of given types or ids from Fiware into the local @@ -713,24 +727,26 @@ def load_instances_from_fiware( service_path=fiware_header.service_path, cb_url=cb_url, iota_url=iota_url, - ngsi_version=fiware_version + ngsi_version=fiware_version, ) client = self.get_client(header) - entities = client.get_entity_list(entity_ids=entity_ids, - entity_types=entity_types, - id_pattern=id_pattern, - type_pattern=type_pattern, - q=q, - limit=limit) + entities = client.get_entity_list( + entity_ids=entity_ids, + entity_types=entity_types, + id_pattern=id_pattern, + type_pattern=type_pattern, + q=q, + limit=limit, + ) client.close() - return [self._context_entity_to_semantic_class(e, header) - for e in entities] + return [self._context_entity_to_semantic_class(e, header) for e in entities] - def get_entity_from_fiware(self, instance_identifier: InstanceIdentifier) \ - -> ContextEntity: + def get_entity_from_fiware( + self, instance_identifier: InstanceIdentifier + ) -> ContextEntity: """ Retrieve the current entry of an instance in Fiware @@ -745,12 +761,13 @@ def get_entity_from_fiware(self, instance_identifier: InstanceIdentifier) \ """ client = self.get_client(instance_identifier.header) - return client.get_entity(entity_id=instance_identifier.id, - entity_type=instance_identifier.type) + return client.get_entity( + entity_id=instance_identifier.id, entity_type=instance_identifier.type + ) def load_instances( - self, - identifiers: List[InstanceIdentifier]) -> List[SemanticClass]: + self, identifiers: List[InstanceIdentifier] + ) -> List[SemanticClass]: """ Load all instances, if no local state of it exists it will get taken from Fiware and registered locally @@ -833,10 +850,7 @@ def load_local_state_from_json(self, json: str): """ self.instance_registry.load(json, self) - def visualize_local_state( - self, - display_individuals_rule: str = "ALL" - ): + def visualize_local_state(self, display_individuals_rule: str = "ALL"): """ Visualise all instances in the local state in a network graph that shows which instances reference each other over which fields @@ -858,19 +872,24 @@ def visualize_local_state( ValueError: if display_individuals_rule is invalid """ - if not display_individuals_rule == "ALL" and \ - not display_individuals_rule == "NONE" and \ - not display_individuals_rule == "USED": + if ( + not display_individuals_rule == "ALL" + and not display_individuals_rule == "NONE" + and not display_individuals_rule == "USED" + ): raise ValueError(f"Invalid parameter {display_individuals_rule}") import igraph + g = igraph.Graph(directed=True) for instance in self.get_all_local_instances(): - g.add_vertex(name=instance.id, - label=f"\n\n\n {instance.get_type()} \n {instance.id}", - color="green") + g.add_vertex( + name=instance.id, + label=f"\n\n\n {instance.get_type()} \n {instance.id}", + color="green", + ) used_individuals_names: Set[str] = set() for instance in self.get_all_local_instances(): @@ -887,26 +906,30 @@ def visualize_local_state( if display_individuals_rule == "ALL": used_individuals_names.update(self.individual_catalogue.keys()) - for individual in [self.get_individual(name) for name in - used_individuals_names]: - g.add_vertex(label=f"\n\n\n{individual.get_name()}", - name=individual.get_name(), - color="blue") + for individual in [ + self.get_individual(name) for name in used_individuals_names + ]: + g.add_vertex( + label=f"\n\n\n{individual.get_name()}", + name=individual.get_name(), + color="blue", + ) layout = g.layout("fr") - visual_style = {"vertex_size": 20, - "vertex_color": g.vs["color"], - "vertex_label": g.vs["label"], - "edge_label": g.es["name"], - "layout": layout, - "bbox": (len(g.vs) * 50, len(g.vs) * 50)} + visual_style = { + "vertex_size": 20, + "vertex_color": g.vs["color"], + "vertex_label": g.vs["label"], + "edge_label": g.es["name"], + "layout": layout, + "bbox": (len(g.vs) * 50, len(g.vs) * 50), + } igraph.plot(g, **visual_style) def generate_cytoscape_for_local_state( - self, - display_only_used_individuals: bool = True - ): + self, display_only_used_individuals: bool = True + ): """ Generate a graph definition that can be loaded into a cytoscape visualisation tool, that describes the complete current local state. @@ -928,52 +951,34 @@ def generate_cytoscape_for_local_state( # graph design stylesheet = [ + {"selector": "node", "style": {"label": "data(label)", "z-index": 9999}}, { - 'selector': 'node', - 'style': { - 'label': 'data(label)', - 'z-index': 9999 - } - }, - { - 'selector': 'edge', - 'style': { - 'curve-style': 'bezier', - 'target-arrow-color': 'black', - 'target-arrow-shape': 'triangle', - 'line-color': 'black', + "selector": "edge", + "style": { + "curve-style": "bezier", + "target-arrow-color": "black", + "target-arrow-shape": "triangle", + "line-color": "black", "opacity": 0.45, - 'z-index': 5000, - } + "z-index": 5000, + }, }, { - 'selector': '.center', - 'style': { - 'shape': 'rectangle', - 'background-color': 'black' - } + "selector": ".center", + "style": {"shape": "rectangle", "background-color": "black"}, }, { - 'selector': '.individual', - 'style': { - 'shape': 'circle', - 'background-color': 'orange' - } + "selector": ".individual", + "style": {"shape": "circle", "background-color": "orange"}, }, { - 'selector': '.instance', - 'style': { - 'shape': 'circle', - 'background-color': 'green' - } + "selector": ".instance", + "style": {"shape": "circle", "background-color": "green"}, }, { - 'selector': '.collection', - 'style': { - 'shape': 'triangle', - 'background-color': 'gray' - } - } + "selector": ".collection", + "style": {"shape": "triangle", "background-color": "gray"}, + }, ] nodes = [] @@ -1000,12 +1005,18 @@ def get_node_id(item: Union[SemanticClass, SemanticIndividual]) -> str: return item.get_identifier().model_dump_json() for instance in self.get_all_local_instances(): - label = f'({instance.get_type()}){instance.metadata.name}' - nodes.append({'data': {'id': get_node_id(instance), - 'label': label, - 'parent_id': '', - 'classes': "instance item"}, - 'classes': "instance item"}) + label = f"({instance.get_type()}){instance.metadata.name}" + nodes.append( + { + "data": { + "id": get_node_id(instance), + "label": label, + "parent_id": "", + "classes": "instance item", + }, + "classes": "instance item", + } + ) for instance in self.get_all_local_instances(): @@ -1020,46 +1031,78 @@ def get_node_id(item: Union[SemanticClass, SemanticIndividual]) -> str: pass elif len(values) == 1: edge_id = uuid.uuid4().hex - edges.append({'data': {'id': edge_id, - 'source': get_node_id(instance), - 'target': get_node_id(values[0])}}) + edges.append( + { + "data": { + "id": edge_id, + "source": get_node_id(instance), + "target": get_node_id(values[0]), + } + } + ) edge_name = rel_field.name - stylesheet.append({'selector': '#' + edge_id, - 'style': {'label': edge_name}}) + stylesheet.append( + {"selector": "#" + edge_id, "style": {"label": edge_name}} + ) else: edge_id = uuid.uuid4().hex node_id = uuid.uuid4().hex - nodes.append({'data': {'id': node_id, - 'label': '', - 'parent_id': '', - 'classes': "collection"}, - 'classes': "collection"}) - - edges.append({'data': {'id': edge_id, - 'source': get_node_id(instance), - 'target': node_id}}) + nodes.append( + { + "data": { + "id": node_id, + "label": "", + "parent_id": "", + "classes": "collection", + }, + "classes": "collection", + } + ) + + edges.append( + { + "data": { + "id": edge_id, + "source": get_node_id(instance), + "target": node_id, + } + } + ) edge_name = rel_field.name - stylesheet.append({'selector': '#' + edge_id, - 'style': {'label': edge_name}}) + stylesheet.append( + {"selector": "#" + edge_id, "style": {"label": edge_name}} + ) for value in values: edge_id = uuid.uuid4().hex - edges.append({'data': {'id': edge_id, - 'source': node_id, - 'target': get_node_id(value)}}) + edges.append( + { + "data": { + "id": edge_id, + "source": node_id, + "target": get_node_id(value), + } + } + ) for individual_name in used_individual_names: - nodes.append({'data': {'id': individual_name, - 'label': individual_name, 'parent_id': '', - 'classes': "individual item"}, - 'classes': "individual item"}) + nodes.append( + { + "data": { + "id": individual_name, + "label": individual_name, + "parent_id": "", + "classes": "individual item", + }, + "classes": "individual item", + } + ) - elements = {'nodes': nodes, 'edges': edges} + elements = {"nodes": nodes, "edges": edges} return elements, stylesheet - def merge_local_and_live_instance_state(self, instance: SemanticClass) ->\ - None: + def merge_local_and_live_instance_state(self, instance: SemanticClass) -> None: """ The live state of the instance is fetched from Fiware (if it exists) and the two states are merged: @@ -1087,12 +1130,14 @@ def merge_local_and_live_instance_state(self, instance: SemanticClass) ->\ """ def converted_attribute_values(field, attribute) -> Set: - return {self._convert_value_fitting_for_field(field, value) for - value in attribute.value} + return { + self._convert_value_fitting_for_field(field, value) + for value in attribute.value + } def _get_added_and_removed_values( - old_values: Union[List, Set, Any], - current_values: Union[List, Set, Any]) -> (Set, Set): + old_values: Union[List, Set, Any], current_values: Union[List, Set, Any] + ) -> (Set, Set): old_set = set(old_values) current_set = set(current_values) @@ -1114,13 +1159,15 @@ def _get_added_and_removed_values( # instance is new. Save it as is client = self.get_client(instance.header) - if not client.does_entity_exist(entity_id=instance.id, - entity_type=instance.get_type()): + if not client.does_entity_exist( + entity_id=instance.id, entity_type=instance.get_type() + ): return client = self.get_client(instance.header) - live_entity = client.get_entity(entity_id=instance.id, - entity_type=instance.get_type()) + live_entity = client.get_entity( + entity_id=instance.id, entity_type=instance.get_type() + ) client.close() current_entity = instance.build_context_entity() @@ -1133,18 +1180,21 @@ def _get_added_and_removed_values( for field in instance.get_fields(): # live_values = set(live_entity.get_attribute(field.name).value) live_values = converted_attribute_values( - field, live_entity.get_attribute(field.name)) + field, live_entity.get_attribute(field.name) + ) old_values = converted_attribute_values( - field, old_entity.get_attribute(field.name)) + field, old_entity.get_attribute(field.name) + ) current_values = converted_attribute_values( - field, current_entity.get_attribute(field.name)) + field, current_entity.get_attribute(field.name) + ) - (added_values, deleted_values) = \ - _get_added_and_removed_values( - old_values, current_values - # old_entity.get_attribute(field.name).value, - # current_entity.get_attribute(field.name).value - ) + (added_values, deleted_values) = _get_added_and_removed_values( + old_values, + current_values, + # old_entity.get_attribute(field.name).value, + # current_entity.get_attribute(field.name).value + ) for value in added_values: live_values.add(value) @@ -1155,19 +1205,15 @@ def _get_added_and_removed_values( new_values = list(live_values) # update local stated with merged result field._set.clear() # very important to not use field.clear, - # as that methode would also delete references + # as that methode would also delete references for value in new_values: - converted_value = self._convert_value_fitting_for_field( - field, value) + converted_value = self._convert_value_fitting_for_field(field, value) field._set.add(converted_value) # ------merge references----------------------------------------------- - merged_references: Dict = live_entity.get_attribute( - "referencedBy").value - current_references: Dict = current_entity.get_attribute( - "referencedBy").value - old_references: Dict = old_entity.get_attribute( - "referencedBy").value + merged_references: Dict = live_entity.get_attribute("referencedBy").value + current_references: Dict = current_entity.get_attribute("referencedBy").value + old_references: Dict = old_entity.get_attribute("referencedBy").value keys = set(current_references.keys()) keys.update(old_references.keys()) @@ -1181,7 +1227,8 @@ def _get_added_and_removed_values( old_values = old_references[key] (added_values, deleted_values) = _get_added_and_removed_values( - current_values=current_values, old_values=old_values) + current_values=current_values, old_values=old_values + ) # ensure the merged state has each key if key not in merged_references.keys(): @@ -1209,14 +1256,14 @@ def _get_added_and_removed_values( instance.references.clear() for key, value in merged_references.items(): # replace back the protected . (. not allowed in keys in fiware) - instance.references[InstanceIdentifier.model_validate_json(key.replace( - "---", "."))] = value + instance.references[ + InstanceIdentifier.model_validate_json(key.replace("---", ".")) + ] = value # ------merge device settings---------------------------------------- if isinstance(instance, SemanticDeviceClass): old_settings = old_entity.get_attribute("deviceSettings").value - current_settings = \ - current_entity.get_attribute("deviceSettings").value + current_settings = current_entity.get_attribute("deviceSettings").value new_settings = live_entity.get_attribute("deviceSettings").value # keys are always the same @@ -1242,10 +1289,14 @@ def find_fitting_model(self, search_term: str, limit: int = 5) -> List[str]: List[str], containing 0 to [limit] ordered propositions (best first) """ class_names = list(self.class_catalogue.keys()) - suggestions = [item[0] for item in process.extract( - query=search_term.casefold(), - choices=class_names, - score_cutoff=50, - limit=limit)] + suggestions = [ + item[0] + for item in process.extract( + query=search_term.casefold(), + choices=class_names, + score_cutoff=50, + limit=limit, + ) + ] return suggestions diff --git a/filip/semantics/semantics_models.py b/filip/semantics/semantics_models.py index 473299dd..2f9d3399 100644 --- a/filip/semantics/semantics_models.py +++ b/filip/semantics/semantics_models.py @@ -5,22 +5,38 @@ import pydantic as pyd import requests from aenum import Enum -from typing import List, Tuple, Dict, Type, TYPE_CHECKING, Optional, Union, \ - Set, Iterator, Any +from typing import ( + List, + Tuple, + Dict, + Type, + TYPE_CHECKING, + Optional, + Union, + Set, + Iterator, + Any, +) import filip.models.ngsi_v2.iot as iot + # from filip.models.ngsi_v2.iot import ExpressionLanguage, TransportProtocol from filip.models.base import DataType, NgsiVersion from filip.utils.validators import FiwareRegex -from filip.models.ngsi_v2.context import ContextEntity, NamedContextAttribute, \ - NamedCommand +from filip.models.ngsi_v2.context import ( + ContextEntity, + NamedContextAttribute, + NamedCommand, +) from filip.models import FiwareHeader from pydantic import ConfigDict, BaseModel, Field from filip.config import settings from filip.semantics.vocabulary.entities import DatatypeFields, DatatypeType -from filip.semantics.vocabulary_configurator import label_blacklist, \ - label_char_whitelist +from filip.semantics.vocabulary_configurator import ( + label_blacklist, + label_char_whitelist, +) if TYPE_CHECKING: from filip.semantics.semantics_manager import SemanticsManager @@ -33,24 +49,26 @@ class InstanceHeader(FiwareHeader): The header is not bound to one Fiware Setup, but can describe the exact location in the web """ + model_config = ConfigDict(frozen=True, use_enum_values=True) - cb_url: str = Field(default=settings.CB_URL, - description="Url of the ContextBroker from the Fiware " - "setup") - iota_url: str = Field(default=settings.IOTA_URL, - description="Url of the IoTABroker from the Fiware " - "setup") + cb_url: str = Field( + default=settings.CB_URL, + description="Url of the ContextBroker from the Fiware " "setup", + ) + iota_url: str = Field( + default=settings.IOTA_URL, + description="Url of the IoTABroker from the Fiware " "setup", + ) - ngsi_version: NgsiVersion = Field(default=NgsiVersion.v2, - description="Used Version in the " - "Fiware setup") + ngsi_version: NgsiVersion = Field( + default=NgsiVersion.v2, description="Used Version in the " "Fiware setup" + ) def get_fiware_header(self) -> FiwareHeader: """ Get a Filip FiwareHeader from the InstanceHeader """ - return FiwareHeader(service=self.service, - service_path=self.service_path) + return FiwareHeader(service=self.service, service_path=self.service_path) class InstanceIdentifier(BaseModel): @@ -58,13 +76,15 @@ class InstanceIdentifier(BaseModel): Each Instance of a SemanticClass posses a unique identifier that is directly linked to one Fiware entry """ + model_config = ConfigDict(frozen=True) id: str = Field(description="Id of the entry in Fiware") - type: str = Field(description="Type of the entry in Fiware, equal to " - "class_name") - header: InstanceHeader = Field(description="describes the Fiware " - "Location were the instance " - "will be / is saved.") + type: str = Field(description="Type of the entry in Fiware, equal to " "class_name") + header: InstanceHeader = Field( + description="describes the Fiware " + "Location were the instance " + "will be / is saved." + ) class Datatype(DatatypeFields): @@ -121,6 +141,7 @@ def value_is_valid(self, value: str) -> bool: if self.type == "date": try: from dateutil.parser import parse + parse(value, fuzzy=False) return True @@ -138,16 +159,18 @@ class DevicePropertyInstanceLink(BaseModel): Modeled as a standalone model, to bypass the read-only logic of DeviceProperty """ + instance_identifier: Optional[InstanceIdentifier] = Field( - default=None, - description="Identifier of the instance holding this Property") - semantic_manager: Optional['SemanticsManager'] = Field( - default=None, - description="Link to the governing semantic_manager") + default=None, description="Identifier of the instance holding this Property" + ) + semantic_manager: Optional["SemanticsManager"] = Field( + default=None, description="Link to the governing semantic_manager" + ) field_name: Optional[str] = Field( default=None, description="Name of the field to which this property was added " - "in the instance") + "in the instance", + ) class DeviceProperty(BaseModel): @@ -158,6 +181,7 @@ class DeviceProperty(BaseModel): A property can only belong to one field of one instance. Assigning it to multiple fields will result in an error. """ + model_config = ConfigDict() name: str = Field("Internally used name in the IoT Device") @@ -165,14 +189,16 @@ class DeviceProperty(BaseModel): """Additional properties describing the instance and field where this \ property was added""" - def _get_instance(self) -> 'SemanticClass': + def _get_instance(self) -> "SemanticClass": """Get the instance object to which this property was added""" return self._instance_link.semantic_manager.get_instance( - self._instance_link.instance_identifier) + self._instance_link.instance_identifier + ) - def _get_field_from_fiware(self, field_name: str, required_type: str) \ - -> NamedContextAttribute: + def _get_field_from_fiware( + self, field_name: str, required_type: str + ) -> NamedContextAttribute: """ Retrieves live information about a field from the assigned instance from Fiware @@ -188,33 +214,40 @@ def _get_field_from_fiware(self, field_name: str, required_type: str) \ """ if self._instance_link.field_name is None: - raise Exception("This DeviceProperty needs to be added to a " - "device field of an SemanticDeviceClass instance " - "and the state saved before this methode can be " - "executed") + raise Exception( + "This DeviceProperty needs to be added to a " + "device field of an SemanticDeviceClass instance " + "and the state saved before this methode can be " + "executed" + ) try: - entity = self._instance_link.semantic_manager. \ - get_entity_from_fiware( - instance_identifier=self._instance_link.instance_identifier) + entity = self._instance_link.semantic_manager.get_entity_from_fiware( + instance_identifier=self._instance_link.instance_identifier + ) except requests.RequestException: - raise Exception("The instance to which this property belongs is " - "not yet present in Fiware, you need to save the " - "state first") + raise Exception( + "The instance to which this property belongs is " + "not yet present in Fiware, you need to save the " + "state first" + ) try: attr = entity.get_attribute(field_name) except requests.RequestException: - raise Exception("This property was not yet saved in Fiware. " - "You need to save the state first before this " - "methode can be executed") + raise Exception( + "This property was not yet saved in Fiware. " + "You need to save the state first before this " + "methode can be executed" + ) if not attr.type == required_type: - raise Exception("The field in Fiware has a wrong type, " - "an uncaught naming conflict happened") + raise Exception( + "The field in Fiware has a wrong type, " + "an uncaught naming conflict happened" + ) return attr - def get_all_field_names(self, field_name: Optional[str] = None) \ - -> List[str]: + def get_all_field_names(self, field_name: Optional[str] = None) -> List[str]: """ Get all field names which this property creates in the fiware instance @@ -239,6 +272,7 @@ class Command(DeviceProperty): A command can only belong to one field of one instance. Assigning it to multiple fields will result in an error. """ + model_config = ConfigDict(frozen=True) def send(self): @@ -249,13 +283,16 @@ def send(self): Exception: If the command was not yet saved to Fiware """ client = self._instance_link.semantic_manager.get_client( - self._instance_link.instance_identifier.header) + self._instance_link.instance_identifier.header + ) context_command = NamedCommand(name=self.name, value="") identifier = self._instance_link.instance_identifier - client.post_command(entity_id=identifier.id, - entity_type=identifier.type, - command=context_command) + client.post_command( + entity_id=identifier.id, + entity_type=identifier.type, + command=context_command, + ) client.close() def get_info(self) -> str: @@ -265,8 +302,9 @@ def get_info(self) -> str: Raises: Exception: If the command was not yet saved to Fiware """ - return self._get_field_from_fiware(field_name=f'{self.name}_info', - required_type="commandResult").value + return self._get_field_from_fiware( + field_name=f"{self.name}_info", required_type="commandResult" + ).value def get_status(self): """ @@ -275,11 +313,11 @@ def get_status(self): Raises: Exception: If the command was not yet saved to Fiware """ - return self._get_field_from_fiware(field_name=f'{self.name}_status', - required_type="commandStatus").value + return self._get_field_from_fiware( + field_name=f"{self.name}_status", required_type="commandStatus" + ).value - def get_all_field_names(self, field_name: Optional[str] = None) \ - -> List[str]: + def get_all_field_names(self, field_name: Optional[str] = None) -> List[str]: """ Get all the field names that this command will add to Fiware @@ -293,7 +331,8 @@ class DeviceAttributeType(str, Enum): """ Retrieval type of the DeviceAttribute value from the IoT Device into Fiware """ - _init_ = 'value __doc__' + + _init_ = "value __doc__" lazy = "lazy", "The value is only read out if it is requested" active = "active", "The value is kept up-to-date" @@ -310,10 +349,11 @@ class DeviceAttribute(DeviceProperty): A DeviceAttribute can only belong to one field of one instance. Assigning it to multiple fields will result in an error. """ + model_config = ConfigDict(frozen=True, use_enum_values=True) attribute_type: DeviceAttributeType = Field( description="States if the attribute is read actively or lazy from " - "the IoT Device into Fiware" + "the IoT Device into Fiware" ) def get_value(self): @@ -324,11 +364,11 @@ def get_value(self): Exception: If the DeviceAttribute was not yet saved to Fiware """ return self._get_field_from_fiware( - field_name=f'{self._instance_link.field_name}_{self.name}', - required_type="StructuredValue").value + field_name=f"{self._instance_link.field_name}_{self.name}", + required_type="StructuredValue", + ).value - def get_all_field_names(self, field_name: Optional[str] = None) \ - -> List[str]: + def get_all_field_names(self, field_name: Optional[str] = None) -> List[str]: """ Get all field names which this property creates in the fiware instance @@ -340,7 +380,7 @@ def get_all_field_names(self, field_name: Optional[str] = None) \ """ if field_name is None: field_name = self._instance_link.field_name - return [f'{field_name}_{self.name}'] + return [f"{field_name}_{self.name}"] class Field(BaseModel): @@ -354,14 +394,16 @@ class Field(BaseModel): The fields of a class are predefined. A field can contain standard values on init """ + model_config = ConfigDict() name: str = Field( default="", description="Name of the Field, corresponds to the property name that " - "it has in the SemanticClass") + "it has in the SemanticClass", + ) - _semantic_manager: 'SemanticsManager' + _semantic_manager: "SemanticsManager" "Reference to the global SemanticsManager" _instance_identifier: InstanceIdentifier @@ -369,7 +411,8 @@ class Field(BaseModel): _set: Set = Field( default=set(), - description="Internal set of the field, to which values are saved") + description="Internal set of the field, to which values are saved", + ) def __init__(self, name, semantic_manager): self._semantic_manager = semantic_manager @@ -393,10 +436,16 @@ def build_context_attribute(self) -> NamedContextAttribute: """ pass - def build_device_attributes(self) -> List[Union[iot.DeviceAttribute, - iot.LazyDeviceAttribute, - iot.StaticDeviceAttribute, - iot.DeviceCommand]]: + def build_device_attributes( + self, + ) -> List[ + Union[ + iot.DeviceAttribute, + iot.LazyDeviceAttribute, + iot.StaticDeviceAttribute, + iot.DeviceCommand, + ] + ]: """ Convert the field to a DeviceAttribute that can eb added to a DeviceEntity @@ -420,7 +469,7 @@ def build_device_attributes(self) -> List[Union[iot.DeviceAttribute, type=DataType.STRUCTUREDVALUE, value=values, entity_name=None, - entity_type=None + entity_type=None, ) ] @@ -508,10 +557,10 @@ def __str__(self): Returns: str """ - result = f'Field: {self.name},\n\tvalues: [' + result = f"Field: {self.name},\n\tvalues: [" values = self.get_all_raw() for value in values: - result += f'{value}, ' + result += f"{value}, " if len(values) > 0: result = result[:-2] return result @@ -541,7 +590,7 @@ def _convert_value(self, v): """ return v - def _get_instance(self) -> 'SemanticClass': + def _get_instance(self) -> "SemanticClass": """ Get the instance object to which this field belongs """ @@ -628,19 +677,24 @@ def name_check(self, v: _internal_type): taken_fields = self._get_instance().get_all_field_names() for name in v.get_all_field_names(field_name=self.name): if name in taken_fields: - raise NameError(f"The property can not be added to the field " - f"{self.name}, because the instance already" - f" posses a field with the name {name}") + raise NameError( + f"The property can not be added to the field " + f"{self.name}, because the instance already" + f" posses a field with the name {name}" + ) if name in label_blacklist: - raise NameError(f"The property can not be added to the field " - f"{self.name}, because the name {name} is " - f"forbidden") + raise NameError( + f"The property can not be added to the field " + f"{self.name}, because the name {name} is " + f"forbidden" + ) for c in name: if c not in label_char_whitelist: raise NameError( f"The property can not be added to the field " f"{self.name}, because the name {name} " - f"contains the forbidden character {c}") + f"contains the forbidden character {c}" + ) def remove(self, v): """List function: Remove a values @@ -669,8 +723,9 @@ def add(self, v): # assert that the given value fulfills certain conditions assert isinstance(v, self._internal_type) assert isinstance(v, DeviceProperty) - assert v._instance_link.instance_identifier is None, \ - "DeviceProperty can only belong to one device instance" + assert ( + v._instance_link.instance_identifier is None + ), "DeviceProperty can only belong to one device instance" # test if name of v is valid, if not an error is raised self.name_check(v) @@ -714,7 +769,7 @@ def build_context_attribute(self) -> NamedContextAttribute: class CommandField(DeviceField): """ - A Field that holds commands that can be send to the device + A Field that holds commands that can be send to the device """ _internal_type = Command @@ -728,10 +783,16 @@ def get_all(self) -> List[Command]: def __iter__(self) -> Iterator[Command]: return super().__iter__() - def build_device_attributes(self) -> List[Union[iot.DeviceAttribute, - iot.LazyDeviceAttribute, - iot.StaticDeviceAttribute, - iot.DeviceCommand]]: + def build_device_attributes( + self, + ) -> List[ + Union[ + iot.DeviceAttribute, + iot.LazyDeviceAttribute, + iot.StaticDeviceAttribute, + iot.DeviceCommand, + ] + ]: attrs = super().build_device_attributes() for command in self.get_all_raw(): attrs.append( @@ -744,9 +805,10 @@ def build_device_attributes(self) -> List[Union[iot.DeviceAttribute, class DeviceAttributeField(DeviceField): """ - A Field that holds attributes of the device that can be referenced for - live reading of the device + A Field that holds attributes of the device that can be referenced for + live reading of the device """ + _internal_type = DeviceAttribute def get_all_raw(self) -> Set[DeviceAttribute]: @@ -758,10 +820,16 @@ def get_all(self) -> List[DeviceAttribute]: def __iter__(self) -> Iterator[DeviceAttribute]: return super().__iter__() - def build_device_attributes(self) -> List[Union[iot.DeviceAttribute, - iot.LazyDeviceAttribute, - iot.StaticDeviceAttribute, - iot.DeviceCommand]]: + def build_device_attributes( + self, + ) -> List[ + Union[ + iot.DeviceAttribute, + iot.LazyDeviceAttribute, + iot.StaticDeviceAttribute, + iot.DeviceCommand, + ] + ]: attrs = super().build_device_attributes() for attribute in self.get_all_raw(): @@ -773,7 +841,7 @@ def build_device_attributes(self) -> List[Union[iot.DeviceAttribute, name=f"{self.name}_{attribute.name}", type=DataType.STRUCTUREDVALUE, entity_name=None, - entity_type=None + entity_type=None, ) ) else: @@ -783,7 +851,7 @@ def build_device_attributes(self) -> List[Union[iot.DeviceAttribute, name=f"{self.name}_{attribute.name}", type=DataType.STRUCTUREDVALUE, entity_name=None, - entity_type=None + entity_type=None, ) ) @@ -804,8 +872,8 @@ class RuleField(Field): _rules: List[Tuple[str, List[List[str]]]] """rule formatted for machine readability """ rule: str = pyd.Field( - default="", - description="rule formatted for human readability") + default="", description="rule formatted for human readability" + ) def __init__(self, rule, name, semantic_manager): self._semantic_manager = semantic_manager @@ -931,7 +999,7 @@ def __str__(self): str """ result = super(RuleField, self).__str__() - result += f'],\n\trule: ({self.rule})' + result += f"],\n\trule: ({self.rule})" return result def _get_all_rule_type_names(self) -> Set[str]: @@ -966,7 +1034,7 @@ def build_context_attribute(self) -> NamedContextAttribute: return NamedContextAttribute( name=self.name, type=DataType.STRUCTUREDVALUE, - value=[v for v in self.get_all_raw()] + value=[v for v in self.get_all_raw()], ) def add(self, v): @@ -976,7 +1044,7 @@ def add(self, v): self._set.add(v) def __str__(self): - return 'Data' + super().__str__() + return "Data" + super().__str__() def get_possible_enum_values(self) -> List[str]: """ @@ -1000,23 +1068,26 @@ def get_all_possible_datatypes(self) -> List[Datatype]: Returns: List[Datatype] """ - return [self._semantic_manager.get_datatype(type_name) - for type_name in self._get_all_rule_type_names()] + return [ + self._semantic_manager.get_datatype(type_name) + for type_name in self._get_all_rule_type_names() + ] class RelationField(RuleField): """ - Field for CombinedObjectRelation - A Field that contains links to other instances of SemanticClasses, - or Individuals - - Internally this field only holds: - - InstanceIdentifiers for SemanticClasses. If a value is accessed - the corresponding instance is loaded form the local registry - or hot loaded form Fiware - - Names for Individuals. If a value is accessed a new object of - that individual is returned (All instances are equal) + Field for CombinedObjectRelation + A Field that contains links to other instances of SemanticClasses, + or Individuals + + Internally this field only holds: + - InstanceIdentifiers for SemanticClasses. If a value is accessed + the corresponding instance is loaded form the local registry + or hot loaded form Fiware + - Names for Individuals. If a value is accessed a new object of + that individual is returned (All instances are equal) """ + _rules: List[Tuple[str, List[List[Type]]]] = [] inverse_of: List[str] = [] """List of all field names which are inverse to this field. @@ -1044,9 +1115,7 @@ def build_context_attribute(self) -> NamedContextAttribute: values.append(v) return NamedContextAttribute( - name=self.name, - type=DataType.RELATIONSHIP, - value=values + name=self.name, type=DataType.RELATIONSHIP, value=values ) def _convert_value(self, v): @@ -1059,8 +1128,8 @@ def _convert_value(self, v): elif isinstance(v, str): return self._semantic_manager.get_individual(v) - def add(self, v: Union['SemanticClass', 'SemanticIndividual']): - """ see class description + def add(self, v: Union["SemanticClass", "SemanticIndividual"]): + """see class description Raises: AttributeError: if value not an instance of 'SemanticClass' or 'SemanticIndividual' @@ -1075,11 +1144,13 @@ def add(self, v: Union['SemanticClass', 'SemanticIndividual']): elif isinstance(v, SemanticIndividual): self._set.add(v.get_name()) else: - raise AttributeError("Only instances of a SemanticClass or a " - "SemanticIndividual can be given as value") + raise AttributeError( + "Only instances of a SemanticClass or a " + "SemanticIndividual can be given as value" + ) def remove(self, v): - """ see class description""" + """see class description""" if isinstance(v, SemanticClass): identifier = v.get_identifier() @@ -1104,9 +1175,11 @@ def remove(self, v): elif isinstance(v, SemanticIndividual): self._set.remove(v.get_name()) else: - raise KeyError(f"v is neither of type SemanticIndividual nor SemanticClass but {type(v)}") + raise KeyError( + f"v is neither of type SemanticIndividual nor SemanticClass but {type(v)}" + ) - def _add_inverse(self, v: 'SemanticClass'): + def _add_inverse(self, v: "SemanticClass"): """ If a value is added to this field, and this field has an inverse logic field bound to it. @@ -1122,21 +1195,21 @@ def _add_inverse(self, v: 'SemanticClass'): field.add(self._get_instance()) def __str__(self): - """ see class description""" - return 'Relation' + super().__str__() + """see class description""" + return "Relation" + super().__str__() - def __iter__(self) -> \ - Iterator[Union['SemanticClass', 'SemanticIndividual']]: + def __iter__(self) -> Iterator[Union["SemanticClass", "SemanticIndividual"]]: return super().__iter__() - def get_all(self) -> List[Union['SemanticClass', 'SemanticIndividual']]: + def get_all(self) -> List[Union["SemanticClass", "SemanticIndividual"]]: return super(RelationField, self).get_all() def get_all_raw(self) -> Set[Union[InstanceIdentifier, str]]: return super().get_all_raw() - def get_all_possible_classes(self, include_subclasses: bool = False) -> \ - List[Type['SemanticClass']]: + def get_all_possible_classes( + self, include_subclasses: bool = False + ) -> List[Type["SemanticClass"]]: """ Get all SemanticClass types that are stated as allowed for this field. @@ -1150,15 +1223,14 @@ def get_all_possible_classes(self, include_subclasses: bool = False) -> \ res = set() for class_name in self._get_all_rule_type_names(): if class_name.__name__ in self._semantic_manager.class_catalogue: - class_ = self._semantic_manager. \ - get_class_by_name(class_name.__name__) + class_ = self._semantic_manager.get_class_by_name(class_name.__name__) res.add(class_) if include_subclasses: res.update(class_.__subclasses__()) return list(res) - def get_all_possible_individuals(self) -> List['SemanticIndividual']: + def get_all_possible_individuals(self) -> List["SemanticIndividual"]: """ Get all SemanticIndividuals that are stated as allowed for this field. @@ -1177,6 +1249,7 @@ class InstanceState(BaseModel): """State of instance that it had in Fiware on the moment of the last load Wrapped in an object to bypass the SemanticClass immutability """ + state: Optional[ContextEntity] = None @@ -1186,13 +1259,14 @@ class SemanticMetadata(BaseModel): A name and comment that can be used by the user to better identify the instance """ + model_config = ConfigDict(validate_assignment=True) - name: str = pyd.Field(default="", - description="Optional user-given name for the " - "instance") - comment: str = pyd.Field(default="", - description="Optional user-given comment for " - "the instance") + name: str = pyd.Field( + default="", description="Optional user-given name for the " "instance" + ) + comment: str = pyd.Field( + default="", description="Optional user-given comment for " "the instance" + ) class SemanticClass(BaseModel): @@ -1209,10 +1283,12 @@ class SemanticClass(BaseModel): loaded and returned, else a new instance of the class is initialised and returned """ + model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) header: InstanceHeader = pyd.Field( description="Header of instance. Holds the information where the " - "instance is saved in Fiware") + "instance is saved in Fiware" + ) id: str = pyd.Field( description="Id of the instance, equal to Fiware ContextEntity Id", regex=FiwareRegex.standard.value, @@ -1221,25 +1297,29 @@ class SemanticClass(BaseModel): old_state: InstanceState = pyd.Field( default=InstanceState(), description="State in Fiware the moment the instance was loaded " - "in the local registry. Used when saving. " - "Only the made changes are reflected") + "in the local registry. Used when saving. " + "Only the made changes are reflected", + ) references: Dict[InstanceIdentifier, List[str]] = pyd.Field( default={}, description="references made to this instance in other instances " - "RelationFields") + "RelationFields", + ) semantic_manager: BaseModel = pyd.Field( default=None, description="Pointer to the governing semantic_manager, " - "vague type to prevent forward ref problems. " - "But it will be of type 'SemanticsManager' in runtime") + "vague type to prevent forward ref problems. " + "But it will be of type 'SemanticsManager' in runtime", + ) metadata: SemanticMetadata = pyd.Field( default=SemanticMetadata(), description="Meta information about the instance. A name and comment " - "that can be used by the user to better identify the " - "instance") + "that can be used by the user to better identify the " + "instance", + ) def add_reference(self, identifier: InstanceIdentifier, relation_name: str): """ @@ -1255,8 +1335,7 @@ def add_reference(self, identifier: InstanceIdentifier, relation_name: str): self.references[identifier] = [] self.references[identifier].append(relation_name) - def remove_reference(self, identifier: InstanceIdentifier, - relation_name: str): + def remove_reference(self, identifier: InstanceIdentifier, relation_name: str): """ Remove the note of reference @@ -1272,31 +1351,37 @@ def remove_reference(self, identifier: InstanceIdentifier, del self.references[identifier] def __new__(cls, *args, **kwargs): - semantic_manager_ = kwargs['semantic_manager'] + semantic_manager_ = kwargs["semantic_manager"] - if 'enforce_new' in kwargs: - enforce_new = kwargs['enforce_new'] + if "enforce_new" in kwargs: + enforce_new = kwargs["enforce_new"] else: enforce_new = False - if 'identifier' in kwargs: - instance_id = kwargs['identifier'].id - header_ = kwargs['identifier'].header - assert cls.__name__ == kwargs['identifier'].type + if "identifier" in kwargs: + instance_id = kwargs["identifier"].id + header_ = kwargs["identifier"].header + assert cls.__name__ == kwargs["identifier"].type else: - instance_id = kwargs['id'] if 'id' in kwargs else "" + instance_id = kwargs["id"] if "id" in kwargs else "" import re - assert re.match(FiwareRegex.standard.value, instance_id), "Invalid character in ID" - header_ = kwargs['header'] if 'header' in kwargs else \ - semantic_manager_.get_default_header() + assert re.match( + FiwareRegex.standard.value, instance_id + ), "Invalid character in ID" + + header_ = ( + kwargs["header"] + if "header" in kwargs + else semantic_manager_.get_default_header() + ) if not instance_id == "" and not enforce_new: - identifier = InstanceIdentifier(id=instance_id, - type=cls.__name__, - header=header_) + identifier = InstanceIdentifier( + id=instance_id, type=cls.__name__, header=header_ + ) if semantic_manager_.does_instance_exists(identifier=identifier): return semantic_manager_.load_instance(identifier=identifier) @@ -1304,17 +1389,19 @@ def __new__(cls, *args, **kwargs): return super().__new__(cls) def __init__(self, *args, **kwargs): - semantic_manager_ = kwargs['semantic_manager'] + semantic_manager_ = kwargs["semantic_manager"] - if 'identifier' in kwargs: - instance_id_ = kwargs['identifier'].id - header_ = kwargs['identifier'].header - assert self.get_type() == kwargs['identifier'].type + if "identifier" in kwargs: + instance_id_ = kwargs["identifier"].id + header_ = kwargs["identifier"].header + assert self.get_type() == kwargs["identifier"].type else: - instance_id_ = kwargs['id'] if 'id' in kwargs \ - else str(uuid.uuid4()) - header_ = kwargs['header'] if 'header' in kwargs else \ - semantic_manager_.get_default_header() + instance_id_ = kwargs["id"] if "id" in kwargs else str(uuid.uuid4()) + header_ = ( + kwargs["header"] + if "header" in kwargs + else semantic_manager_.get_default_header() + ) # old_state_ = kwargs['old_state'] if 'old_state' in kwargs else None @@ -1324,8 +1411,8 @@ def __init__(self, *args, **kwargs): header=header_, ) - if 'enforce_new' in kwargs: - enforce_new = kwargs['enforce_new'] + if "enforce_new" in kwargs: + enforce_new = kwargs["enforce_new"] else: enforce_new = False @@ -1336,10 +1423,12 @@ def __init__(self, *args, **kwargs): if semantic_manager_.does_instance_exists(identifier_): return - super().__init__(id=instance_id_, - header=header_, - semantic_manager=semantic_manager_, - references={}) + super().__init__( + id=instance_id_, + header=header_, + semantic_manager=semantic_manager_, + references={}, + ) semantic_manager_.instance_registry.register(self) @@ -1499,8 +1588,9 @@ def get_field_by_name(self, field_name: str) -> Field: if value.name == field_name: return value - raise KeyError(f'{field_name} is not a valid Field for class ' - f'{self._get_class_name()}') + raise KeyError( + f"{field_name} is not a valid Field for class " f"{self._get_class_name()}" + ) def _build_reference_dict(self) -> Dict: """ @@ -1514,8 +1604,10 @@ def _build_reference_dict(self) -> Dict: Returns: Dict, with . replaced by --- """ - return {identifier.json().replace(".", "---"): value - for (identifier, value) in self.references.items()} + return { + identifier.json().replace(".", "---"): value + for (identifier, value) in self.references.items() + } def build_context_entity(self) -> ContextEntity: """ @@ -1525,10 +1617,7 @@ def build_context_entity(self) -> ContextEntity: Returns: ContextEntity """ - entity = ContextEntity( - id=self.id, - type=self._get_class_name() - ) + entity = ContextEntity(id=self.id, type=self._get_class_name()) for field in self.get_fields(): entity.add_attributes([field.build_context_attribute()]) @@ -1536,20 +1625,24 @@ def build_context_entity(self) -> ContextEntity: reference_str_dict = self._build_reference_dict() # add meta attributes - entity.add_attributes([ - NamedContextAttribute( - name="referencedBy", - type=DataType.STRUCTUREDVALUE, - value=reference_str_dict - ) - ]) - entity.add_attributes([ - NamedContextAttribute( - name="metadata", - type=DataType.STRUCTUREDVALUE, - value=self.metadata.model_dump() - ) - ]) + entity.add_attributes( + [ + NamedContextAttribute( + name="referencedBy", + type=DataType.STRUCTUREDVALUE, + value=reference_str_dict, + ) + ] + ) + entity.add_attributes( + [ + NamedContextAttribute( + name="metadata", + type=DataType.STRUCTUREDVALUE, + value=self.metadata.model_dump(), + ) + ] + ) return entity @@ -1560,8 +1653,7 @@ def get_identifier(self) -> InstanceIdentifier: Returns: str """ - return InstanceIdentifier(id=self.id, type=self.get_type(), - header=self.header) + return InstanceIdentifier(id=self.id, type=self.get_type(), header=self.header) def get_all_field_names(self) -> List[str]: res = [] @@ -1570,7 +1662,7 @@ def get_all_field_names(self) -> List[str]: return res def __str__(self): - return str(self.model_dump(exclude={'semantic_manager', 'old_state'})) + return str(self.model_dump(exclude={"semantic_manager", "old_state"})) def __hash__(self): values = [] @@ -1579,14 +1671,19 @@ def __hash__(self): ref_string = "" for ref in self.references.values(): - ref_string += f', {ref}' - - return hash((self.id, self.header, - self.metadata.name, self.metadata.comment, - frozenset(self.references.keys()), - ref_string, - frozenset(values) - )) + ref_string += f", {ref}" + + return hash( + ( + self.id, + self.header, + self.metadata.name, + self.metadata.comment, + frozenset(self.references.keys()), + ref_string, + frozenset(values), + ) + ) class SemanticDeviceClass(SemanticClass): @@ -1607,8 +1704,9 @@ class SemanticDeviceClass(SemanticClass): device_settings: iot.DeviceSettings = pyd.Field( default=iot.DeviceSettings(), description="Settings configuring the communication with an IoT Device " - "Wrapped in a model to bypass SemanticDeviceClass " - "immutability") + "Wrapped in a model to bypass SemanticDeviceClass " + "immutability", + ) def is_valid(self): """ @@ -1690,17 +1788,19 @@ def get_device_attribute_field_names(self) -> List[str]: def build_context_entity(self) -> ContextEntity: entity = super(SemanticDeviceClass, self).build_context_entity() - entity.add_attributes([ - NamedContextAttribute( - name="deviceSettings", - type=DataType.STRUCTUREDVALUE, - value=self.device_settings.model_dump() - ) - ]) + entity.add_attributes( + [ + NamedContextAttribute( + name="deviceSettings", + type=DataType.STRUCTUREDVALUE, + value=self.device_settings.model_dump(), + ) + ] + ) return entity def get_device_id(self) -> str: - return f'{self.get_type()}|{self.id}' + return f"{self.get_type()}|{self.id}" def build_context_device(self) -> iot.Device: """ @@ -1714,7 +1814,7 @@ def build_context_device(self) -> iot.Device: device_id=self.get_device_id(), service=self.header.service, service_path=self.header.service_path, - entity_name=f'{self.id}', + entity_name=f"{self.id}", entity_type=self._get_class_name(), apikey=self.device_settings.apikey, endpoint=self.device_settings.endpoint, @@ -1722,7 +1822,7 @@ def build_context_device(self) -> iot.Device: transport=self.device_settings.transport, timestamp=self.device_settings.timestamp, expressionLanguage=self.device_settings.expressionLanguage, - ngsiVersion=self.header.ngsi_version + ngsiVersion=self.header.ngsi_version, ) for field in self.get_fields(): @@ -1743,7 +1843,7 @@ def build_context_device(self) -> iot.Device: iot.StaticDeviceAttribute( name="metadata", type=DataType.STRUCTUREDVALUE, - value=self.metadata.model_dump() + value=self.metadata.model_dump(), ) ) device.add_attribute( @@ -1767,10 +1867,11 @@ class SemanticIndividual(BaseModel): Each instance of an SemanticIndividual Class is equal """ + model_config = ConfigDict(frozen=True) _parent_classes: List[type] = pyd.Field( description="List of ontology parent classes needed to validate " - "RelationFields" + "RelationFields" ) def __eq__(self, other): diff --git a/filip/semantics/vocabulary/__init__.py b/filip/semantics/vocabulary/__init__.py index 27e74268..46ba5247 100644 --- a/filip/semantics/vocabulary/__init__.py +++ b/filip/semantics/vocabulary/__init__.py @@ -1,27 +1,18 @@ -from .entities import \ - Entity, \ - Class, \ - Individual, \ - DataProperty, \ - ObjectProperty,\ - DataFieldType, \ - Datatype, \ - DatatypeType -from .relation import \ - TargetStatement,\ - StatementType, \ - RestrictionType, \ - Relation -from .combined_relations import \ - CombinedRelation, \ - CombinedObjectRelation, \ - CombinedDataRelation -from .source import \ - DependencyStatement, \ - ParsingError, \ - Source -from .vocabulary import \ - IdType, \ - LabelSummary, \ - VocabularySettings, \ - Vocabulary +from .entities import ( + Entity, + Class, + Individual, + DataProperty, + ObjectProperty, + DataFieldType, + Datatype, + DatatypeType, +) +from .relation import TargetStatement, StatementType, RestrictionType, Relation +from .combined_relations import ( + CombinedRelation, + CombinedObjectRelation, + CombinedDataRelation, +) +from .source import DependencyStatement, ParsingError, Source +from .vocabulary import IdType, LabelSummary, VocabularySettings, Vocabulary diff --git a/filip/semantics/vocabulary/combined_relations.py b/filip/semantics/vocabulary/combined_relations.py index 036b0481..c5cbfa9d 100644 --- a/filip/semantics/vocabulary/combined_relations.py +++ b/filip/semantics/vocabulary/combined_relations.py @@ -1,4 +1,5 @@ """Vocabulary Models for CombinedRelations""" + from aenum import Enum from filip.semantics.vocabulary import DataFieldType @@ -23,20 +24,23 @@ class CombinedRelation(BaseModel): relation_ids: List[str] = Field( default=[], description="List of all relations of the class that are " - "bundled; have the same property") - property_iri: str = Field(description="IRI of the property, under which " - "the relations are bundled") - class_iri: str = Field(description="IRI of the class the relations and " - "this CR belongs to") - - def get_relations(self, vocabulary: 'Vocabulary') -> List[Relation]: + "bundled; have the same property", + ) + property_iri: str = Field( + description="IRI of the property, under which " "the relations are bundled" + ) + class_iri: str = Field( + description="IRI of the class the relations and " "this CR belongs to" + ) + + def get_relations(self, vocabulary: "Vocabulary") -> List[Relation]: result = [] for id in self.relation_ids: result.append(vocabulary.get_relation_by_id(id)) return result - def get_property_label(self, vocabulary: 'Vocabulary') -> str: + def get_property_label(self, vocabulary: "Vocabulary") -> str: """Get the label of the Property. Overwritten by children Args: @@ -47,8 +51,7 @@ def get_property_label(self, vocabulary: 'Vocabulary') -> str: """ return "" - def get_all_targetstatements_as_string(self, vocabulary: 'Vocabulary') \ - -> str: + def get_all_targetstatements_as_string(self, vocabulary: "Vocabulary") -> str: """ Get a string stating all conditions(target statement) that need to be fulfilled, so that this CR is fulfilled @@ -65,7 +68,7 @@ def get_all_targetstatements_as_string(self, vocabulary: 'Vocabulary') \ return res[:-2] - def get_all_target_iris(self, vocabulary: 'Vocabulary') -> Set[str]: + def get_all_target_iris(self, vocabulary: "Vocabulary") -> Set[str]: """Get all iris of referenced targets Args: @@ -81,8 +84,8 @@ def get_all_target_iris(self, vocabulary: 'Vocabulary') -> Set[str]: iris.update(relation.get_all_target_iris()) return iris - def get_all_target_labels(self, vocabulary: 'Vocabulary') -> Set[str]: - """ Get all labels of referenced targets + def get_all_target_labels(self, vocabulary: "Vocabulary") -> Set[str]: + """Get all labels of referenced targets Args: vocabulary (Vocabulary): Vocabulary of the project @@ -90,11 +93,12 @@ def get_all_target_labels(self, vocabulary: 'Vocabulary') -> Set[str]: Returns: set(str) """ - return {vocabulary.get_label_for_entity_iri(iri) - for iri in self.get_all_target_iris(vocabulary)} + return { + vocabulary.get_label_for_entity_iri(iri) + for iri in self.get_all_target_iris(vocabulary) + } - def export_rule(self, vocabulary: 'Vocabulary', - stringify_fields: bool) -> str: + def export_rule(self, vocabulary: "Vocabulary", stringify_fields: bool) -> str: """Get the rule as string Args: @@ -106,12 +110,14 @@ def export_rule(self, vocabulary: 'Vocabulary', str """ - rules = [vocabulary.get_relation_by_id(id).export_rule(vocabulary) - for id in self.relation_ids] + rules = [ + vocabulary.get_relation_by_id(id).export_rule(vocabulary) + for id in self.relation_ids + ] if stringify_fields: return str(rules).replace('"', "") else: - return str(rules).replace("'","").replace('"', "'") + return str(rules).replace("'", "").replace('"', "'") class CombinedDataRelation(CombinedRelation): @@ -120,7 +126,7 @@ class CombinedDataRelation(CombinedRelation): Represents one Data Field of a class """ - def get_property_label(self, vocabulary: 'Vocabulary') -> str: + def get_property_label(self, vocabulary: "Vocabulary") -> str: """Get the label of the DataProperty Args: @@ -131,8 +137,7 @@ def get_property_label(self, vocabulary: 'Vocabulary') -> str: """ return vocabulary.get_data_property(self.property_iri).get_label() - def get_possible_enum_target_values(self, vocabulary: 'Vocabulary') \ - -> List[str]: + def get_possible_enum_target_values(self, vocabulary: "Vocabulary") -> List[str]: """Get all enum values that are allowed as values for this Data field Args: @@ -150,7 +155,7 @@ def get_possible_enum_target_values(self, vocabulary: 'Vocabulary') \ return sorted(list(enum_values)) - def get_field_type(self, vocabulary: 'Vocabulary') -> DataFieldType: + def get_field_type(self, vocabulary: "Vocabulary") -> DataFieldType: """Get type of CDR (command, devicedata , simple) Args: @@ -162,7 +167,7 @@ def get_field_type(self, vocabulary: 'Vocabulary') -> DataFieldType: property = vocabulary.get_data_property(self.property_iri) return property.field_type - def is_device_relation(self, vocabulary: 'Vocabulary') -> bool: + def is_device_relation(self, vocabulary: "Vocabulary") -> bool: """Test if the CDR is a device property(command, or readings) Args: @@ -190,17 +195,17 @@ def get_all_possible_target_class_iris(self, vocabulary) -> List[str]: List[str] """ from . import Vocabulary + assert isinstance(vocabulary, Vocabulary) relations = self.get_relations(vocabulary) result_set = set() for relation in relations: - result_set.update(relation. - get_all_possible_target_class_iris(vocabulary)) + result_set.update(relation.get_all_possible_target_class_iris(vocabulary)) return list(result_set) - def get_property_label(self, vocabulary: 'Vocabulary') -> str: + def get_property_label(self, vocabulary: "Vocabulary") -> str: """Get the label of the ObjectProperty Args: @@ -211,15 +216,17 @@ def get_property_label(self, vocabulary: 'Vocabulary') -> str: """ return vocabulary.get_object_property(self.property_iri).get_label() - def get_inverse_of_labels(self, vocabulary: 'Vocabulary') -> List[str]: + def get_inverse_of_labels(self, vocabulary: "Vocabulary") -> List[str]: """Get the labels of the inverse_of properties of this COR - Args: - vocabulary (Vocabulary): Vocabulary of the project + Args: + vocabulary (Vocabulary): Vocabulary of the project - Returns: - List[str] - """ + Returns: + List[str] + """ property = vocabulary.get_object_property(self.property_iri) - return [vocabulary.get_entity_by_iri(iri).label - for iri in property.inverse_property_iris] \ No newline at end of file + return [ + vocabulary.get_entity_by_iri(iri).label + for iri in property.inverse_property_iris + ] diff --git a/filip/semantics/vocabulary/entities.py b/filip/semantics/vocabulary/entities.py index 8b11cd3c..56c9da43 100644 --- a/filip/semantics/vocabulary/entities.py +++ b/filip/semantics/vocabulary/entities.py @@ -7,13 +7,14 @@ from .source import DependencyStatement if TYPE_CHECKING: - from . import \ - CombinedObjectRelation, \ - CombinedDataRelation, \ - CombinedRelation, \ - Relation, \ - Vocabulary, \ - Source + from . import ( + CombinedObjectRelation, + CombinedDataRelation, + CombinedRelation, + Relation, + Vocabulary, + Source, + ) class Entity(BaseModel): @@ -27,28 +28,32 @@ class Entity(BaseModel): field key. The user can overwrite the given label """ + iri: str = Field(description="Unique Internationalized Resource Identifier") label: str = Field( default="", description="Label (displayname) extracted from source file " - "(multiple Entities could have the same label)") + "(multiple Entities could have the same label)", + ) user_set_label: Any = Field( default="", description="Given by user and overwrites 'label'." - " Needed to make labels unique") + " Needed to make labels unique", + ) comment: str = Field( - default="", - description="Comment extracted from the ontology/source") + default="", description="Comment extracted from the ontology/source" + ) source_ids: Set[str] = Field( - default=set(), - description="IDs of the sources that influenced this class") + default=set(), description="IDs of the sources that influenced this class" + ) predefined: bool = Field( default=False, description="Stats if the entity is not extracted from a source, " - "but predefined in the program (Standard Datatypes)") + "but predefined in the program (Standard Datatypes)", + ) def get_label(self) -> str: - """ Get the label for the entity. + """Get the label for the entity. If the user has set a label it is returned, else the label extracted from the source @@ -60,8 +65,8 @@ def get_label(self) -> str: return self.get_original_label() - def set_label(self, label:str): - """ Change the display label of the entity + def set_label(self, label: str): + """Change the display label of the entity Args: label (str): Label that the label should have @@ -69,7 +74,7 @@ def set_label(self, label:str): self.user_set_label = label def get_ontology_iri(self) -> str: - """ Get the IRI of the ontology that this entity belongs to + """Get the IRI of the ontology that this entity belongs to (extracted from IRI) Returns: @@ -78,8 +83,8 @@ def get_ontology_iri(self) -> str: index = self.iri.find("#") return self.iri[:index] - def get_source_names(self, vocabulary: 'Vocabulary') -> List[str]: - """ Get the names of all the sources + def get_source_names(self, vocabulary: "Vocabulary") -> List[str]: + """Get the names of all the sources Args: vocabulary (Vocabulary): Vocabulary of the project @@ -87,13 +92,12 @@ def get_source_names(self, vocabulary: 'Vocabulary') -> List[str]: Returns: str """ - names = [vocabulary.get_source(id).get_name() for - id in self.source_ids] + names = [vocabulary.get_source(id).get_name() for id in self.source_ids] return names - def get_sources(self, vocabulary: 'Vocabulary') -> List['Source']: - """ Get all the source objects that influenced this entity. + def get_sources(self, vocabulary: "Vocabulary") -> List["Source"]: + """Get all the source objects that influenced this entity. The sources are sorted according to their names Args: @@ -109,7 +113,7 @@ def get_sources(self, vocabulary: 'Vocabulary') -> List['Source']: return sources def _lists_are_identical(self, a: List, b: List) -> bool: - """ Methode to test if to lists contain the same entries + """Methode to test if to lists contain the same entries Args: a (List): first list @@ -120,7 +124,7 @@ def _lists_are_identical(self, a: List, b: List) -> bool: return len(set(a).intersection(b)) == len(set(a)) and len(a) == len(b) def is_renamed(self) -> bool: - """ Check if the entity was renamed by the user + """Check if the entity was renamed by the user Returns: bool @@ -128,7 +132,7 @@ def is_renamed(self) -> bool: return not self.user_set_label == "" def get_original_label(self) -> str: - """ Get label as defined in the source + """Get label as defined in the source It can be that the label is empty, then extract the label from the iri Returns: @@ -155,24 +159,27 @@ class Class(Entity): # The objects whose ids/iris are listed here can be looked up in the # vocabulary of this class child_class_iris: List[str] = Field( - default=[], - description="All class_iris of classes that inherit from this class") + default=[], description="All class_iris of classes that inherit from this class" + ) ancestor_class_iris: List[str] = Field( default=[], - description="All class_iris of classes from which this class inherits") + description="All class_iris of classes from which this class inherits", + ) parent_class_iris: List[str] = Field( default=[], description="All class_iris of classes that are direct parents of this " - "class") + "class", + ) relation_ids: List[str] = Field( - default=[], - description="All ids of relations defined for this class") + default=[], description="All ids of relations defined for this class" + ) combined_object_relation_ids: List[str] = Field( default=[], - description="All combined_object_relations ids defined for this class") + description="All combined_object_relations ids defined for this class", + ) combined_data_relation_ids: List[str] = Field( - default=[], - description="All combined_data_relations ids defined for this class") + default=[], description="All combined_data_relations ids defined for this class" + ) def get_relation_ids(self) -> List[str]: """Get all ids of relations belonging to this class @@ -182,7 +189,7 @@ def get_relation_ids(self) -> List[str]: """ return self.relation_ids - def get_relations(self, vocabulary: 'Vocabulary') -> List['Relation']: + def get_relations(self, vocabulary: "Vocabulary") -> List["Relation"]: """Get all relations belonging to this class Args: @@ -197,8 +204,9 @@ def get_relations(self, vocabulary: 'Vocabulary') -> List['Relation']: return result - def get_combined_object_relations(self, vocabulary: 'Vocabulary') -> \ - List['CombinedObjectRelation']: + def get_combined_object_relations( + self, vocabulary: "Vocabulary" + ) -> List["CombinedObjectRelation"]: """Get all combined object relations belonging to this class Args: @@ -214,8 +222,9 @@ def get_combined_object_relations(self, vocabulary: 'Vocabulary') -> \ return result - def get_combined_data_relations(self, vocabulary: 'Vocabulary') -> \ - List['CombinedDataRelation']: + def get_combined_data_relations( + self, vocabulary: "Vocabulary" + ) -> List["CombinedDataRelation"]: """Get all combined data relations belonging to this class Args: @@ -231,8 +240,9 @@ def get_combined_data_relations(self, vocabulary: 'Vocabulary') -> \ return result - def get_combined_relations(self, vocabulary: 'Vocabulary') -> \ - List['CombinedRelation']: + def get_combined_relations( + self, vocabulary: "Vocabulary" + ) -> List["CombinedRelation"]: """Get all combined relations belonging to this class Args: @@ -263,8 +273,8 @@ def is_child_of_all_classes(self, target_list: List[str]) -> bool: return True def get_combined_object_relation_with_property_iri( - self, obj_prop_iri: str, vocabulary: 'Vocabulary') \ - -> 'CombinedObjectRelation': + self, obj_prop_iri: str, vocabulary: "Vocabulary" + ) -> "CombinedObjectRelation": """ Get the CombinedObjectRelation of this class that combines the relations of the given ObjectProperty @@ -281,8 +291,7 @@ def get_combined_object_relation_with_property_iri( return cor return None - def get_combined_data_relation_with_property_iri(self, property_iri, - vocabulary): + def get_combined_data_relation_with_property_iri(self, property_iri, vocabulary): """ Get the CombinedDataRelation of this class that combines the relations of the given DataProperty @@ -299,8 +308,9 @@ def get_combined_data_relation_with_property_iri(self, property_iri, return cdr return None - def get_combined_relation_with_property_iri(self, property_iri, vocabulary)\ - -> Union['CombinedRelation', None]: + def get_combined_relation_with_property_iri( + self, property_iri, vocabulary + ) -> Union["CombinedRelation", None]: """ Get the CombinedRelation of this class that combines the relations of the given Property @@ -313,7 +323,7 @@ def get_combined_relation_with_property_iri(self, property_iri, vocabulary)\ Returns: CombinedRelation, None if iri is unknown - """ + """ for cdr in self.get_combined_data_relations(vocabulary): if cdr.property_iri == property_iri: return cdr @@ -322,7 +332,7 @@ def get_combined_relation_with_property_iri(self, property_iri, vocabulary)\ return cor return None - def get_ancestor_classes(self, vocabulary: 'Vocabulary') -> List['Class']: + def get_ancestor_classes(self, vocabulary: "Vocabulary") -> List["Class"]: """Get all ancestor classes of this class Args: @@ -336,9 +346,9 @@ def get_ancestor_classes(self, vocabulary: 'Vocabulary') -> List['Class']: ancestors.append(vocabulary.get_class_by_iri(ancestor_iri)) return ancestors - def get_parent_classes(self, - vocabulary: 'Vocabulary', - remove_redundancy: bool = False) -> List['Class']: + def get_parent_classes( + self, vocabulary: "Vocabulary", remove_redundancy: bool = False + ) -> List["Class"]: """Get all parent classes of this class Args: @@ -364,8 +374,9 @@ def get_parent_classes(self, return parents - def treat_dependency_statements(self, vocabulary: 'Vocabulary') -> \ - List[DependencyStatement]: + def treat_dependency_statements( + self, vocabulary: "Vocabulary" + ) -> List[DependencyStatement]: """ Purge and list all pointers/iris that are not contained in the vocabulary @@ -383,11 +394,14 @@ def treat_dependency_statements(self, vocabulary: 'Vocabulary') -> \ parents_to_purge = [] for parent_iri in self.parent_class_iris: found = parent_iri in vocabulary.classes - statements.append(DependencyStatement(type="Parent Class", - class_iri=self.iri, - dependency_iri=parent_iri, - fulfilled=found - )) + statements.append( + DependencyStatement( + type="Parent Class", + class_iri=self.iri, + dependency_iri=parent_iri, + fulfilled=found, + ) + ) if not found: parents_to_purge.append(parent_iri) for iri in parents_to_purge: @@ -398,7 +412,8 @@ def treat_dependency_statements(self, vocabulary: 'Vocabulary') -> \ for relation in self.get_relations(vocabulary): relation_statements = relation.get_dependency_statements( - vocabulary, self.get_ontology_iri(), self.iri) + vocabulary, self.get_ontology_iri(), self.iri + ) for statement in relation_statements: if statement.fulfilled == False: relation_ids_to_purge.add(relation.id) @@ -410,8 +425,9 @@ def treat_dependency_statements(self, vocabulary: 'Vocabulary') -> \ return statements - def get_next_combined_relation_id(self, current_cr_id: str, - object_relations: bool) -> str: + def get_next_combined_relation_id( + self, current_cr_id: str, object_relations: bool + ) -> str: """Get the alphabetically(Property label) next CombinedRelation. If no CR is after the given one, the first is returned @@ -430,13 +446,14 @@ def get_next_combined_relation_id(self, current_cr_id: str, list_ = self.combined_object_relation_ids current_index = list_.index(current_cr_id) - res_index = current_index+1 + res_index = current_index + 1 if res_index >= len(list_): res_index = 0 return list_[res_index] - def get_previous_combined_relation_id(self, current_cr_id: str, - object_relations: bool) -> str: + def get_previous_combined_relation_id( + self, current_cr_id: str, object_relations: bool + ) -> str: """Get the alphabetically(Property label) previous CombinedRelation. If no CR is before the given one, the last is returned @@ -458,12 +475,12 @@ def get_previous_combined_relation_id(self, current_cr_id: str, current_index = list_.index(current_cr_id) res_index = current_index - 1 if res_index < 0: - res_index = len(list_)-1 + res_index = len(list_) - 1 return list_[res_index] - def is_logically_equivalent_to(self, class_: 'Class', - vocabulary: 'Vocabulary', - old_vocabulary: 'Vocabulary') -> bool: + def is_logically_equivalent_to( + self, class_: "Class", vocabulary: "Vocabulary", old_vocabulary: "Vocabulary" + ) -> bool: """Test if a class is logically equivalent in two vocabularies. Args: @@ -476,18 +493,21 @@ def is_logically_equivalent_to(self, class_: 'Class', """ # test if parent classes are identical - if not self._lists_are_identical(class_.parent_class_iris, - self.parent_class_iris): + if not self._lists_are_identical( + class_.parent_class_iris, self.parent_class_iris + ): return False # test if combined object relation ids are identical - if not self._lists_are_identical(class_.combined_object_relation_ids, - self.combined_object_relation_ids): + if not self._lists_are_identical( + class_.combined_object_relation_ids, self.combined_object_relation_ids + ): return False # test if combined data relation ids are identical - if not self._lists_are_identical(class_.combined_data_relation_ids, - self.combined_data_relation_ids): + if not self._lists_are_identical( + class_.combined_data_relation_ids, self.combined_data_relation_ids + ): return False # test if combined relations are identical @@ -502,14 +522,13 @@ def is_logically_equivalent_to(self, class_: 'Class', for old_relation in old_cr.get_relations(old_vocabulary): old_relation_strings.append(old_relation.to_string(vocabulary)) - if not self._lists_are_identical(relation_strings, - old_relation_strings): + if not self._lists_are_identical(relation_strings, old_relation_strings): return False return True - def is_iot_class(self, vocabulary: 'Vocabulary') -> bool: + def is_iot_class(self, vocabulary: "Vocabulary") -> bool: """ A class is an iot/device class if it contains one CDR, where the relation is marked as a device relation: DeviceAttribute/Command @@ -533,39 +552,42 @@ class DatatypeType(str, Enum): """ Types of a Datatype """ - string = 'string' - number = 'number' - date = 'date' - enum = 'enum' + + string = "string" + number = "number" + date = "date" + enum = "enum" class DatatypeFields(BaseModel): """Key Fields describing a Datatype""" - type: DatatypeType = Field(default=DatatypeType.string, - description="Type of the datatype") + + type: DatatypeType = Field( + default=DatatypeType.string, description="Type of the datatype" + ) number_has_range: Any = Field( - default=False, - description="If Type==Number: Does the datatype define a range") + default=False, description="If Type==Number: Does the datatype define a range" + ) number_range_min: Union[int, str] = Field( default="/", description="If Type==Number: Min value of the datatype range, " - "if a range is defined") + "if a range is defined", + ) number_range_max: Union[int, str] = Field( default="/", description="If Type==Number: Max value of the datatype range, " - "if a range is defined") + "if a range is defined", + ) number_decimal_allowed: bool = Field( - default=False, - description="If Type==Number: Are decimal numbers allowed?") + default=False, description="If Type==Number: Are decimal numbers allowed?" + ) forbidden_chars: List[str] = Field( - default=[], - description="If Type==String: Blacklisted chars") + default=[], description="If Type==String: Blacklisted chars" + ) allowed_chars: List[str] = Field( - default=[], - description="If Type==String: Whitelisted chars") - enum_values: List[str] = Field( - default=[], - description="If Type==Enum: Enum values") + default=[], description="If Type==String: Whitelisted chars" + ) + enum_values: List[str] = Field(default=[], description="If Type==Enum: Enum values") class Datatype(Entity, DatatypeFields): @@ -580,18 +602,26 @@ class Datatype(Entity, DatatypeFields): vocabulary """ - def export(self) -> Dict[str,str]: - """ Export datatype as dict + def export(self) -> Dict[str, str]: + """Export datatype as dict Returns: Dict[str,str] """ - res = self.model_dump(include={'type', 'number_has_range', - 'number_range_min', 'number_range_max', - 'number_decimal_allowed', 'forbidden_chars', - 'allowed_chars', 'enum_values'}, - exclude_defaults=True) - res['type'] = self.type.value + res = self.model_dump( + include={ + "type", + "number_has_range", + "number_range_min", + "number_range_max", + "number_decimal_allowed", + "forbidden_chars", + "allowed_chars", + "enum_values", + }, + exclude_defaults=True, + ) + res["type"] = self.type.value return res def value_is_valid(self, value: str) -> bool: @@ -644,6 +674,7 @@ def value_is_valid(self, value: str) -> bool: if self.type == DatatypeType.date: try: from dateutil.parser import parse + parse(value, fuzzy=False) return True @@ -652,9 +683,12 @@ def value_is_valid(self, value: str) -> bool: return True - def is_logically_equivalent_to(self, datatype:'Datatype', - vocabulary: 'Vocabulary', - old_vocabulary: 'Vocabulary') -> bool: + def is_logically_equivalent_to( + self, + datatype: "Datatype", + vocabulary: "Vocabulary", + old_vocabulary: "Vocabulary", + ) -> bool: """Test if this datatype is logically equivalent to the given datatype Args: @@ -691,7 +725,8 @@ class Individual(Entity): parent_class_iris: List[str] = Field( default=[], description="List of all parent class iris, " - "an individual can have multiple parents") + "an individual can have multiple parents", + ) def to_string(self) -> str: """Get a string representation of the Individual @@ -699,10 +734,10 @@ def to_string(self) -> str: Returns: str """ - return "(Individual)"+self.get_label() + return "(Individual)" + self.get_label() - def get_ancestor_iris(self, vocabulary: 'Vocabulary') -> List[str]: - """ Get all iris of ancestor classes + def get_ancestor_iris(self, vocabulary: "Vocabulary") -> List[str]: + """Get all iris of ancestor classes Args: vocabulary (Vocabulary): Vocabulary of the project @@ -713,13 +748,14 @@ def get_ancestor_iris(self, vocabulary: 'Vocabulary') -> List[str]: ancestor_iris = set() for parent_iri in self.parent_class_iris: ancestor_iris.add(parent_iri) - ancestor_iris.update(vocabulary.get_class_by_iri(parent_iri). - ancestor_class_iris) + ancestor_iris.update( + vocabulary.get_class_by_iri(parent_iri).ancestor_class_iris + ) return list(ancestor_iris) - def get_parent_classes(self, vocabulary: 'Vocabulary') -> List['Class']: - """ Get all parent class objects + def get_parent_classes(self, vocabulary: "Vocabulary") -> List["Class"]: + """Get all parent class objects Args: vocabulary (Vocabulary): Vocabulary of the project @@ -732,9 +768,12 @@ def get_parent_classes(self, vocabulary: 'Vocabulary') -> List['Class']: parents.append(vocabulary.get_class_by_iri(parent_iri)) return parents - def is_logically_equivalent_to(self, individual: 'Individual', - vocabulary: 'Vocabulary', - old_vocabulary: 'Vocabulary') -> bool: + def is_logically_equivalent_to( + self, + individual: "Individual", + vocabulary: "Vocabulary", + old_vocabulary: "Vocabulary", + ) -> bool: """Test if this individal is logically equivalent in two vocabularies. Args: @@ -749,14 +788,16 @@ def is_logically_equivalent_to(self, individual: 'Individual', bool """ - if not self._lists_are_identical(self.parent_class_iris, - individual.parent_class_iris): + if not self._lists_are_identical( + self.parent_class_iris, individual.parent_class_iris + ): return False return True - def treat_dependency_statements(self, vocabulary: 'Vocabulary') -> \ - List[DependencyStatement]: - """ Purge and list all pointers/iris that are not contained in the + def treat_dependency_statements( + self, vocabulary: "Vocabulary" + ) -> List[DependencyStatement]: + """Purge and list all pointers/iris that are not contained in the vocabulary Args: @@ -770,11 +811,14 @@ def treat_dependency_statements(self, vocabulary: 'Vocabulary') -> \ for parent_iri in self.parent_class_iris: found = parent_iri in vocabulary.classes - statements.append(DependencyStatement(type="Parent Class", - class_iri=self.iri, - dependency_iri=parent_iri, - fulfilled=found - )) + statements.append( + DependencyStatement( + type="Parent Class", + class_iri=self.iri, + dependency_iri=parent_iri, + fulfilled=found, + ) + ) if not found: self.parent_class_iris.remove(parent_iri) @@ -784,6 +828,7 @@ def treat_dependency_statements(self, vocabulary: 'Vocabulary') -> \ class DataFieldType(str, Enum): """Type of the field that represents the DataProperty""" + command = "command" device_attribute = "device_attribute" simple = "simple" @@ -797,7 +842,7 @@ class DataProperty(Entity): field_type: DataFieldType = Field( default=DataFieldType.simple, description="Type of the dataproperty; set by the user while " - "configuring the vocabulary" + "configuring the vocabulary", ) @@ -809,9 +854,10 @@ class ObjectProperty(Entity): inverse_property_iris: Set[str] = Field( default=set(), description="List of property iris that are inverse:Of; " - "If an instance i2 is added in an instance i1 " - "for this property. Then i1 is added to i2 under the" - " inverseProperty (if the class has that property)") + "If an instance i2 is added in an instance i1 " + "for this property. Then i1 is added to i2 under the" + " inverseProperty (if the class has that property)", + ) def add_inverse_property_iri(self, iri: str): """Add an inverse property @@ -824,9 +870,12 @@ def add_inverse_property_iri(self, iri: str): """ self.inverse_property_iris.add(iri) - def is_logically_equivalent_to(self, object_property: 'ObjectProperty', - vocabulary: 'Vocabulary', - old_vocabulary: 'Vocabulary') -> bool: + def is_logically_equivalent_to( + self, + object_property: "ObjectProperty", + vocabulary: "Vocabulary", + old_vocabulary: "Vocabulary", + ) -> bool: """Test if this Property in the new_vocabulary is logically equivalent to the object_property in the old_vocabulary @@ -841,8 +890,7 @@ def is_logically_equivalent_to(self, object_property: 'ObjectProperty', Returns: bool """ - if not self.inverse_property_iris == \ - object_property.inverse_property_iris: + if not self.inverse_property_iris == object_property.inverse_property_iris: return False return True diff --git a/filip/semantics/vocabulary/relation.py b/filip/semantics/vocabulary/relation.py index 99e65ecc..5cf10845 100644 --- a/filip/semantics/vocabulary/relation.py +++ b/filip/semantics/vocabulary/relation.py @@ -21,9 +21,10 @@ class StatementType(str, Enum): A statement is either a leaf and holds an iri/label or it is a combination of leafs with or / and """ - OR = 'or' - AND = 'and' - LEAF = 'leaf' + + OR = "or" + AND = "and" + LEAF = "leaf" class TargetStatement(BaseModel): @@ -43,20 +44,20 @@ class TargetStatement(BaseModel): target_data_value: Optional[str] = Field( default=None, - description="Holds the value if the relation is a hasValue (LEAF only)") - target_iri: str = Field( - default="", - description="The IRI of the target (LEAF only)") - target_statements: List['TargetStatement'] = Field( + description="Holds the value if the relation is a hasValue (LEAF only)", + ) + target_iri: str = Field(default="", description="The IRI of the target (LEAF only)") + target_statements: List["TargetStatement"] = Field( default=[], description="The targetstatements that are combined with this " - "targetstatement (and/or only)" + "targetstatement (and/or only)", + ) + type: StatementType = Field( + default=StatementType.LEAF, description="Statement types" ) - type: StatementType = Field(default=StatementType.LEAF, - description="Statement types") def set_target(self, target_iri: str, target_data_value: str = None): - """ Set target for this statement and make it a LEAF statement + """Set target for this statement and make it a LEAF statement Args: target_iri (str): iri of the target (class or datatype iri) @@ -136,7 +137,7 @@ def get_all_targets(self) -> List[List[str]]: counter += 1 return result - def to_string(self, vocabulary: 'Vocabulary') -> str: + def to_string(self, vocabulary: "Vocabulary") -> str: """Get a string representation of the targetstatment Args: @@ -159,8 +160,7 @@ def to_string(self, vocabulary: 'Vocabulary') -> str: return result - def is_fulfilled_by_iri_value(self, value: str, ancestor_values: List[str]) \ - -> bool: + def is_fulfilled_by_iri_value(self, value: str, ancestor_values: List[str]) -> bool: """ Test if a set of values fulfills the targetstatement; Only for objectRelations @@ -190,8 +190,7 @@ def is_fulfilled_by_iri_value(self, value: str, ancestor_values: List[str]) \ return True return False - def is_fulfilled_by_data_value(self, value: str, vocabulary: 'Vocabulary') \ - -> bool: + def is_fulfilled_by_data_value(self, value: str, vocabulary: "Vocabulary") -> bool: """ Test if a set of values fulfills the targetstatement; Only for dataRelations @@ -208,13 +207,14 @@ def is_fulfilled_by_data_value(self, value: str, vocabulary: 'Vocabulary') \ return value == self.target_data_value from .vocabulary import IdType + if not vocabulary.is_id_of_type(self.target_iri, IdType.datatype): return False else: datatype = vocabulary.get_datatype(self.target_iri) return datatype.value_is_valid(value) - def retrieve_label(self, vocabulary: 'Vocabulary') -> str: + def retrieve_label(self, vocabulary: "Vocabulary") -> str: """Get the label of the target_iri. Only logical for Leaf statements Args: @@ -231,10 +231,8 @@ def retrieve_label(self, vocabulary: 'Vocabulary') -> str: return "" def get_dependency_statements( - self, - vocabulary: 'Vocabulary', - ontology_iri: str, - class_iri: str) -> List[DependencyStatement]: + self, vocabulary: "Vocabulary", ontology_iri: str, class_iri: str + ) -> List[DependencyStatement]: """ Get a list of all pointers/iris that are not contained in the vocabulary. Purging is done in class @@ -255,21 +253,26 @@ def get_dependency_statements( if self.target_data_value is None: # check if predefined datatype if not vocabulary.iri_is_predefined_datatype(self.target_iri): - found = self.target_iri in vocabulary.classes or \ - self.target_iri in vocabulary.datatypes or \ - self.target_iri in vocabulary.individuals + found = ( + self.target_iri in vocabulary.classes + or self.target_iri in vocabulary.datatypes + or self.target_iri in vocabulary.individuals + ) statements.append( DependencyStatement( type="Relation Target", class_iri=class_iri, dependency_iri=self.target_iri, - fulfilled=found) + fulfilled=found, + ) ) else: for target_statement in self.target_statements: statements.extend( target_statement.get_dependency_statements( - vocabulary, ontology_iri, class_iri)) + vocabulary, ontology_iri, class_iri + ) + ) return statements @@ -281,14 +284,15 @@ def get_dependency_statements( class RestrictionType(str, Enum): """RestrictionTypes, as defined for OWL""" - _init_ = 'value __doc__' - some = 'some', 'at least 1 value of that target' - only = 'only', 'only value of that target' - min = 'min', 'min n values of that target' - max = 'max', 'max n values of that target' - exactly = 'exactly', 'exactly n values of that target' - value = 'value', 'predefined value' + _init_ = "value __doc__" + + some = "some", "at least 1 value of that target" + only = "only", "only value of that target" + min = "min", "min n values of that target" + max = "max", "max n values of that target" + exactly = "exactly", "exactly n values of that target" + value = "value", "predefined value" class Relation(BaseModel): @@ -303,21 +307,21 @@ class can/should have under this property inherit it """ - id: str = Field(description="Unique generated Relation ID, " - "for internal use") + id: str = Field(description="Unique generated Relation ID, " "for internal use") restriction_type: RestrictionType = Field( - default=None, - description="Restriction type of this relation") + default=None, description="Restriction type of this relation" + ) restriction_cardinality: int = Field( - default=-1, - description="Only needed for min, max, equaly states the 'n'") + default=-1, description="Only needed for min, max, equaly states the 'n'" + ) property_iri: str = Field( - default="", - description="IRI of the property (data- or object-)") + default="", description="IRI of the property (data- or object-)" + ) target_statement: TargetStatement = Field( default=None, description="Complex statement which classes/datatype_catalogue " - "are allowed/required") + "are allowed/required", + ) def get_targets(self) -> List[List[str]]: """Get all targets specified in the target statement in AND-OR Notation @@ -327,8 +331,8 @@ def get_targets(self) -> List[List[str]]: """ return self.target_statement.get_all_targets() - def to_string(self, vocabulary: 'Vocabulary') -> str: - """ Get a string representation of the relation + def to_string(self, vocabulary: "Vocabulary") -> str: + """Get a string representation of the relation Args: vocabulary (Vocabulary): Vocabulary of this project @@ -338,15 +342,21 @@ def to_string(self, vocabulary: 'Vocabulary') -> str: """ if self.restriction_cardinality == -1: - return "{} {}".format(self.restriction_type, self.target_statement. - to_string(vocabulary)) + return "{} {}".format( + self.restriction_type, self.target_statement.to_string(vocabulary) + ) else: - return self.restriction_type + " " + \ - str(self.restriction_cardinality) + " " \ - + self.target_statement.to_string(vocabulary) - - def is_restriction_fulfilled(self, number_of_fulfilling_values: int, - total_number_of_values: int) -> bool: + return ( + self.restriction_type + + " " + + str(self.restriction_cardinality) + + " " + + self.target_statement.to_string(vocabulary) + ) + + def is_restriction_fulfilled( + self, number_of_fulfilling_values: int, total_number_of_values: int + ) -> bool: """Test if the restriction type is fulfilled by comparing the number of fulfilling values against the total number of values given @@ -366,21 +376,18 @@ def is_restriction_fulfilled(self, number_of_fulfilling_values: int, if self.restriction_type == RestrictionType.only: return number_of_fulfilling_values == total_number_of_values if self.restriction_type == RestrictionType.min: - return number_of_fulfilling_values >= \ - (int)(self.restriction_cardinality) + return number_of_fulfilling_values >= (int)(self.restriction_cardinality) if self.restriction_type == RestrictionType.max: - return number_of_fulfilling_values <= \ - (int)(self.restriction_cardinality) + return number_of_fulfilling_values <= (int)(self.restriction_cardinality) if self.restriction_type == RestrictionType.exactly: - return number_of_fulfilling_values == \ - (int)(self.restriction_cardinality) + return number_of_fulfilling_values == (int)(self.restriction_cardinality) if self.restriction_type == RestrictionType.value: return number_of_fulfilling_values >= 1 def get_dependency_statements( - self, vocabulary: 'Vocabulary', ontology_iri: str, class_iri: str) \ - -> List[DependencyStatement]: - """ Get a list of all pointers/iris that are not contained in the + self, vocabulary: "Vocabulary", ontology_iri: str, class_iri: str + ) -> List[DependencyStatement]: + """Get a list of all pointers/iris that are not contained in the vocabulary Purging is done in class @@ -395,22 +402,34 @@ def get_dependency_statements( """ statements = [] - found = self.property_iri in vocabulary.object_properties or \ - self.property_iri in vocabulary.data_properties - - statements.append(DependencyStatement(type="Relation Property", - class_iri=class_iri, - dependency_iri=self.property_iri, - fulfilled=found)) - - statements.extend(self.target_statement.get_dependency_statements( - vocabulary, ontology_iri, class_iri)) + found = ( + self.property_iri in vocabulary.object_properties + or self.property_iri in vocabulary.data_properties + ) + + statements.append( + DependencyStatement( + type="Relation Property", + class_iri=class_iri, + dependency_iri=self.property_iri, + fulfilled=found, + ) + ) + + statements.extend( + self.target_statement.get_dependency_statements( + vocabulary, ontology_iri, class_iri + ) + ) return statements def is_fulfilled_with_iris( - self, vocabulary: 'Vocabulary', values: List[str], - ancestor_values: List[List[str]]) -> bool: + self, + vocabulary: "Vocabulary", + values: List[str], + ancestor_values: List[List[str]], + ) -> bool: """Test if a set of values fulfills the rules of the relation Args: @@ -424,14 +443,15 @@ def is_fulfilled_with_iris( number_of_fulfilling_values = 0 for i in range(len(values)): if self.target_statement.is_fulfilled_by_iri_value( - values[i], ancestor_values[i]): + values[i], ancestor_values[i] + ): number_of_fulfilling_values += 1 - return self.is_restriction_fulfilled(number_of_fulfilling_values, - len(values)) + return self.is_restriction_fulfilled(number_of_fulfilling_values, len(values)) - def is_fulfilled_with_values(self, vocabulary: 'Vocabulary', - values: List[str]) -> bool: + def is_fulfilled_with_values( + self, vocabulary: "Vocabulary", values: List[str] + ) -> bool: """Test if a set of values fulfills the rules of the relation. Used if property is a data property @@ -445,15 +465,12 @@ def is_fulfilled_with_values(self, vocabulary: 'Vocabulary', number_of_fulfilling_values = 0 for i in range(len(values)): - if self.target_statement.is_fulfilled_by_data_value(values[i], - vocabulary): + if self.target_statement.is_fulfilled_by_data_value(values[i], vocabulary): number_of_fulfilling_values += 1 - return self.is_restriction_fulfilled(number_of_fulfilling_values, - len(values)) + return self.is_restriction_fulfilled(number_of_fulfilling_values, len(values)) - def get_all_possible_target_class_iris(self, vocabulary: 'Vocabulary') \ - -> Set[str]: + def get_all_possible_target_class_iris(self, vocabulary: "Vocabulary") -> Set[str]: """Get a set of class iris that are possible values for an objectRelation @@ -476,14 +493,12 @@ def get_all_possible_target_class_iris(self, vocabulary: 'Vocabulary') \ for class_ in vocabulary.get_classes(): if class_.is_child_of_all_classes(target_list): possible_class_iris.add(class_.iri) - children = vocabulary.get_class_by_iri(class_.iri). \ - child_class_iris + children = vocabulary.get_class_by_iri(class_.iri).child_class_iris possible_class_iris.update(children) return possible_class_iris - def get_possible_enum_target_values(self, vocabulary: 'Vocabulary') -> \ - List[str]: + def get_possible_enum_target_values(self, vocabulary: "Vocabulary") -> List[str]: """Get all allowed enum target values for a data relation Args: @@ -495,9 +510,9 @@ def get_possible_enum_target_values(self, vocabulary: 'Vocabulary') -> \ targets: List[List[str]] = self.target_statement.get_all_targets() from .vocabulary import IdType + # methode only makes sense for data relations - if not vocabulary.is_id_of_type(self.property_iri, - IdType.data_property): + if not vocabulary.is_id_of_type(self.property_iri, IdType.data_property): return [] res = [] @@ -532,7 +547,7 @@ def get_all_target_iris(self) -> Set[str]: return iris - def export_rule(self, vocabulary: 'Vocabulary') -> (str, str): + def export_rule(self, vocabulary: "Vocabulary") -> (str, str): """Get the rule as string Args: @@ -549,8 +564,9 @@ def export_rule(self, vocabulary: 'Vocabulary') -> (str, str): new_list.append(vocabulary.get_label_for_entity_iri(iri)) if (int)(self.restriction_cardinality) > 0: - return f'"{self.restriction_type.value}|' \ - f'{self.restriction_cardinality}"', targets + return ( + f'"{self.restriction_type.value}|' f'{self.restriction_cardinality}"', + targets, + ) else: return f'"{self.restriction_type.value}"', targets - diff --git a/filip/semantics/vocabulary/source.py b/filip/semantics/vocabulary/source.py index 828214de..c6e74c2b 100644 --- a/filip/semantics/vocabulary/source.py +++ b/filip/semantics/vocabulary/source.py @@ -15,46 +15,43 @@ class DependencyStatement(BaseModel): """Information about one dependency statement in the source A dependency is a reference of one iri in an other entity definition """ + source_iri: str = Field( - default="", - description="Iri of the source containing the statement") + default="", description="Iri of the source containing the statement" + ) source_name: str = Field( - default="", - description="Name of the source containing the statement") + default="", description="Name of the source containing the statement" + ) type: str = Field( description="Possible types: Parent Class, Relation Property, " - "Relation Target") - class_iri: str = Field( - description="Iri of the class containing the statement") + "Relation Target" + ) + class_iri: str = Field(description="Iri of the class containing the statement") dependency_iri: str = Field(description="Entity Iri of the dependency") fulfilled: bool = Field( - description="True if the dependency_iri is registered in the " - "vocabulary") + description="True if the dependency_iri is registered in the " "vocabulary" + ) class ParsingError(BaseModel): """Object represents one issue that arose while parsing a source, - and holds all relevant details for that issue""" + and holds all relevant details for that issue""" + model_config = ConfigDict(use_enum_values=True) level: LogLevel = Field(description="Severity of error") - source_iri: str = Field(description= - "Iri of the source containing the error") + source_iri: str = Field(description="Iri of the source containing the error") source_name: Optional[str] = Field( - default=None, - description="Name of the source, only set in get_function" + default=None, description="Name of the source, only set in get_function" ) entity_type: str = Field( description="Type of the problematic entity: Class, Individual,.." - "ID_type in string form" + "ID_type in string form" ) entity_iri: str = Field(description="Iri of the problematic entity") entity_label: Optional[str] = Field( - default=None, - description="Name of the source, only set in get_function" - ) - message: str = Field( - description="Message describing the error" + default=None, description="Name of the source, only set in get_function" ) + message: str = Field(description="Message describing the error") class Source(BaseModel): @@ -64,33 +61,32 @@ class Source(BaseModel): vocabulary """ - id: str = Field(default="", - description="unique ID of the source; for internal use") - source_name: str = Field(default="", - description="Name of the source ") + id: str = Field(default="", description="unique ID of the source; for internal use") + source_name: str = Field(default="", description="Name of the source ") content: str = Field( - default="", - description="File content of the provided ontology file") - parsing_log: List['ParsingError'] = Field( + default="", description="File content of the provided ontology file" + ) + parsing_log: List["ParsingError"] = Field( default=[], - description="Log containing all issues that were discovered while " - "parsing") + description="Log containing all issues that were discovered while " "parsing", + ) dependency_statements: List[DependencyStatement] = Field( - default=[], - description="List of all statements in source") + default=[], description="List of all statements in source" + ) timestamp: datetime.datetime = Field( - description="timestamp when the source was added to the project") + description="timestamp when the source was added to the project" + ) ontology_iri: str = Field( - default=None, - description="Iri of the ontology of the source") + default=None, description="Iri of the ontology of the source" + ) predefined: bool = Field( default=False, description="Stating if the source is a predefined source; " - "a predefined source is added to each project containing " - "owl:Thing and predefined Datatypes") + "a predefined source is added to each project containing " + "owl:Thing and predefined Datatypes", + ) - def get_number_of_id_type(self, vocabulary: 'Vocabulary', - id_type: 'IdType') -> int: + def get_number_of_id_type(self, vocabulary: "Vocabulary", id_type: "IdType") -> int: """Get the number how many entities of a given type are created by or influenced by this source @@ -103,6 +99,7 @@ def get_number_of_id_type(self, vocabulary: 'Vocabulary', """ from . import IdType + id_func = "/" iri_list = [] @@ -140,7 +137,7 @@ def get_name(self) -> str: """ return self.source_name - def treat_dependency_statements(self, vocabulary: 'Vocabulary'): + def treat_dependency_statements(self, vocabulary: "Vocabulary"): """ Log and purge all pointers/iris in entities that are not contained in the vocabulary @@ -156,13 +153,15 @@ def treat_dependency_statements(self, vocabulary: 'Vocabulary'): for class_ in vocabulary.get_classes(): if self.id in class_.source_ids: dependency_statements.extend( - class_.treat_dependency_statements(vocabulary)) + class_.treat_dependency_statements(vocabulary) + ) for individual_iri in vocabulary.individuals: individual = vocabulary.get_individual(individual_iri) if self.id in individual.source_ids: dependency_statements.extend( - individual.treat_dependency_statements(vocabulary)) + individual.treat_dependency_statements(vocabulary) + ) for statement in dependency_statements: statement.source_iri = self.ontology_iri @@ -170,8 +169,9 @@ def treat_dependency_statements(self, vocabulary: 'Vocabulary'): self.dependency_statements = dependency_statements - def add_parsing_log_entry(self, level: 'LoggingLevel', entity_type: 'IdType', - entity_iri: str, msg: str): + def add_parsing_log_entry( + self, level: "LoggingLevel", entity_type: "IdType", entity_iri: str, msg: str + ): """ Add a parsing log entry for an entity, if an issue in parsing was discovered @@ -187,15 +187,18 @@ def add_parsing_log_entry(self, level: 'LoggingLevel', entity_type: 'IdType', """ from . import ParsingError - self.parsing_log.append(ParsingError( - level=level, - entity_type=str(entity_type), - entity_iri=entity_iri, - message=msg, - source_iri=self.ontology_iri - )) - - def get_parsing_log(self, vocabulary: 'Vocabulary') -> List['ParsingError']: + + self.parsing_log.append( + ParsingError( + level=level, + entity_type=str(entity_type), + entity_iri=entity_iri, + message=msg, + source_iri=self.ontology_iri, + ) + ) + + def get_parsing_log(self, vocabulary: "Vocabulary") -> List["ParsingError"]: """Get the Parsinglog, where the labels of the entities are filled in Args: @@ -223,5 +226,3 @@ def clear(self): """ self.parsing_log = [] self.dependency_statements = [] - - diff --git a/filip/semantics/vocabulary/vocabulary.py b/filip/semantics/vocabulary/vocabulary.py index 58f0be24..774631e6 100644 --- a/filip/semantics/vocabulary/vocabulary.py +++ b/filip/semantics/vocabulary/vocabulary.py @@ -14,29 +14,30 @@ class LabelSummary(BaseModel): """ Model holding all information for label conflicts in a vocabulary """ + class_label_duplicates: Dict[str, List[Entity]] = Field( description="All Labels that are used more than once for class_names " - "on export." - "Key: Label, Values: List of entities with key label" + "on export." + "Key: Label, Values: List of entities with key label" ) field_label_duplicates: Dict[str, List[Entity]] = Field( description="All Labels that are used more than once for property_names" - "on export." - "Key: Label, Values: List of entities with key label" + "on export." + "Key: Label, Values: List of entities with key label" ) datatype_label_duplicates: Dict[str, List[Entity]] = Field( description="All Labels that are used more than once for datatype " - "on export." - "Key: Label, Values: List of entities with key label" + "on export." + "Key: Label, Values: List of entities with key label" ) blacklisted_labels: List[Tuple[str, Entity]] = Field( description="All Labels that are blacklisted, " - "Tuple(Label, Entity with label)" + "Tuple(Label, Entity with label)" ) labels_with_illegal_chars: List[Tuple[str, Entity]] = Field( description="All Labels that contain illegal characters, " - "Tuple(Label, Entity with label)" + "Tuple(Label, Entity with label)" ) def is_valid(self) -> bool: @@ -45,11 +46,13 @@ def is_valid(self) -> bool: Returns: bool, True if no entries exist """ - return len(self.class_label_duplicates) == 0 and \ - len(self.field_label_duplicates) == 0 and \ - len(self.datatype_label_duplicates) == 0 and \ - len(self.blacklisted_labels) == 0 and \ - len(self.labels_with_illegal_chars) == 0 + return ( + len(self.class_label_duplicates) == 0 + and len(self.field_label_duplicates) == 0 + and len(self.datatype_label_duplicates) == 0 + and len(self.blacklisted_labels) == 0 + and len(self.labels_with_illegal_chars) == 0 + ) def __str__(self): res = "" @@ -90,14 +93,15 @@ def print_list(collection): class IdType(str, Enum): """Type of object that is referenced by an id/iri""" - class_ = 'Class' - object_property = 'Object Property' - data_property = 'Data Property' - datatype = 'Datatype' - relation = 'Relation' - combined_relation = 'Combined Relation' - individual = 'Individual' - source = 'Source' + + class_ = "Class" + object_property = "Object Property" + data_property = "Data Property" + datatype = "Datatype" + relation = "Relation" + combined_relation = "Combined Relation" + individual = "Individual" + source = "Source" class VocabularySettings(BaseModel): @@ -105,30 +109,31 @@ class VocabularySettings(BaseModel): Settings that state how labels of ontology entities should be automatically converted on parsing """ + pascal_case_class_labels: bool = Field( default=True, description="If true, convert all class labels given in the ontologies " - "to PascalCase" + "to PascalCase", ) pascal_case_individual_labels: bool = Field( default=True, description="If true, convert all labels of individuals given in the " - "ontologies to PascalCase" + "ontologies to PascalCase", ) camel_case_property_labels: bool = Field( default=True, description="If true, convert all labels of properties given in the " - "ontologies to camelCase" + "ontologies to camelCase", ) camel_case_datatype_labels: bool = Field( default=True, description="If true, convert all labels of datatypes given in the " - "ontologies to camelCase" + "ontologies to camelCase", ) pascal_case_datatype_enum_labels: bool = Field( default=True, description="If true, convert all values of enum datatypes given in " - "the to PascalCase" + "the to PascalCase", ) @@ -148,53 +153,59 @@ class Vocabulary(BaseModel): """ classes: Dict[str, Class] = Field( - default={}, - description="Classes of the vocabulary. Key: class_iri") + default={}, description="Classes of the vocabulary. Key: class_iri" + ) object_properties: Dict[str, ObjectProperty] = Field( default={}, - description="ObjectProperties of the vocabulary. " - "Key: object_property_iri") + description="ObjectProperties of the vocabulary. " "Key: object_property_iri", + ) data_properties: Dict[str, DataProperty] = Field( default={}, - description="DataProperties of the vocabulary. Key: data_property_iri") + description="DataProperties of the vocabulary. Key: data_property_iri", + ) datatypes: Dict[str, Datatype] = Field( - default={}, - description="Datatypes of the vocabulary. Key: datatype_iri") + default={}, description="Datatypes of the vocabulary. Key: datatype_iri" + ) individuals: Dict[str, Individual] = Field( - default={}, - description="Individuals in the vocabulary. Key: individual_iri") + default={}, description="Individuals in the vocabulary. Key: individual_iri" + ) relations: Dict[str, Relation] = Field( default={}, - description="Relations of classes in the vocabulary. Key: relation_id") + description="Relations of classes in the vocabulary. Key: relation_id", + ) combined_object_relations: Dict[str, CombinedObjectRelation] = Field( default={}, description="CombinedObjectRelations of classes in the vocabulary." - " Key: combined_relation_id") + " Key: combined_relation_id", + ) combined_data_relations: Dict[str, CombinedDataRelation] = Field( default={}, description="CombinedDataRelations of classes in the vocabulary." - "Key: combined_data_id") + "Key: combined_data_id", + ) sources: Dict[str, Source] = Field( - default={}, - description="Sources of the vocabulary. Key: source_id") + default={}, description="Sources of the vocabulary. Key: source_id" + ) id_types: Dict[str, IdType] = Field( default={}, description="Maps all entity iris and (combined)relations to their " - "Entity/Object type, to speed up lookups") + "Entity/Object type, to speed up lookups", + ) original_label_summary: Optional[LabelSummary] = Field( default=None, - description="Original label after parsing, before the user made " - "changes") + description="Original label after parsing, before the user made " "changes", + ) settings: VocabularySettings = Field( default=VocabularySettings(), - description="Settings how to auto transform the entity labels") + description="Settings how to auto transform the entity labels", + ) - def get_type_of_id(self, id: str) -> Union[IdType,None]: + def get_type_of_id(self, id: str) -> Union[IdType, None]: """Get the type (class, relation,...) of an iri/id Args: @@ -263,7 +274,7 @@ def iri_is_predefined_datatype(self, iri: str) -> bool: if self.id_types[iri] is IdType.datatype: return self.get_datatype(iri).predefined - def get_datatype(self, datatype_iri:str) -> Datatype: + def get_datatype(self, datatype_iri: str) -> Datatype: """Get the datatype belonging to the iri Args: @@ -334,15 +345,14 @@ def get_classes(self) -> List[Class]: def get_classes_sorted_by_label(self) -> List[Class]: """Get all classes sorted by their labels - Returns: - List[Class]: sorted classes, ascending + Returns: + List[Class]: sorted classes, ascending """ - return sorted(self.classes.values(), - key=operator.methodcaller("get_label"), - reverse=False) + return sorted( + self.classes.values(), key=operator.methodcaller("get_label"), reverse=False + ) - def get_entity_list_sorted_by_label(self, list: List[Entity]) \ - -> List[Entity]: + def get_entity_list_sorted_by_label(self, list: List[Entity]) -> List[Entity]: """Sort a given entity list by their labels Args: @@ -350,8 +360,7 @@ def get_entity_list_sorted_by_label(self, list: List[Entity]) \ Returns: List[Entity]: sorted list """ - return sorted(list, key=operator.methodcaller("get_label"), - reverse=False) + return sorted(list, key=operator.methodcaller("get_label"), reverse=False) def get_object_properties_sorted_by_label(self) -> List[ObjectProperty]: """Get all object properties of the vocabulary sorted by their labels @@ -359,8 +368,11 @@ def get_object_properties_sorted_by_label(self) -> List[ObjectProperty]: Returns: List[ObjectProperty], sorted by ascending labels """ - return sorted(self.object_properties.values(), - key=operator.methodcaller("get_label"), reverse=False) + return sorted( + self.object_properties.values(), + key=operator.methodcaller("get_label"), + reverse=False, + ) def get_data_properties_sorted_by_label(self) -> List[DataProperty]: """Get all data properties of the vocabulary sorted by their labels @@ -368,8 +380,11 @@ def get_data_properties_sorted_by_label(self) -> List[DataProperty]: Returns: List[DataProperty], sorted by ascending labels """ - return sorted(self.data_properties.values(), - key=operator.methodcaller("get_label"), reverse=False) + return sorted( + self.data_properties.values(), + key=operator.methodcaller("get_label"), + reverse=False, + ) def get_individuals_sorted_by_label(self) -> List[Individual]: """Get all individuals of the vocabulary sorted by their labels @@ -377,8 +392,11 @@ def get_individuals_sorted_by_label(self) -> List[Individual]: Returns: List[Individual], sorted by ascending labels """ - return sorted(self.individuals.values(), - key=operator.methodcaller("get_label"), reverse=False) + return sorted( + self.individuals.values(), + key=operator.methodcaller("get_label"), + reverse=False, + ) def get_datatypes_sorted_by_label(self) -> List[Datatype]: """Get all datatypes of the vocabulary sorted by their labels @@ -386,8 +404,11 @@ def get_datatypes_sorted_by_label(self) -> List[Datatype]: Returns: List[Datatype], sorted by ascending labels """ - return sorted(self.datatypes.values(), - key=operator.methodcaller("get_label"), reverse=False) + return sorted( + self.datatypes.values(), + key=operator.methodcaller("get_label"), + reverse=False, + ) def get_relation_by_id(self, id: str) -> Relation: """Get Relation by relation id @@ -435,8 +456,7 @@ def get_combined_data_relation_by_id(self, id: str) -> CombinedDataRelation: """ return self.combined_data_relations[id] - def get_combined_object_relation_by_id(self, id: str)\ - -> CombinedObjectRelation: + def get_combined_object_relation_by_id(self, id: str) -> CombinedObjectRelation: """Get CombinedObjectRelation by id Args: @@ -603,12 +623,13 @@ def get_all_entities(self) -> List[Entity]: Returns: List[Entity] """ - lists = [self.classes.values(), - self.object_properties.values(), - self.data_properties.values(), - self.datatypes.values(), - self.individuals.values() - ] + lists = [ + self.classes.values(), + self.object_properties.values(), + self.data_properties.values(), + self.datatypes.values(), + self.individuals.values(), + ] res = [] for l in lists: @@ -621,5 +642,8 @@ def get_enum_dataytypes(self) -> Dict[str, Datatype]: Returns: Dict[str, Datatype], {datatype.iri: Datatype} """ - return {datatype.iri: datatype for datatype in self.datatypes.values() - if len(datatype.enum_values) > 0 and not datatype.predefined} + return { + datatype.iri: datatype + for datatype in self.datatypes.values() + if len(datatype.enum_values) > 0 and not datatype.predefined + } diff --git a/filip/semantics/vocabulary_configurator.py b/filip/semantics/vocabulary_configurator.py index 5462da18..ec23b7d2 100644 --- a/filip/semantics/vocabulary_configurator.py +++ b/filip/semantics/vocabulary_configurator.py @@ -15,29 +15,55 @@ from filip.semantics.ontology_parser.post_processer import PostProcessor from filip.semantics.ontology_parser.rdfparser import RdfParser -from filip.semantics.vocabulary import \ - LabelSummary, \ - Vocabulary, \ - Source, \ - Entity, \ - RestrictionType, \ - Class, \ - ParsingError, \ - CombinedRelation, \ - DataFieldType, \ - DependencyStatement, \ - VocabularySettings +from filip.semantics.vocabulary import ( + LabelSummary, + Vocabulary, + Source, + Entity, + RestrictionType, + Class, + ParsingError, + CombinedRelation, + DataFieldType, + DependencyStatement, + VocabularySettings, +) # Blacklist containing all labels that are forbidden for entities to have label_blacklist = list(keyword.kwlist) label_blacklist.extend(["referencedBy", "deviceSettings"]) -label_blacklist.extend(["references", "device_settings", "header", - "old_state", "", "semantic_manager", "delete", - "metadata"]) +label_blacklist.extend( + [ + "references", + "device_settings", + "header", + "old_state", + "", + "semantic_manager", + "delete", + "metadata", + ] +) label_blacklist.extend(["id", "type", "class"]) -label_blacklist.extend(["str", "int", "float", "complex", "list", "tuple", - "range", "dict", "list", "set", "frozenset", "bool", - "bytes", "bytearray", "memoryview"]) +label_blacklist.extend( + [ + "str", + "int", + "float", + "complex", + "list", + "tuple", + "range", + "dict", + "list", + "set", + "frozenset", + "bool", + "bytes", + "bytearray", + "memoryview", + ] +) # Whitelist containing all chars that an entity label can consist of label_char_whitelist = ascii_letters + digits + "_" @@ -50,9 +76,9 @@ class VocabularyConfigurator: """ @classmethod - def create_vocabulary(cls, - settings: VocabularySettings = VocabularySettings()) \ - -> Vocabulary: + def create_vocabulary( + cls, settings: VocabularySettings = VocabularySettings() + ) -> Vocabulary: """ Create a new blank vocabulary with given settings @@ -66,8 +92,9 @@ def create_vocabulary(cls, return Vocabulary(settings=settings) @classmethod - def delete_source_from_vocabulary(cls, vocabulary: Vocabulary, - source_id: str) -> Vocabulary: + def delete_source_from_vocabulary( + cls, vocabulary: Vocabulary, source_id: str + ) -> Vocabulary: """ Delete a source from the vocabulary @@ -88,28 +115,28 @@ def delete_source_from_vocabulary(cls, vocabulary: Vocabulary, for source in vocabulary.sources.values(): if not source_id == source.id: parser.parse_source_into_vocabulary( - source=copy.deepcopy(source), vocabulary=new_vocabulary) + source=copy.deepcopy(source), vocabulary=new_vocabulary + ) else: found = True PostProcessor.post_process_vocabulary( - vocabulary=new_vocabulary, old_vocabulary=vocabulary) + vocabulary=new_vocabulary, old_vocabulary=vocabulary + ) if not found: - raise ValueError( - f"Source with source_id {source_id} not in vocabulary") + raise ValueError(f"Source with source_id {source_id} not in vocabulary") PostProcessor.transfer_settings( - new_vocabulary=new_vocabulary, old_vocabulary=vocabulary) + new_vocabulary=new_vocabulary, old_vocabulary=vocabulary + ) return new_vocabulary @classmethod def add_ontology_to_vocabulary_as_link( - cls, - vocabulary: Vocabulary, - link: str, - source_name: Optional[str] = None) -> Vocabulary: + cls, vocabulary: Vocabulary, link: str, source_name: Optional[str] = None + ) -> Vocabulary: """ Add a source to the vocabulary with via a weblink. Source name will be extracted from link, if no name is given @@ -133,18 +160,19 @@ def add_ontology_to_vocabulary_as_link( if source_name is None: source_name = wget.filename_from_url(link) - file_str = io.TextIOWrapper(file_bytes, encoding='utf-8').read() + file_str = io.TextIOWrapper(file_bytes, encoding="utf-8").read() - return cls.add_ontology_to_vocabulary_as_string(vocabulary=vocabulary, - source_name=source_name, - source_content=file_str) + return cls.add_ontology_to_vocabulary_as_string( + vocabulary=vocabulary, source_name=source_name, source_content=file_str + ) @classmethod def add_ontology_to_vocabulary_as_file( - cls, - vocabulary: Vocabulary, - path_to_file: str, - source_name: Optional[str] = None) -> Vocabulary: + cls, + vocabulary: Vocabulary, + path_to_file: str, + source_name: Optional[str] = None, + ) -> Vocabulary: """ Add a source to the vocabulary with via a file path. Source name will be extracted from path, if no name is given @@ -163,23 +191,22 @@ def add_ontology_to_vocabulary_as_file( New Vocabulary with the given source added to it """ - with open(path_to_file, 'r') as file: + with open(path_to_file, "r") as file: data = file.read() if source_name is None: source_name = os.path.basename(path_to_file).split(".")[0] - source = Source(source_name=source_name, - content=data, - timestamp=datetime.now()) + source = Source(source_name=source_name, content=data, timestamp=datetime.now()) return VocabularyConfigurator._parse_sources_into_vocabulary( - vocabulary=vocabulary, sources=[source]) + vocabulary=vocabulary, sources=[source] + ) @classmethod - def add_ontology_to_vocabulary_as_string(cls, vocabulary: Vocabulary, - source_name: str, - source_content: str) -> Vocabulary: + def add_ontology_to_vocabulary_as_string( + cls, vocabulary: Vocabulary, source_name: str, source_content: str + ) -> Vocabulary: """ Add a source to the vocabulary by giving the source content as string. Source name needs to be given @@ -197,16 +224,18 @@ def add_ontology_to_vocabulary_as_string(cls, vocabulary: Vocabulary, Returns: New Vocabulary with the given source added to it """ - source = Source(source_name=source_name, - content=source_content, - timestamp=datetime.now()) + source = Source( + source_name=source_name, content=source_content, timestamp=datetime.now() + ) return VocabularyConfigurator._parse_sources_into_vocabulary( - vocabulary=vocabulary, sources=[source]) + vocabulary=vocabulary, sources=[source] + ) @classmethod - def _parse_sources_into_vocabulary(cls, vocabulary: Vocabulary, - sources: List[Source]) -> Vocabulary: + def _parse_sources_into_vocabulary( + cls, vocabulary: Vocabulary, sources: List[Source] + ) -> Vocabulary: """ Parse the given source objects into the vocabulary @@ -229,16 +258,19 @@ def _parse_sources_into_vocabulary(cls, vocabulary: Vocabulary, for source in vocabulary.sources.values(): source_copy = copy.deepcopy(source) source_copy.clear() - parser.parse_source_into_vocabulary(source=source_copy, - vocabulary=new_vocabulary) + parser.parse_source_into_vocabulary( + source=source_copy, vocabulary=new_vocabulary + ) # try to parse in the new sources and post_process try: for source in sources: - parser.parse_source_into_vocabulary(source=source, - vocabulary=new_vocabulary) + parser.parse_source_into_vocabulary( + source=source, vocabulary=new_vocabulary + ) PostProcessor.post_process_vocabulary( - vocabulary=new_vocabulary, old_vocabulary=vocabulary) + vocabulary=new_vocabulary, old_vocabulary=vocabulary + ) except Exception as e: raise ParsingException(e.args) @@ -272,8 +304,7 @@ def is_label_illegal(cls, label: str) -> bool: return False @classmethod - def get_label_conflicts_in_vocabulary(cls, vocabulary: Vocabulary) -> \ - LabelSummary: + def get_label_conflicts_in_vocabulary(cls, vocabulary: Vocabulary) -> LabelSummary: """ Compute a summary for all labels present in the vocabulary. The summary contains all naming clashes and illegal labels. @@ -333,21 +364,33 @@ def get_illegal_labels(entities_to_check: List[Dict]): summary = LabelSummary( class_label_duplicates=get_conflicts_in_group( - [vocabulary.classes, vocabulary.individuals, - vocabulary.get_enum_dataytypes()]), + [ + vocabulary.classes, + vocabulary.individuals, + vocabulary.get_enum_dataytypes(), + ] + ), field_label_duplicates=get_conflicts_in_group( - [vocabulary.data_properties, vocabulary.object_properties]), - datatype_label_duplicates=get_conflicts_in_group( - [vocabulary.datatypes]), - blacklisted_labels=get_blacklisted_labels([ - vocabulary.classes, vocabulary.individuals, - vocabulary.data_properties, vocabulary.object_properties - ]), - labels_with_illegal_chars=get_illegal_labels([ - vocabulary.classes, vocabulary.individuals, - vocabulary.data_properties, vocabulary.object_properties, - vocabulary.datatypes - ]), + [vocabulary.data_properties, vocabulary.object_properties] + ), + datatype_label_duplicates=get_conflicts_in_group([vocabulary.datatypes]), + blacklisted_labels=get_blacklisted_labels( + [ + vocabulary.classes, + vocabulary.individuals, + vocabulary.data_properties, + vocabulary.object_properties, + ] + ), + labels_with_illegal_chars=get_illegal_labels( + [ + vocabulary.classes, + vocabulary.individuals, + vocabulary.data_properties, + vocabulary.object_properties, + vocabulary.datatypes, + ] + ), ) return summary @@ -365,11 +408,13 @@ def is_vocabulary_valid(cls, vocabulary: Vocabulary) -> bool: bool """ return VocabularyConfigurator.get_label_conflicts_in_vocabulary( - vocabulary).is_valid() + vocabulary + ).is_valid() @classmethod - def get_missing_dependency_statements(cls, vocabulary: Vocabulary) -> \ - List[DependencyStatement]: + def get_missing_dependency_statements( + cls, vocabulary: Vocabulary + ) -> List[DependencyStatement]: """ Get a list of all Dependencies that are currently missing in the vocabulary in form of DependencyStatements @@ -425,12 +470,12 @@ def get_parsing_logs(cls, vocabulary: Vocabulary) -> List[ParsingError]: @classmethod def generate_vocabulary_models( - cls, - vocabulary: Vocabulary, - path: Optional[str] = None, - filename: Optional[str] = None, - alternative_manager_name: Optional[str] = None) -> \ - Optional[str]: + cls, + vocabulary: Vocabulary, + path: Optional[str] = None, + filename: Optional[str] = None, + alternative_manager_name: Optional[str] = None, + ) -> Optional[str]: """ Export the given vocabulary as python model file. All vocabulary classes will be converted to python classes, @@ -461,7 +506,7 @@ def generate_vocabulary_models( "prevented the generation of models. Check for conflicts with: " "VocabularyConfigurator." "get_label_conflicts_in_vocabulary(vocabulary)" - ) + ) def split_string_into_lines(string: str, limit: int) -> [str]: """Helper methode, takes a long string and splits it into @@ -481,9 +526,9 @@ def split_string_into_lines(string: str, limit: int) -> [str]: for char in string: if char == " ": last_space_index = current_index - if current_index-last_split_index > limit: - result.append(string[last_split_index: last_space_index]) - last_split_index = last_space_index+1 + if current_index - last_split_index > limit: + result.append(string[last_split_index:last_space_index]) + last_split_index = last_space_index + 1 current_index += 1 # add the remaining part, if the last character of the string was @@ -492,29 +537,35 @@ def split_string_into_lines(string: str, limit: int) -> [str]: result.append(string[last_split_index:current_index]) return result - content: str = '"""\nAutogenerated Models for the vocabulary ' \ - 'described ' \ - 'by the ontologies:\n' + content: str = ( + '"""\nAutogenerated Models for the vocabulary ' + "described " + "by the ontologies:\n" + ) for source in vocabulary.sources.values(): if not source.predefined: - content += f'\t{source.ontology_iri} ({source.source_name})\n' + content += f"\t{source.ontology_iri} ({source.source_name})\n" content += '"""\n\n' # imports content += "from enum import Enum\n" content += "from typing import Dict, Union, List\n" - content += "from filip.semantics.semantics_models import\\" \ - "\n\tSemanticClass,\\" \ - "\n\tSemanticIndividual,\\" \ - "\n\tRelationField,\\" \ - "\n\tDataField,\\" \ - "\n\tSemanticDeviceClass,\\" \ - "\n\tDeviceAttributeField,\\" \ - "\n\tCommandField" + content += ( + "from filip.semantics.semantics_models import\\" + "\n\tSemanticClass,\\" + "\n\tSemanticIndividual,\\" + "\n\tRelationField,\\" + "\n\tDataField,\\" + "\n\tSemanticDeviceClass,\\" + "\n\tDeviceAttributeField,\\" + "\n\tCommandField" + ) content += "\n" - content += "from filip.semantics.semantics_manager import\\" \ - "\n\tSemanticsManager,\\" \ - "\n\tInstanceRegistry" + content += ( + "from filip.semantics.semantics_manager import\\" + "\n\tSemanticsManager,\\" + "\n\tInstanceRegistry" + ) content += "\n\n\n" content += f"semantic_manager: SemanticsManager = SemanticsManager(" @@ -570,9 +621,9 @@ def split_string_into_lines(string: str, limit: int) -> [str]: child = vocabulary.classes[child_iri] # all parents added, add child to queue - if len([p for p in child.parent_class_iris - if p in added_class_iris]) == len( - child.parent_class_iris): + if len( + [p for p in child.parent_class_iris if p in added_class_iris] + ) == len(child.parent_class_iris): if not child_iri in added_class_iris: iri_queue.append(child_iri) @@ -582,21 +633,20 @@ def split_string_into_lines(string: str, limit: int) -> [str]: content += "\n\n\n" # Parent Classes parent_class_string = "" - parents = class_.get_parent_classes(vocabulary, - remove_redundancy=True) + parents = class_.get_parent_classes(vocabulary, remove_redundancy=True) # Device Class, only add if this is a device class and it was not # added for a parent if class_.is_iot_class(vocabulary): - if True not in [p.is_iot_class(vocabulary) for p in - parents]: + if True not in [p.is_iot_class(vocabulary) for p in parents]: parent_class_string = " ,SemanticDeviceClass" for parent in parents: parent_class_string += f", {parent.get_label()}" parent_class_string = parent_class_string[ - 2:] # remove first comma and space + 2: + ] # remove first comma and space if parent_class_string == "": parent_class_string = "SemanticClass" @@ -612,8 +662,10 @@ def split_string_into_lines(string: str, limit: int) -> [str]: content += f"Source(s): \n\t\t" for source_id in class_.source_ids: - content += f"{vocabulary.sources[source_id].ontology_iri} " \ - f"({vocabulary.sources[source_id].source_name})" + content += ( + f"{vocabulary.sources[source_id].ontology_iri} " + f"({vocabulary.sources[source_id].source_name})" + ) content += f'\n\t"""' # ------Constructors------ @@ -649,26 +701,31 @@ def split_string_into_lines(string: str, limit: int) -> [str]: for cdr in class_.get_combined_data_relations(vocabulary): if not cdr.is_device_relation(vocabulary): content += "\n\t\t\t" - content += \ - f"self." \ - f"{cdr.get_property_label(vocabulary)}._rules = " \ + content += ( + f"self." + f"{cdr.get_property_label(vocabulary)}._rules = " f"{cdr.export_rule(vocabulary, stringify_fields=True)}" + ) if len(class_.get_combined_object_relations(vocabulary)) > 0: content += "\n" for cor in class_.get_combined_object_relations(vocabulary): content += "\n\t\t\t" - content += f"self." \ - f"{cor.get_property_label(vocabulary)}._rules = " \ - f"{cor.export_rule(vocabulary, stringify_fields=False)}" + content += ( + f"self." + f"{cor.get_property_label(vocabulary)}._rules = " + f"{cor.export_rule(vocabulary, stringify_fields=False)}" + ) if len(class_.get_combined_relations(vocabulary)) > 0: content += "\n" for cr in class_.get_combined_relations(vocabulary): content += "\n\t\t\t" - content += f"self.{cr.get_property_label(vocabulary)}" \ - f"._instance_identifier = " \ - f"self.get_identifier()" + content += ( + f"self.{cr.get_property_label(vocabulary)}" + f"._instance_identifier = " + f"self.get_identifier()" + ) # ------Add preset Values------ for cdr in class_.get_combined_data_relations(vocabulary): @@ -682,11 +739,12 @@ def split_string_into_lines(string: str, limit: int) -> [str]: # changed them if rel.restriction_type == RestrictionType.value: content += "\n\t\t\t" - content += \ - f"self." \ - f"{cdr.get_property_label(vocabulary)}" \ - f".add(" \ + content += ( + f"self." + f"{cdr.get_property_label(vocabulary)}" + f".add(" f"'{rel.target_statement.target_data_value}')" + ) if len(class_.get_combined_object_relations(vocabulary)) > 0: content += "\n" @@ -696,14 +754,14 @@ def split_string_into_lines(string: str, limit: int) -> [str]: # Only add the statement on the uppermost occurring class for rel in cor.get_relations(vocabulary): if rel.id in class_.relation_ids: - i = vocabulary. \ - get_label_for_entity_iri( - rel.get_targets()[0][0]) + i = vocabulary.get_label_for_entity_iri(rel.get_targets()[0][0]) if rel.restriction_type == RestrictionType.value: content += "\n\t\t\t" - content += f"self." \ - f"{cor.get_property_label(vocabulary)}" \ - f".add({i}())" + content += ( + f"self." + f"{cor.get_property_label(vocabulary)}" + f".add({i}())" + ) # if no content was added af the not initialised if, removed it # again, and its preceding \n @@ -720,7 +778,7 @@ def build_field_comment(cr: CombinedRelation) -> str: if comment != "": res += f'\n\t"""' for line in split_string_into_lines(comment, 75): - res += f'\n\t{line}' + res += f"\n\t{line}" res += f'\n\t"""' return res @@ -737,9 +795,10 @@ def build_field_comment(cr: CombinedRelation) -> str: content += "\n\t\t" content += f"name='{label}'," content += "\n\t\t" - content += \ - f"rule='" \ + content += ( + f"rule='" f"{cdr.get_all_targetstatements_as_string(vocabulary)}'," + ) content += "\n\t\t" content += f"semantic_manager=semantic_manager)" content += build_field_comment(cdr) @@ -757,8 +816,9 @@ def build_field_comment(cr: CombinedRelation) -> str: elif cdr_type == DataFieldType.device_attribute: content += "\n\n\t" label = cdr.get_property_label(vocabulary) - content += f"{label}: DeviceAttributeField " \ - f"= DeviceAttributeField(" + content += ( + f"{label}: DeviceAttributeField " f"= DeviceAttributeField(" + ) content += "\n\t\t" content += f"name='{label}'," content += "\n\t\t" @@ -776,8 +836,9 @@ def build_field_comment(cr: CombinedRelation) -> str: content += "\n\t\t" content += f"name='{label}'," content += "\n\t\t" - content += f"rule='" \ - f"{cor.get_all_targetstatements_as_string(vocabulary)}'," + content += ( + f"rule='" f"{cor.get_all_targetstatements_as_string(vocabulary)}'," + ) content += "\n\t\t" if not len(cor.get_inverse_of_labels(vocabulary)) == 0: content += "inverse_of=" @@ -859,7 +920,7 @@ def build_field_comment(cr: CombinedRelation) -> str: else: path = pathlib.Path(path).joinpath(filename).with_suffix(".py") - with open(path, "w", encoding ="utf-8") as text_file: + with open(path, "w", encoding="utf-8") as text_file: text_file.write(content) diff --git a/filip/utils/__init__.py b/filip/utils/__init__.py index 8659c59a..e1d93937 100644 --- a/filip/utils/__init__.py +++ b/filip/utils/__init__.py @@ -1,7 +1,9 @@ """ Utility module for helper functions """ + from .validators import validate_http_url, validate_mqtt_url -from .datetime import \ - convert_datetime_to_iso_8601_with_z_suffix, \ - transform_to_utc_datetime +from .datetime import ( + convert_datetime_to_iso_8601_with_z_suffix, + transform_to_utc_datetime, +) diff --git a/filip/utils/cleanup.py b/filip/utils/cleanup.py index 764d9762..f7b0439e 100644 --- a/filip/utils/cleanup.py +++ b/filip/utils/cleanup.py @@ -1,6 +1,7 @@ """ Functions to clean up a tenant within a fiware based platform. """ + import warnings from functools import wraps @@ -8,10 +9,7 @@ from requests import RequestException from typing import Callable, List, Union from filip.models import FiwareHeader, FiwareLDHeader -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient, QuantumLeapClient from filip.clients.ngsi_ld.cb import ContextBrokerLDClient from filip.models.ngsi_ld.context import ActionTypeLD import logging @@ -20,10 +18,11 @@ logger.setLevel(logging.DEBUG) -def clear_context_broker_ld(url: str = None, - fiware_ld_header: FiwareLDHeader = None, - cb_ld_client: ContextBrokerLDClient = None - ): +def clear_context_broker_ld( + url: str = None, + fiware_ld_header: FiwareLDHeader = None, + cb_ld_client: ContextBrokerLDClient = None, +): """ Function deletes all entities and subscriptions for a tenant in an LD context broker. @@ -47,8 +46,9 @@ def clear_context_broker_ld(url: str = None, while entity_list: entity_list = client.get_entity_list(limit=100) if entity_list: - client.entity_batch_operation(action_type=ActionTypeLD.DELETE, - entities=entity_list) + client.entity_batch_operation( + action_type=ActionTypeLD.DELETE, entities=entity_list + ) except RequestException as e: logger.warning("Could not clean entities completely") raise @@ -63,11 +63,12 @@ def clear_context_broker_ld(url: str = None, raise -def clear_context_broker(url: str=None, - fiware_header: FiwareHeader=None, - clear_registrations: bool = False, - cb_client: ContextBrokerClient = None - ): +def clear_context_broker( + url: str = None, + fiware_header: FiwareHeader = None, + clear_registrations: bool = False, + cb_client: ContextBrokerClient = None, +): """ Function deletes all entities, registrations and subscriptions for a given fiware header. To use TLS connection you need to provide the cb_client parameter @@ -111,10 +112,11 @@ def clear_context_broker(url: str=None, assert len(client.get_subscription_list()) == 0 - -def clear_iot_agent(url: Union[str, AnyHttpUrl] = None, - fiware_header: FiwareHeader = None, - iota_client: IoTAClient = None): +def clear_iot_agent( + url: Union[str, AnyHttpUrl] = None, + fiware_header: FiwareHeader = None, + iota_client: IoTAClient = None, +): """ Function deletes all device groups and devices for a given fiware header. To use TLS connection you need to provide the iota_client parameter @@ -142,14 +144,15 @@ def clear_iot_agent(url: Union[str, AnyHttpUrl] = None, # clear groups for group in client.get_group_list(): - client.delete_group(resource=group.resource, - apikey=group.apikey) + client.delete_group(resource=group.resource, apikey=group.apikey) assert len(client.get_group_list()) == 0 -def clear_quantumleap(url: str = None, - fiware_header: FiwareHeader = None, - ql_client: QuantumLeapClient = None): +def clear_quantumleap( + url: str = None, + fiware_header: FiwareHeader = None, + ql_client: QuantumLeapClient = None, +): """ Function deletes all data for a given fiware header. To use TLS connection you need to provide the ql_client parameter as an argument with the Session object including the certificate and private key. @@ -161,6 +164,7 @@ def clear_quantumleap(url: str = None, Returns: None """ + def handle_emtpy_db_exception(err: RequestException) -> None: """ When the database is empty for request quantumleap returns a 404 @@ -170,11 +174,14 @@ def handle_emtpy_db_exception(err: RequestException) -> None: Args: err: exception raised by delete function """ - if err.response.status_code == 404 \ - and err.response.json().get('error', None) == 'Not Found': + if ( + err.response.status_code == 404 + and err.response.json().get("error", None) == "Not Found" + ): pass else: raise + assert url or ql_client, "Either url or client object must be given" # create client if ql_client is None: @@ -191,18 +198,19 @@ def handle_emtpy_db_exception(err: RequestException) -> None: # will be executed for all found entities for entity in entities: - client.delete_entity(entity_id=entity.entityId, - entity_type=entity.entityType) - - -def clear_all(*, - fiware_header: FiwareHeader = None, - cb_url: str = None, - iota_url: Union[str, List[str]] = None, - ql_url: str = None, - cb_client: ContextBrokerClient = None, - iota_client: IoTAClient = None, - ql_client: QuantumLeapClient = None): + client.delete_entity(entity_id=entity.entityId, entity_type=entity.entityType) + + +def clear_all( + *, + fiware_header: FiwareHeader = None, + cb_url: str = None, + iota_url: Union[str, List[str]] = None, + ql_url: str = None, + cb_client: ContextBrokerClient = None, + iota_client: IoTAClient = None, + ql_client: QuantumLeapClient = None +): """ Clears all services that a url is provided for. If cb_url is provided, the registration will also be deleted. @@ -236,22 +244,28 @@ def clear_all(*, clear_iot_agent(url=url, fiware_header=fiware_header) if cb_url is not None or cb_client is not None: - clear_context_broker(url=cb_url, fiware_header=fiware_header, cb_client=cb_client, - clear_registrations=True) + clear_context_broker( + url=cb_url, + fiware_header=fiware_header, + cb_client=cb_client, + clear_registrations=True, + ) if ql_url is not None or ql_client is not None: clear_quantumleap(url=ql_url, fiware_header=fiware_header, ql_client=ql_client) -def clean_test(*, - fiware_service: str, - fiware_servicepath: str, - cb_url: str = None, - iota_url: Union[str, List[str]] = None, - ql_url: str = None, - cb_client: ContextBrokerClient = None, - iota_client: IoTAClient = None, - ql_client: QuantumLeapClient = None) -> Callable: +def clean_test( + *, + fiware_service: str, + fiware_servicepath: str, + cb_url: str = None, + iota_url: Union[str, List[str]] = None, + ql_url: str = None, + cb_client: ContextBrokerClient = None, + iota_client: IoTAClient = None, + ql_client: QuantumLeapClient = None +) -> Callable: """ Decorator to clean up the server before and after the test @@ -274,29 +288,36 @@ def clean_test(*, Returns: Decorator for clean tests """ - fiware_header = FiwareHeader(service=fiware_service, - service_path=fiware_servicepath) - clear_all(fiware_header=fiware_header, - cb_url=cb_url, - iota_url=iota_url, - ql_url=ql_url, - cb_client=cb_client, - iota_client=iota_client, - ql_client=ql_client) + fiware_header = FiwareHeader( + service=fiware_service, service_path=fiware_servicepath + ) + clear_all( + fiware_header=fiware_header, + cb_url=cb_url, + iota_url=iota_url, + ql_url=ql_url, + cb_client=cb_client, + iota_client=iota_client, + ql_client=ql_client, + ) + # Inner decorator function def decorator(func): # Wrapper function for the decorated function @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) - return wrapper - clear_all(fiware_header=fiware_header, - cb_url=cb_url, - iota_url=iota_url, - ql_url=ql_url, - cb_client=cb_client, - iota_client=iota_client, - ql_client=ql_client) + return wrapper - return decorator \ No newline at end of file + clear_all( + fiware_header=fiware_header, + cb_url=cb_url, + iota_url=iota_url, + ql_url=ql_url, + cb_client=cb_client, + iota_client=iota_client, + ql_client=ql_client, + ) + + return decorator diff --git a/filip/utils/data.py b/filip/utils/data.py index bff76f11..fc2ba446 100644 --- a/filip/utils/data.py +++ b/filip/utils/data.py @@ -29,7 +29,7 @@ def load_datapackage(url: str, package_name: str) -> Dict[str, pd.DataFrame]: # create directory for data if not exists validate_http_url(url=url) - path = Path(__file__).parent.parent.absolute().joinpath('data') + path = Path(__file__).parent.parent.absolute().joinpath("data") path.mkdir(parents=True, exist_ok=True) package_path = path.joinpath(package_name) @@ -42,28 +42,29 @@ def load_datapackage(url: str, package_name: str) -> Dict[str, pd.DataFrame]: file_name = file[:-4] # read in each file as one dataframe, prevents the deletion of NaN # values with na_filter=False - frame = pd.read_csv(package_path.joinpath(file), - index_col=0, - header=0, - na_filter=False) + frame = pd.read_csv( + package_path.joinpath(file), index_col=0, header=0, na_filter=False + ) data[file_name] = frame else: # download external data and store data - logger.info("Could not find data package in 'filip.data'. Will " - "try to download from %s", url) + logger.info( + "Could not find data package in 'filip.data'. Will " + "try to download from %s", + url, + ) try: data = read_datapackage(url) # rename keys - data = {k.replace('-', '_'): v for k, v in data.items()} + data = {k.replace("-", "_"): v for k, v in data.items()} os.mkdir(package_path) # store data in filip.data for k, v in data.items(): v: DataFrame = v v.loc[:, :] = v[:].applymap(str) - table_filepath = \ - str(package_path) + f"\\{k.replace('-', '_')}.csv" + table_filepath = str(package_path) + f"\\{k.replace('-', '_')}.csv" v.to_csv(table_filepath) except: diff --git a/filip/utils/datetime.py b/filip/utils/datetime.py index 5b6b195a..c987b3c6 100644 --- a/filip/utils/datetime.py +++ b/filip/utils/datetime.py @@ -25,6 +25,4 @@ def convert_datetime_to_iso_8601_with_z_suffix(dt: datetime) -> str: String in iso 8601 notation with z-suffix """ dt = transform_to_utc_datetime(dt) - return dt.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]+'Z' - - + return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" diff --git a/filip/utils/filter.py b/filip/utils/filter.py index c616b2b0..0a7ddc61 100644 --- a/filip/utils/filter.py +++ b/filip/utils/filter.py @@ -1,6 +1,7 @@ """ Filter functions to keep client code clean and easy to use. """ + from typing import List, Union from filip.clients.ngsi_v2 import ContextBrokerClient from filip.models import FiwareHeader @@ -9,10 +10,12 @@ from requests.exceptions import RequestException -def filter_device_list(devices: List[Device], - device_ids: Union[str, List[str]] = None, - entity_names: Union[str, List[str]] = None, - entity_types: Union[str, List[str]] = None) -> List[Device]: +def filter_device_list( + devices: List[Device], + device_ids: Union[str, List[str]] = None, + entity_names: Union[str, List[str]] = None, + entity_types: Union[str, List[str]] = None, +) -> List[Device]: """ Filter the given device list based on conditions @@ -31,33 +34,38 @@ def filter_device_list(devices: List[Device], device_ids = [device_ids] devices = [device for device in devices if device.device_id in device_ids] else: - raise TypeError('device_ids must be a string or a list of strings!') + raise TypeError("device_ids must be a string or a list of strings!") if entity_names: if isinstance(entity_names, (list, str)): if isinstance(entity_names, str): entity_names = [entity_names] - devices = [device for device in devices if device.entity_name in entity_names] + devices = [ + device for device in devices if device.entity_name in entity_names + ] else: - raise TypeError('entity_names must be a string or a list of strings!') + raise TypeError("entity_names must be a string or a list of strings!") if entity_types: if isinstance(entity_types, (list, str)): if isinstance(entity_types, str): entity_types = [entity_types] - devices = [device for device in devices if device.entity_type in entity_types] + devices = [ + device for device in devices if device.entity_type in entity_types + ] else: - raise TypeError('entity_types must be a string or a list of strings!') + raise TypeError("entity_types must be a string or a list of strings!") return devices -def filter_subscriptions_by_entity(entity_id: str, - entity_type: str, - url: str = None, - fiware_header: FiwareHeader = None, - subscriptions: List[Subscription] = None, - ) -> List[Subscription]: +def filter_subscriptions_by_entity( + entity_id: str, + entity_type: str, + url: str = None, + fiware_header: FiwareHeader = None, + subscriptions: List[Subscription] = None, +) -> List[Subscription]: """ Function that filters subscriptions based on the entity id or id pattern and entity type or type pattern. The function can be used in two ways, @@ -80,19 +88,21 @@ def filter_subscriptions_by_entity(entity_id: str, for subscription in subscriptions: for entity in subscription.subject.entities: if entity.id == entity_id or ( - entity.idPattern is not None - and entity.idPattern.match(entity_id)): - if entity.type == entity_type or \ - (entity.typePattern is not None and - entity.typePattern.match(entity_type)): + entity.idPattern is not None and entity.idPattern.match(entity_id) + ): + if entity.type == entity_type or ( + entity.typePattern is not None + and entity.typePattern.match(entity_type) + ): filtered_subscriptions.append(subscription) return filtered_subscriptions -def filter_group_list(group_list: List[ServiceGroup], - resources: Union[str, List[str]] = None, - apikeys: Union[str, List[str]] = None - ) -> List[ServiceGroup]: +def filter_group_list( + group_list: List[ServiceGroup], + resources: Union[str, List[str]] = None, + apikeys: Union[str, List[str]] = None, +) -> List[ServiceGroup]: """ Filter service group based on resource and apikey. @@ -109,7 +119,7 @@ def filter_group_list(group_list: List[ServiceGroup], resources = [resources] group_list = [group for group in group_list if group.resource in resources] else: - raise TypeError('resources must be a string or a list of strings!') + raise TypeError("resources must be a string or a list of strings!") if apikeys: if isinstance(apikeys, (list, str)): @@ -117,8 +127,6 @@ def filter_group_list(group_list: List[ServiceGroup], apikeys = [apikeys] group_list = [group for group in group_list if group.apikey in apikeys] else: - raise TypeError('apikeys must be a string or a list of strings!') + raise TypeError("apikeys must be a string or a list of strings!") return group_list - - diff --git a/filip/utils/geo_ql.py b/filip/utils/geo_ql.py index 3df7f185..8d3069ab 100644 --- a/filip/utils/geo_ql.py +++ b/filip/utils/geo_ql.py @@ -1,3 +1,3 @@ """ Implementation of geo query language -""" \ No newline at end of file +""" diff --git a/filip/utils/iot.py b/filip/utils/iot.py index 07869303..575f977d 100644 --- a/filip/utils/iot.py +++ b/filip/utils/iot.py @@ -1,15 +1,19 @@ """ Helper functions to handle the devices related with the IoT Agent """ + import warnings from typing import List, Union from filip.models.ngsi_v2.iot import Device from filip.utils.filter import filter_device_list as filter_device_list_new -def filter_device_list(devices: List[Device], - device_ids: Union[str, List[str]] = None, - entity_names: Union[str, List[str]] = None, - entity_types: Union[str, List[str]] = None) -> List[Device]: + +def filter_device_list( + devices: List[Device], + device_ids: Union[str, List[str]] = None, + entity_names: Union[str, List[str]] = None, + entity_types: Union[str, List[str]] = None, +) -> List[Device]: """ Filter the given device list based on conditions @@ -22,11 +26,15 @@ def filter_device_list(devices: List[Device], Returns: List of matching devices """ - warnings.warn("This function has been moved to 'filip.utils.filter' " - "and will be removed from this module in future releases!", - DeprecationWarning) + warnings.warn( + "This function has been moved to 'filip.utils.filter' " + "and will be removed from this module in future releases!", + DeprecationWarning, + ) - return filter_device_list_new(devices=devices, - device_ids=device_ids, - entity_names=entity_names, - entity_types=entity_types) + return filter_device_list_new( + devices=devices, + device_ids=device_ids, + entity_names=entity_names, + entity_types=entity_types, + ) diff --git a/filip/utils/model_generation.py b/filip/utils/model_generation.py index 0b5bb88b..72b2ab8b 100644 --- a/filip/utils/model_generation.py +++ b/filip/utils/model_generation.py @@ -1,6 +1,7 @@ """ Code generator for data models from schema.json descriptions """ + import json import shutil from pathlib import Path @@ -14,14 +15,14 @@ from filip.models.ngsi_v2.context import ContextAttribute, ContextEntity -def create_data_model_file(*, - path: Union[Path, str], - url: str = None, - schema: Union[Path, str, ParseResult] = None, - schema_type: Union[str, InputFileType] = - InputFileType.JsonSchema, - class_name: str = None - ) -> None: +def create_data_model_file( + *, + path: Union[Path, str], + url: str = None, + schema: Union[Path, str, ParseResult] = None, + schema_type: Union[str, InputFileType] = InputFileType.JsonSchema, + class_name: str = None, +) -> None: """ This will create a data model from data model definitions. The schemas can either downloaded from a url or passed as str or dict. Allowed input @@ -70,29 +71,32 @@ def create_data_model_file(*, with TemporaryDirectory() as temp: temp = Path(temp) - output = Path(temp).joinpath(f'{uuid4()}.py') + output = Path(temp).joinpath(f"{uuid4()}.py") if url: schema = parse.urlparse(url) if not schema: - raise ValueError("Missing argument! Either 'url' or 'schema' " - "must be provided") + raise ValueError( + "Missing argument! Either 'url' or 'schema' " "must be provided" + ) generate( input_=schema, input_file_type=schema_type, output=output, - class_name=class_name) + class_name=class_name, + ) # move temporary file to output directory shutil.move(str(output), str(path)) -def create_context_entity_model(name: str = None, - data: Dict = None, - validators: Dict[str, Any] = None, - path: Union[Path, str] = None) -> \ - Type['ContextEntity']: +def create_context_entity_model( + name: str = None, + data: Dict = None, + validators: Dict[str, Any] = None, + path: Union[Path, str] = None, +) -> Type["ContextEntity"]: r""" Creates a ContextEntity-Model from a dict: @@ -126,31 +130,32 @@ def create_context_entity_model(name: str = None, ContextEntity """ - properties = {key: (ContextAttribute, ...) for key in data.keys() if - key not in ContextEntity.model_fields} + properties = { + key: (ContextAttribute, ...) + for key in data.keys() + if key not in ContextEntity.model_fields + } model = create_model( - __model_name=name or 'GeneratedContextEntity', + __model_name=name or "GeneratedContextEntity", __base__=ContextEntity, __validators__=validators or {}, - **properties + **properties, ) # if path exits a file will be generated that contains the model if path: if isinstance(path, str): - path=Path(path) + path = Path(path) with TemporaryDirectory() as temp: temp = Path(temp) - output = Path(temp).joinpath(f'{uuid4()}.json') + output = Path(temp).joinpath(f"{uuid4()}.json") output.touch(exist_ok=True) - with output.open('w') as f: + with output.open("w") as f: json.dump(model.model_json_schema(), f, indent=2) - if path.suffix == '.json': + if path.suffix == ".json": # move temporary file to output directory shutil.move(str(output), str(path)) - elif path.suffix == '.py': - create_data_model_file(path=path, - schema=output, - class_name=name) + elif path.suffix == ".py": + create_data_model_file(path=path, schema=output, class_name=name) return model diff --git a/filip/utils/simple_ql.py b/filip/utils/simple_ql.py index 97c2d6b5..b96cdbb6 100644 --- a/filip/utils/simple_ql.py +++ b/filip/utils/simple_ql.py @@ -9,6 +9,7 @@ https://telefonicaid.github.io/fiware-orion/api/v2/stable/ """ + import regex as re from aenum import Enum from typing import Union, List, Tuple, Any @@ -18,87 +19,109 @@ class Operator(str, Enum): """ The list of operators (and the format of the values they use) is as follows: """ - _init_ = 'value __doc__' - - EQUAL = '==', "Single element, e.g. temperature!=41. For an entity to " \ - "match, it must contain the target property (temperature) " \ - "and the target property value must not be the query value " \ - "(41). " \ - "A list of comma-separated values, e.g. color!=black," \ - "red. For an entity to match, it must contain the target " \ - "property and the target property value must not be any " \ - "of the values in the list (AND clause) (or not include any "\ - "of the values in the list in case the target property " \ - "value is an array). Eg. entities whose attribute color is " \ - "set to black will not match, while entities whose " \ - "attribute color is set to white will match." \ - "A range, specified as a minimum and maximum separated by " \ - ".., e.g. temperature!=10..20. For an entity to match, " \ - "it must contain the target property (temperature) and the " \ - "target property value must not be between the upper and " \ - "lower limits (both included). Ranges can only be used " \ - "with elements target properties that represent dates (in " \ - "ISO8601 format), numbers or strings. " - UNEQUAL = '!=', "Single element, e.g. temperature!=41. For an entity to " \ - "match, it must contain the target property " \ - "(temperature) and the target property value must not be " \ - "the query value (41). A list of comma-separated values, " \ - "e.g. color!=black,red. For an entity to match, it must " \ - "contain the target property and the target property " \ - "value must not be any of the values in the list (AND " \ - "clause) (or not include any of the values in the list " \ - "in case the target property value is an array). Eg. " \ - "entities whose attribute color is set to black will not " \ - "match, while entities whose attribute color is set to " \ - "white will match. A range, specified as a minimum and " \ - "maximum separated by .., e.g. temperature!=10..20. For " \ - "an entity to match, it must contain the target property " \ - "(temperature) and the target property value must not be " \ - "between the upper and lower limits (both included). " \ - "Ranges can only be used with elements target properties " \ - "that represent dates (in ISO8601 format), numbers or " \ - "strings. " - GREATER_THAN = '>', "The right-hand side must be a single element, e.g. " \ - "temperature>42. For an entity to match, it must " \ - "contain the target property (temperature) and the " \ - "target property value must be strictly greater than " \ - "the query value (42). This operation is only valid " \ - "for target properties of type date, number or " \ - "string (used with target properties of other types " \ - "may lead to unpredictable results). " - LESS_THAN = '<', "The right-hand side must be a single element, e.g. " \ - "temperature<43. For an entity to match, it must " \ - "contain the target property (temperature) and the " \ - "target property value must be strictly less than the " \ - "value (43). This operation is only valid for target " \ - "properties of type date, number or string (used with " \ - "target properties of other types may lead to " \ - "unpredictable results). " - GREATER_OR_EQUAL = '>=', "The right-hand side must be a single element, " \ - "e.g. temperature>=44. For an entity to match, " \ - "it must contain the target property (" \ - "temperature) and the target property value " \ - "must be greater than or equal to that value " \ - "(44). This operation is only valid for target " \ - "properties of type date, number or string " \ - "(used with target properties of other types " \ - "may lead to unpredictable results). " - LESS_OR_EQUAL = '<=', "The right-hand side must be a single element, " \ - "e.g. temperature<=45. For an entity to match, " \ - "it must contain the target property (temperature) " \ - "and the target property value must be less than " \ - "or equal to that value (45). This operation is " \ - "only valid for target properties of type date, " \ - "number or string (used with target properties of " \ - "other types may lead to unpredictable results). " - MATCH_PATTERN = '~=', "The value matches a given pattern, expressed as a " \ - "regular expression, e.g. color~=ow. For an entity " \ - "to match, it must contain the target property (" \ - "color) and the target property value must match " \ - "the string in the right-hand side, 'ow' in this " \ - "example (brown and yellow would match, black and " \ - "white would not). This operation is only valid " \ - "for target properties of type string. " + + _init_ = "value __doc__" + + EQUAL = ( + "==", + "Single element, e.g. temperature!=41. For an entity to " + "match, it must contain the target property (temperature) " + "and the target property value must not be the query value " + "(41). " + "A list of comma-separated values, e.g. color!=black," + "red. For an entity to match, it must contain the target " + "property and the target property value must not be any " + "of the values in the list (AND clause) (or not include any " + "of the values in the list in case the target property " + "value is an array). Eg. entities whose attribute color is " + "set to black will not match, while entities whose " + "attribute color is set to white will match." + "A range, specified as a minimum and maximum separated by " + ".., e.g. temperature!=10..20. For an entity to match, " + "it must contain the target property (temperature) and the " + "target property value must not be between the upper and " + "lower limits (both included). Ranges can only be used " + "with elements target properties that represent dates (in " + "ISO8601 format), numbers or strings. ", + ) + UNEQUAL = ( + "!=", + "Single element, e.g. temperature!=41. For an entity to " + "match, it must contain the target property " + "(temperature) and the target property value must not be " + "the query value (41). A list of comma-separated values, " + "e.g. color!=black,red. For an entity to match, it must " + "contain the target property and the target property " + "value must not be any of the values in the list (AND " + "clause) (or not include any of the values in the list " + "in case the target property value is an array). Eg. " + "entities whose attribute color is set to black will not " + "match, while entities whose attribute color is set to " + "white will match. A range, specified as a minimum and " + "maximum separated by .., e.g. temperature!=10..20. For " + "an entity to match, it must contain the target property " + "(temperature) and the target property value must not be " + "between the upper and lower limits (both included). " + "Ranges can only be used with elements target properties " + "that represent dates (in ISO8601 format), numbers or " + "strings. ", + ) + GREATER_THAN = ( + ">", + "The right-hand side must be a single element, e.g. " + "temperature>42. For an entity to match, it must " + "contain the target property (temperature) and the " + "target property value must be strictly greater than " + "the query value (42). This operation is only valid " + "for target properties of type date, number or " + "string (used with target properties of other types " + "may lead to unpredictable results). ", + ) + LESS_THAN = ( + "<", + "The right-hand side must be a single element, e.g. " + "temperature<43. For an entity to match, it must " + "contain the target property (temperature) and the " + "target property value must be strictly less than the " + "value (43). This operation is only valid for target " + "properties of type date, number or string (used with " + "target properties of other types may lead to " + "unpredictable results). ", + ) + GREATER_OR_EQUAL = ( + ">=", + "The right-hand side must be a single element, " + "e.g. temperature>=44. For an entity to match, " + "it must contain the target property (" + "temperature) and the target property value " + "must be greater than or equal to that value " + "(44). This operation is only valid for target " + "properties of type date, number or string " + "(used with target properties of other types " + "may lead to unpredictable results). ", + ) + LESS_OR_EQUAL = ( + "<=", + "The right-hand side must be a single element, " + "e.g. temperature<=45. For an entity to match, " + "it must contain the target property (temperature) " + "and the target property value must be less than " + "or equal to that value (45). This operation is " + "only valid for target properties of type date, " + "number or string (used with target properties of " + "other types may lead to unpredictable results). ", + ) + MATCH_PATTERN = ( + "~=", + "The value matches a given pattern, expressed as a " + "regular expression, e.g. color~=ow. For an entity " + "to match, it must contain the target property (" + "color) and the target property value must match " + "the string in the right-hand side, 'ow' in this " + "example (brown and yellow would match, black and " + "white would not). This operation is only valid " + "for target properties of type string. ", + ) @classmethod def list(cls): @@ -136,19 +159,22 @@ def validate(cls, value): """ if isinstance(value, (tuple, QueryStatement)): if len(value) != 3: - raise TypeError('3-tuple required') + raise TypeError("3-tuple required") if not isinstance(value[0], str): - raise TypeError('First argument must be a string!') + raise TypeError("First argument must be a string!") if value[1] not in Operator.list(): - raise TypeError('Invalid comparison operator!') - if value[1] not in [Operator.EQUAL, - Operator.UNEQUAL, - Operator.MATCH_PATTERN]: + raise TypeError("Invalid comparison operator!") + if value[1] not in [ + Operator.EQUAL, + Operator.UNEQUAL, + Operator.MATCH_PATTERN, + ]: try: float(value[2]) except ValueError as err: - err.args += ("Invalid combination of operator and right " - "hand side!",) + err.args += ( + "Invalid combination of operator and right " "hand side!", + ) raise return value elif isinstance(value, str): @@ -168,7 +194,7 @@ def to_str(self): right = f"{self[2]}" else: right = self[2] - return ''.join([self[0], self[1], right]) + return "".join([self[0], self[1], right]) @classmethod def parse_str(cls, string: str): @@ -195,11 +221,11 @@ def parse_str(cls, string: str): raise ValueError def __str__(self): - """ Return str(self). """ + """Return str(self).""" return self.to_str() def __repr__(self): - """ Return repr(self). """ + """Return repr(self).""" return self.to_str().__repr__() @@ -207,9 +233,10 @@ class QueryString: """ Class for validated QueryStrings that can be used in api clients """ - def __init__(self, qs: Union[Tuple, - QueryStatement, - List[Union[QueryStatement, Tuple]]]): + + def __init__( + self, qs: Union[Tuple, QueryStatement, List[Union[QueryStatement, Tuple]]] + ): qs = self.__check_arguments(qs=qs) self._qs = qs @@ -235,7 +262,7 @@ def __check_arguments(cls, qs): elif isinstance(qs, tuple): qs = [QueryStatement(*qs)] else: - raise ValueError('Invalid argument!') + raise ValueError("Invalid argument!") return qs def update(self, qs: Union[Tuple, QueryStatement, List[QueryStatement]]): @@ -277,7 +304,7 @@ def validate(cls, v): return v if isinstance(v, str): return cls.parse_str(v) - raise ValueError('Invalid argument!') + raise ValueError("Invalid argument!") def to_str(self): """ @@ -286,7 +313,7 @@ def to_str(self): Returns: String: query string that can be added to requests as parameter """ - return ';'.join([q.to_str() for q in self._qs]) + return ";".join([q.to_str() for q in self._qs]) @classmethod def parse_str(cls, string: str): @@ -299,7 +326,7 @@ def parse_str(cls, string: str): Returns: QueryString """ - q_parts = string.split(';') + q_parts = string.split(";") qs = [] for part in q_parts: q = QueryStatement.parse_str(part) @@ -307,9 +334,9 @@ def parse_str(cls, string: str): return QueryString(qs=qs) def __str__(self): - """ Return str(self). """ + """Return str(self).""" return self.to_str() def __repr__(self): - """ Return repr(self). """ + """Return repr(self).""" return self.to_str().__repr__() diff --git a/filip/utils/validators.py b/filip/utils/validators.py index 551574b9..c17c5e7c 100644 --- a/filip/utils/validators.py +++ b/filip/utils/validators.py @@ -1,6 +1,7 @@ """ Helper functions to prohibit boiler plate code """ + import logging import re import warnings @@ -21,16 +22,20 @@ class FiwareRegex(str, Enum): Collection of Regex expression used to check if the value of a Pydantic field, can be used in the related Fiware field. """ - _init_ = 'value __doc__' - standard = r"(^((?![?&#/\"' ])[\x00-\x7F])*$)", \ - "Prevents any string that contains at least one of the " \ - "symbols: ? & # / ' \" or a whitespace" - string_protect = r"(?!^id$)(?!^type$)(?!^geo:location$)" \ - r"(^((?![?&#/\"' ])[\x00-\x7F])*$)",\ - "Prevents any string that contains at least one of " \ - "the symbols: ? & # / ' \" or a whitespace." \ - "AND the strings: id, type, geo:location" + _init_ = "value __doc__" + + standard = ( + r"(^((?![?&#/\"' ])[\x00-\x7F])*$)", + "Prevents any string that contains at least one of the " + "symbols: ? & # / ' \" or a whitespace", + ) + string_protect = ( + r"(?!^id$)(?!^type$)(?!^geo:location$)" r"(^((?![?&#/\"' ])[\x00-\x7F])*$)", + "Prevents any string that contains at least one of " + "the symbols: ? & # / ' \" or a whitespace." + "AND the strings: id, type, geo:location", + ) @validate_call @@ -97,11 +102,9 @@ def validate_escape_character_free(value: Any) -> Any: # if a value here is not a string, it will also not contain ' or " value = str(value) if '"' == value[-1:] or '"' == value[0:1]: - raise ValueError(f"The value {value} contains " - f"the forbidden char \"") + raise ValueError(f"The value {value} contains " f'the forbidden char "') if "'" == value[-1:] or "'" == value[0:1]: - raise ValueError(f"The value {value} contains " - f"the forbidden char '") + raise ValueError(f"The value {value} contains " f"the forbidden char '") return values @@ -109,9 +112,9 @@ def match_regex(value: str, pattern: str): regex = re.compile(pattern) if not regex.match(value): raise PydanticCustomError( - 'string_pattern_mismatch', + "string_pattern_mismatch", "String should match pattern '{pattern}'", - {'pattern': pattern}, + {"pattern": pattern}, ) return value @@ -121,6 +124,7 @@ def wrapper(arg): if arg is None: return arg return func(arg) + return wrapper @@ -134,12 +138,13 @@ def validate_fiware_string_protect_regex(vale: str): @ignore_none_input def validate_mqtt_topic(topic: str): - return match_regex(topic, r'^((?![\'\"#+,])[\x00-\x7F])*$') + return match_regex(topic, r"^((?![\'\"#+,])[\x00-\x7F])*$") @ignore_none_input def validate_fiware_datatype_standard(_type): from filip.models.base import DataType + if isinstance(_type, DataType): return _type elif isinstance(_type, str): @@ -151,6 +156,7 @@ def validate_fiware_datatype_standard(_type): @ignore_none_input def validate_fiware_datatype_string_protect(_type): from filip.models.base import DataType + if isinstance(_type, DataType): return _type elif isinstance(_type, str): @@ -161,14 +167,12 @@ def validate_fiware_datatype_string_protect(_type): @ignore_none_input def validate_fiware_service_path(service_path): - return match_regex(service_path, - r'^((\/\w*)|(\/\#))*(\,((\/\w*)|(\/\#)))*$') + return match_regex(service_path, r"^((\/\w*)|(\/\#))*(\,((\/\w*)|(\/\#)))*$") @ignore_none_input def validate_fiware_service(service): - return match_regex(service, - r"\w*$") + return match_regex(service, r"\w*$") jexl_transformation_functions = { @@ -202,7 +206,7 @@ def validate_fiware_service(service): "addset": "(arr, x) => list(set(arr).add(x))", "removeset": "(arr, x) => list(set(arr).remove(x))", "touppercase": "(val) => str(val).upper()", - "tolowercase": "(val) => str(val).lower()" + "tolowercase": "(val) => str(val).lower()", } @@ -214,7 +218,7 @@ def validate_jexl_expression(expression, attribute_name, device_id): warnings.warn(f"{jexl_expression.name} might not supported") except ParseError: msg = f"Invalid JEXL expression '{expression}' inside the attribute '{attribute_name}' of Device '{device_id}'." - if '|' in expression: + if "|" in expression: msg += " If the expression contains the transform operator '|' you need to remove the spaces around it." raise ParseError(msg) return expression @@ -222,8 +226,10 @@ def validate_jexl_expression(expression, attribute_name, device_id): def validate_expression_language(cls, expressionLanguage): if expressionLanguage == "legacy": - warnings.warn(f"Using 'LEGACY' expression language inside {cls.__name__} is " - f"deprecated. Use 'JEXL' instead.") + warnings.warn( + f"Using 'LEGACY' expression language inside {cls.__name__} is " + f"deprecated. Use 'JEXL' instead." + ) elif expressionLanguage is None: expressionLanguage = "jexl" return expressionLanguage diff --git a/setup.py b/setup.py index 39bbab41..25a07f1e 100644 --- a/setup.py +++ b/setup.py @@ -4,73 +4,74 @@ # read the contents of your README file from pathlib import Path + readme_path = Path(__file__).parent.joinpath("README.md") LONG_DESCRIPTION = readme_path.read_text() -INSTALL_REQUIRES = ['aenum~=3.1.15', - 'datamodel_code_generator[http]~=0.25.0', - 'paho-mqtt~=2.0.0', - 'pandas_datapackage_reader~=0.18.0', - 'pydantic>=2.5.2,<2.7.0', - 'pydantic-settings>=2.0.0,<2.3.0', - 'geojson_pydantic~=1.0.2', - 'stringcase>=1.2.0', - 'rdflib~=6.0.0', - 'regex~=2023.10.3', - 'requests~=2.32.0', - 'rapidfuzz~=3.4.0', - 'geojson-pydantic~=1.0.2', - 'wget~=3.2', - 'PyLD~=2.0.4', - 'pyjexl~=0.3.0'] +INSTALL_REQUIRES = [ + "aenum~=3.1.15", + "datamodel_code_generator[http]~=0.25.0", + "paho-mqtt~=2.0.0", + "pandas_datapackage_reader~=0.18.0", + "pydantic>=2.5.2,<2.7.0", + "pydantic-settings>=2.0.0,<2.3.0", + "geojson_pydantic~=1.0.2", + "stringcase>=1.2.0", + "rdflib~=6.0.0", + "regex~=2023.10.3", + "requests~=2.32.0", + "rapidfuzz~=3.4.0", + "geojson-pydantic~=1.0.2", + "wget~=3.2", + "PyLD~=2.0.4", + "pyjexl~=0.3.0", +] SETUP_REQUIRES = INSTALL_REQUIRES.copy() -VERSION = '0.6.0' +VERSION = "0.6.0" setuptools.setup( - name='filip', + name="filip", version=VERSION, - author='RWTH Aachen University, E.ON Energy Research Center, Institute\ - of Energy Efficient Buildings and Indoor Climate', - author_email='junsong.du@eonerc.rwth-aachen.de', - description='[FI]WARE [Li]brary for [P]ython', + author="RWTH Aachen University, E.ON Energy Research Center, Institute\ + of Energy Efficient Buildings and Indoor Climate", + author_email="junsong.du@eonerc.rwth-aachen.de", + description="[FI]WARE [Li]brary for [P]ython", long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", url="https://github.com/RWTH-EBC/filip", download_url=f"https://github.com/RWTH-EBC/FiLiP/archive/refs/tags/v{VERSION}.tar.gz", project_urls={ - "Documentation": - "https://rwth-ebc.github.io/FiLiP/master/docs/index.html", - "Source": - "https://github.com/RWTH-EBC/filip", - "Download": - f"https://github.com/RWTH-EBC/FiLiP/archive/refs/tags/v{VERSION}.tar.gz"}, + "Documentation": "https://rwth-ebc.github.io/FiLiP/master/docs/index.html", + "Source": "https://github.com/RWTH-EBC/filip", + "Download": f"https://github.com/RWTH-EBC/FiLiP/archive/refs/tags/v{VERSION}.tar.gz", + }, # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. - classifiers=['Development Status :: 3 - Alpha', - 'Topic :: Scientific/Engineering', - 'Intended Audience :: Science/Research', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - "License :: OSI Approved :: BSD License"], - keywords=['iot', 'fiware', 'semantic'], - packages=setuptools.find_packages(exclude=['tests', - 'tests.*', - 'img', - 'tutorials.*', - 'tutorials']), - package_data={'filip': ['data/unece-units/*.csv']}, + classifiers=[ + "Development Status :: 3 - Alpha", + "Topic :: Scientific/Engineering", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: BSD License", + ], + keywords=["iot", "fiware", "semantic"], + packages=setuptools.find_packages( + exclude=["tests", "tests.*", "img", "tutorials.*", "tutorials"] + ), + package_data={"filip": ["data/unece-units/*.csv"]}, setup_requires=SETUP_REQUIRES, # optional modules extras_require={ + "development": ["pre-commit~=4.0.1"], "semantics": ["igraph~=0.11.2"], ":python_version < '3.9'": ["pandas~=1.3.5"], - ":python_version >= '3.9'": ["pandas~=2.1.4"] + ":python_version >= '3.9'": ["pandas~=2.1.4"], }, install_requires=INSTALL_REQUIRES, python_requires=">=3.8", - ) diff --git a/tests/clients/test_mqtt_client.py b/tests/clients/test_mqtt_client.py index 9bbb602d..84320808 100644 --- a/tests/clients/test_mqtt_client.py +++ b/tests/clients/test_mqtt_client.py @@ -2,17 +2,19 @@ import time import datetime import unittest -from random import randrange,Random -from paho.mqtt.client import MQTT_CLEAN_START_FIRST_ONLY,MQTTv5 +from random import randrange, Random +from paho.mqtt.client import MQTT_CLEAN_START_FIRST_ONLY, MQTTv5 from filip.custom_types import AnyMqttUrl from filip.models import FiwareHeader from filip.models.ngsi_v2.context import NamedCommand -from filip.models.ngsi_v2.iot import \ - Device, \ - DeviceAttribute, \ - DeviceCommand, \ - ServiceGroup, PayloadProtocol, \ - TransportProtocol +from filip.models.ngsi_v2.iot import ( + Device, + DeviceAttribute, + DeviceCommand, + ServiceGroup, + PayloadProtocol, + TransportProtocol, +) from filip.clients.mqtt import IoTAMQTTClient from filip.utils.cleanup import clean_test, clear_all from tests.config import settings @@ -28,53 +30,54 @@ class TestMQTTClient(unittest.TestCase): def setUp(self) -> None: self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) - self.service_group_json=ServiceGroup( - apikey=settings.FIWARE_SERVICEPATH.strip('/'), - resource="/iot/json") - self.service_group_ul=ServiceGroup( - apikey=settings.FIWARE_SERVICEPATH.strip('/'), - resource="/iot/d") + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) + self.service_group_json = ServiceGroup( + apikey=settings.FIWARE_SERVICEPATH.strip("/"), resource="/iot/json" + ) + self.service_group_ul = ServiceGroup( + apikey=settings.FIWARE_SERVICEPATH.strip("/"), resource="/iot/d" + ) # create a device configuration - device_attr = DeviceAttribute(name='temperature', - object_id='t', - type="Number") - device_command = DeviceCommand(name='heater', type="Boolean") - - self.device_json = Device(device_id='my_json_device', - entity_name='my_json_device', - entity_type='Thing', - protocol='IoTA-JSON', - transport='MQTT', - apikey=self.service_group_json.apikey, - attributes=[device_attr], - commands=[device_command]) - - self.device_ul = Device(device_id='my_ul_device', - entity_name='my_ul_device', - entity_type='Thing', - protocol='PDI-IoTA-UltraLight', - transport='MQTT', - apikey=self.service_group_ul.apikey, - attributes=[device_attr], - commands=[device_command]) + device_attr = DeviceAttribute(name="temperature", object_id="t", type="Number") + device_command = DeviceCommand(name="heater", type="Boolean") + + self.device_json = Device( + device_id="my_json_device", + entity_name="my_json_device", + entity_type="Thing", + protocol="IoTA-JSON", + transport="MQTT", + apikey=self.service_group_json.apikey, + attributes=[device_attr], + commands=[device_command], + ) + + self.device_ul = Device( + device_id="my_ul_device", + entity_name="my_ul_device", + entity_type="Thing", + protocol="PDI-IoTA-UltraLight", + transport="MQTT", + apikey=self.service_group_ul.apikey, + attributes=[device_attr], + commands=[device_command], + ) self.mqttc = IoTAMQTTClient() - def on_connect(mqttc, obj, flags, rc,properties): + def on_connect(mqttc, obj, flags, rc, properties): mqttc.logger.info("rc: " + str(rc)) def on_connect_fail(mqttc, obj): mqttc.logger.info("Connect failed") - def on_publish(mqttc, obj, mid,rc,properties): + def on_publish(mqttc, obj, mid, rc, properties): mqttc.logger.info("mid: " + str(mid)) - def on_subscribe(mqttc, obj, mid, granted_qos,properties): - mqttc.logger.info("Subscribed: " + str(mid) - + " " + str(granted_qos)) + def on_subscribe(mqttc, obj, mid, granted_qos, properties): + mqttc.logger.info("Subscribed: " + str(mid) + " " + str(granted_qos)) def on_log(mqttc, obj, level, string): mqttc.logger.info(string) @@ -95,25 +98,26 @@ def test_original_functionality(self): second_topic = f"/filip/{settings.FIWARE_SERVICEPATH.strip('/')}/second" first_payload = "filip_test_1" second_payload = "filip_test_2" + def on_message_first(mqttc, obj, msg, properties=None): - self.assertEqual(msg.payload.decode('utf-8'), first_payload) + self.assertEqual(msg.payload.decode("utf-8"), first_payload) def on_message_second(mqttc, obj, msg, properties=None): - self.assertEqual(msg.payload.decode('utf-8'), second_payload) + self.assertEqual(msg.payload.decode("utf-8"), second_payload) - self.mqttc.message_callback_add(sub=first_topic, - callback=on_message_first) - self.mqttc.message_callback_add(sub=second_topic, - callback=on_message_second) + self.mqttc.message_callback_add(sub=first_topic, callback=on_message_first) + self.mqttc.message_callback_add(sub=second_topic, callback=on_message_second) mqtt_broker_url = settings.MQTT_BROKER_URL - self.mqttc.connect(host=mqtt_broker_url.host, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + self.mqttc.connect( + host=mqtt_broker_url.host, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) self.mqttc.subscribe(topic=first_topic) # create a non blocking loop @@ -133,14 +137,14 @@ def on_message_second(mqttc, obj, msg, properties=None): # stop network loop and disconnect cleanly self.mqttc.loop_stop() self.mqttc.disconnect() - + def test_optional_object_id(self): """ Test: Verify the IotaMQTTClient publish function is not raising when missing an object_id. - The test setup is minimal, we are not concerned - with commands/command callbacks, just that publish + The test setup is minimal, we are not concerned + with commands/command callbacks, just that publish works for a specific device argument Setup: publish with: @@ -150,46 +154,51 @@ def test_optional_object_id(self): - with and without object_id - with key = attr.name/in object_id """ - tmp_id="dev_id" - tmp_attrs = [DeviceAttribute(name="temperature", - type="Number"), - DeviceAttribute(name="temperature", - type="Number", - object_id="temp")] - - payloads = [{"temperature":Random().randint(0,50)}, - {"temp":Random().randint(0,50)}] - - for p,attr in zip(payloads,tmp_attrs): - tmp_dev = Device(device_id=tmp_id, - attributes=[attr], - entity_name="tmp_entity", - entity_type="tmp_type", - apikey="tmp_key", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON) - tmp_mqttc = IoTAMQTTClient(protocol=MQTTv5 ,devices=[tmp_dev]) - tmp_mqttc.publish(device_id=tmp_id,payload=p) + tmp_id = "dev_id" + tmp_attrs = [ + DeviceAttribute(name="temperature", type="Number"), + DeviceAttribute(name="temperature", type="Number", object_id="temp"), + ] + + payloads = [ + {"temperature": Random().randint(0, 50)}, + {"temp": Random().randint(0, 50)}, + ] + + for p, attr in zip(payloads, tmp_attrs): + tmp_dev = Device( + device_id=tmp_id, + attributes=[attr], + entity_name="tmp_entity", + entity_type="tmp_type", + apikey="tmp_key", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + ) + tmp_mqttc = IoTAMQTTClient(protocol=MQTTv5, devices=[tmp_dev]) + tmp_mqttc.publish(device_id=tmp_id, payload=p) tmp_mqttc.delete_device(device_id=tmp_id) - #checking if raises correctly + # checking if raises correctly with self.assertRaises(KeyError): - tmp_dev = Device(device_id=tmp_id, - attributes=[tmp_attrs[0]], - entity_name="tmp_entity", - entity_type="tmp_type", - apikey="tmp_key", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON) + tmp_dev = Device( + device_id=tmp_id, + attributes=[tmp_attrs[0]], + entity_name="tmp_entity", + entity_type="tmp_type", + apikey="tmp_key", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + ) tmp_mqttc = IoTAMQTTClient(protocol=MQTTv5, devices=[tmp_dev]) - tmp_mqttc.publish(device_id=tmp_id, - payload={"t": Random().randint(0,50)}) + tmp_mqttc.publish(device_id=tmp_id, payload={"t": Random().randint(0, 50)}) tmp_mqttc.delete_device(device_id=tmp_id) def test_init(self): devices = [self.device_json, self.device_ul] - mqttc = IoTAMQTTClient(devices=devices, - service_groups=[self.service_group_json]) + mqttc = IoTAMQTTClient( + devices=devices, service_groups=[self.service_group_json] + ) self.assertListEqual(mqttc.devices, devices) def test_service_groups(self): @@ -198,18 +207,22 @@ def test_service_groups(self): self.mqttc.add_service_group(service_group="SomethingRandom") with self.assertRaises(ValueError): self.mqttc.add_service_group( - service_group=self.service_group_json.model_dump()) + service_group=self.service_group_json.model_dump() + ) self.assertEqual( self.service_group_json, - self.mqttc.get_service_group(self.service_group_json.apikey)) + self.mqttc.get_service_group(self.service_group_json.apikey), + ) self.mqttc.update_service_group(service_group=self.service_group_json) with self.assertRaises(KeyError): self.mqttc.update_service_group( service_group=self.service_group_json.model_copy( - update={'apikey': 'someOther'})) + update={"apikey": "someOther"} + ) + ) with self.assertRaises(KeyError): self.mqttc.delete_service_group(apikey="SomethingRandom") @@ -229,14 +242,18 @@ def test_devices(self): with self.assertRaises(KeyError): self.mqttc.update_device( device=self.device_json.model_copy( - update={'device_id': "somethingRandom"})) + update={"device_id": "somethingRandom"} + ) + ) self.mqttc.delete_device(device_id=self.device_json.device_id) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_add_command_callback_json(self): """ Test for receiving commands for a specific device @@ -250,49 +267,56 @@ def test_add_command_callback_json(self): self.mqttc.delete_device(device.device_id) def on_command(client, obj, msg): - apikey, device_id, payload = \ - client.get_encoder(PayloadProtocol.IOTA_JSON).decode_message( - msg=msg) + apikey, device_id, payload = client.get_encoder( + PayloadProtocol.IOTA_JSON + ).decode_message(msg=msg) # acknowledge a command. Here command are usually single # messages. The first key is equal to the commands name. - client.publish(device_id=device_id, - command_name=next(iter(payload)), - payload=payload) + client.publish( + device_id=device_id, command_name=next(iter(payload)), payload=payload + ) self.mqttc.add_service_group(self.service_group_json) self.mqttc.add_device(self.device_json) - self.mqttc.add_command_callback(device_id=self.device_json.device_id, - callback=on_command) + self.mqttc.add_command_callback( + device_id=self.device_json.device_id, callback=on_command + ) from filip.clients.ngsi_v2 import HttpClient, HttpClientConfig - httpc_config = HttpClientConfig(cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) - httpc = HttpClient(fiware_header=self.fiware_header, - config=httpc_config) + + httpc_config = HttpClientConfig( + cb_url=settings.CB_URL, iota_url=settings.IOTA_JSON_URL + ) + httpc = HttpClient(fiware_header=self.fiware_header, config=httpc_config) httpc.iota.post_group(service_group=self.service_group_json, update=True) httpc.iota.post_device(device=self.device_json, update=True) mqtt_broker_url: AnyMqttUrl = settings.MQTT_BROKER_URL - self.mqttc.connect(host=mqtt_broker_url.host, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + self.mqttc.connect( + host=mqtt_broker_url.host, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) self.mqttc.subscribe() - entity = httpc.cb.get_entity(entity_id=self.device_json.device_id, - entity_type=self.device_json.entity_type) - context_command = NamedCommand(name=self.device_json.commands[0].name, - value=False) + entity = httpc.cb.get_entity( + entity_id=self.device_json.device_id, + entity_type=self.device_json.entity_type, + ) + context_command = NamedCommand( + name=self.device_json.commands[0].name, value=False + ) self.mqttc.loop_start() - httpc.cb.post_command(entity_id=entity.id, - entity_type=entity.type, - command=context_command) + httpc.cb.post_command( + entity_id=entity.id, entity_type=entity.type, command=context_command + ) time.sleep(5) # close the mqtt listening thread @@ -300,16 +324,20 @@ def on_command(client, obj, msg): # disconnect the mqtt device self.mqttc.disconnect() - entity = httpc.cb.get_entity(entity_id=self.device_json.device_id, - entity_type=self.device_json.entity_type) + entity = httpc.cb.get_entity( + entity_id=self.device_json.device_id, + entity_type=self.device_json.entity_type, + ) # The main part of this test, for all this setup was done self.assertEqual("OK", entity.heater_status.value) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_publish_json(self): """ Test for receiving commands for a specific device @@ -326,43 +354,52 @@ def test_publish_json(self): self.mqttc.add_device(self.device_json) from filip.clients.ngsi_v2 import HttpClient, HttpClientConfig - httpc_config = HttpClientConfig(cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) - httpc = HttpClient(fiware_header=self.fiware_header, - config=httpc_config) + + httpc_config = HttpClientConfig( + cb_url=settings.CB_URL, iota_url=settings.IOTA_JSON_URL + ) + httpc = HttpClient(fiware_header=self.fiware_header, config=httpc_config) httpc.iota.post_group(service_group=self.service_group_json, update=True) httpc.iota.post_device(device=self.device_json, update=True) mqtt_broker_url = settings.MQTT_BROKER_URL - self.mqttc.connect(host=mqtt_broker_url.host, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + self.mqttc.connect( + host=mqtt_broker_url.host, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) self.mqttc.loop_start() - payload = randrange(0, 100, 1)/1000 - self.mqttc.publish(device_id=self.device_json.device_id, - payload={self.device_json.attributes[0].object_id: - payload}) + payload = randrange(0, 100, 1) / 1000 + self.mqttc.publish( + device_id=self.device_json.device_id, + payload={self.device_json.attributes[0].object_id: payload}, + ) time.sleep(1) - entity = httpc.cb.get_entity(entity_id=self.device_json.device_id, - entity_type=self.device_json.entity_type) + entity = httpc.cb.get_entity( + entity_id=self.device_json.device_id, + entity_type=self.device_json.entity_type, + ) self.assertEqual(payload, entity.temperature.value) payload = randrange(0, 100, 1) / 1000 - self.mqttc.publish(device_id=self.device_json.device_id, - attribute_name="temperature", - payload=payload) + self.mqttc.publish( + device_id=self.device_json.device_id, + attribute_name="temperature", + payload=payload, + ) time.sleep(1) - entity = httpc.cb.get_entity(entity_id=self.device_json.device_id, - entity_type=self.device_json.entity_type) + entity = httpc.cb.get_entity( + entity_id=self.device_json.device_id, + entity_type=self.device_json.entity_type, + ) self.assertEqual(payload, entity.temperature.value) - # These test do currently not workt due to time stamp parsing # self.mqttc.publish(device_id=self.device_json.device_id, # payload={self.device_json.attributes[ @@ -372,7 +409,7 @@ def test_publish_json(self): # entity = httpc.cb.get_entity(entity_id=self.device_json.device_id, # entity_type=self.device_json.entity_type) # self.assertEqual(50, entity.temperature.value) -# + # # from datetime import datetime, timedelta # timestamp = datetime.now() + timedelta(days=1) # timestamp = timestamp.astimezone().isoformat() @@ -385,7 +422,7 @@ def test_publish_json(self): # entity_type=self.device_json.entity_type) # self.assertEqual(60, entity.temperature.value) # self.assertEqual(timestamp, entity.TimeInstant.value) -# + # # print(entity.json(indent=2)) # close the mqtt listening thread @@ -393,10 +430,12 @@ def test_publish_json(self): # disconnect the mqtt device self.mqttc.disconnect() - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_UL_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_UL_URL, + ) def test_add_command_callback_ultralight(self): """ Test for receiving commands for a specific device @@ -410,49 +449,57 @@ def test_add_command_callback_ultralight(self): self.mqttc.delete_device(device.device_id) def on_command(client, obj, msg): - apikey, device_id, payload = \ - client.get_encoder(PayloadProtocol.IOTA_UL).decode_message( - msg=msg) + apikey, device_id, payload = client.get_encoder( + PayloadProtocol.IOTA_UL + ).decode_message(msg=msg) # acknowledge a command. Here command are usually single # messages. The first key is equal to the commands name. - client.publish(device_id=device_id, - command_name=next(iter(payload)), - payload={'heater': True}) + client.publish( + device_id=device_id, + command_name=next(iter(payload)), + payload={"heater": True}, + ) self.mqttc.add_service_group(self.service_group_ul) self.mqttc.add_device(self.device_ul) - self.mqttc.add_command_callback(device_id=self.device_ul.device_id, - callback=on_command) + self.mqttc.add_command_callback( + device_id=self.device_ul.device_id, callback=on_command + ) from filip.clients.ngsi_v2 import HttpClient, HttpClientConfig - httpc_config = HttpClientConfig(cb_url=settings.CB_URL, - iota_url=settings.IOTA_UL_URL) - httpc = HttpClient(fiware_header=self.fiware_header, - config=httpc_config) + + httpc_config = HttpClientConfig( + cb_url=settings.CB_URL, iota_url=settings.IOTA_UL_URL + ) + httpc = HttpClient(fiware_header=self.fiware_header, config=httpc_config) httpc.iota.post_group(service_group=self.service_group_ul) httpc.iota.post_device(device=self.device_ul, update=True) mqtt_broker_url = settings.MQTT_BROKER_URL - self.mqttc.connect(host=mqtt_broker_url.host, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + self.mqttc.connect( + host=mqtt_broker_url.host, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) self.mqttc.subscribe() - entity = httpc.cb.get_entity(entity_id=self.device_ul.device_id, - entity_type=self.device_ul.entity_type) - context_command = NamedCommand(name=self.device_ul.commands[0].name, - value=False) + entity = httpc.cb.get_entity( + entity_id=self.device_ul.device_id, entity_type=self.device_ul.entity_type + ) + context_command = NamedCommand( + name=self.device_ul.commands[0].name, value=False + ) self.mqttc.loop_start() - httpc.cb.post_command(entity_id=entity.id, - entity_type=entity.type, - command=context_command) + httpc.cb.post_command( + entity_id=entity.id, entity_type=entity.type, command=context_command + ) time.sleep(5) # close the mqtt listening thread @@ -460,16 +507,19 @@ def on_command(client, obj, msg): # disconnect the mqtt device self.mqttc.disconnect() - entity = httpc.cb.get_entity(entity_id=self.device_ul.device_id, - entity_type=self.device_ul.entity_type) + entity = httpc.cb.get_entity( + entity_id=self.device_ul.device_id, entity_type=self.device_ul.entity_type + ) # The main part of this test, for all this setup was done self.assertEqual("OK", entity.heater_status.value) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_UL_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_UL_URL, + ) def test_publish_ultralight(self): """ Test for receiving commands for a specific device @@ -486,43 +536,50 @@ def test_publish_ultralight(self): self.mqttc.add_device(self.device_ul) from filip.clients.ngsi_v2 import HttpClient, HttpClientConfig - httpc_config = HttpClientConfig(cb_url=settings.CB_URL, - iota_url=settings.IOTA_UL_URL) - httpc = HttpClient(fiware_header=self.fiware_header, - config=httpc_config) - httpc.iota.post_group(service_group=self.service_group_ul, - update=True) + + httpc_config = HttpClientConfig( + cb_url=settings.CB_URL, iota_url=settings.IOTA_UL_URL + ) + httpc = HttpClient(fiware_header=self.fiware_header, config=httpc_config) + httpc.iota.post_group(service_group=self.service_group_ul, update=True) httpc.iota.post_device(device=self.device_ul, update=True) time.sleep(0.5) mqtt_broker_url = settings.MQTT_BROKER_URL - self.mqttc.connect(host=mqtt_broker_url.host, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + self.mqttc.connect( + host=mqtt_broker_url.host, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) self.mqttc.loop_start() - payload = randrange(0, 100, 1)/1000 + payload = randrange(0, 100, 1) / 1000 self.mqttc.publish( device_id=self.device_ul.device_id, - payload={self.device_ul.attributes[0].object_id: payload}) + payload={self.device_ul.attributes[0].object_id: payload}, + ) time.sleep(1) - entity = httpc.cb.get_entity(entity_id=self.device_ul.device_id, - entity_type=self.device_ul.entity_type) + entity = httpc.cb.get_entity( + entity_id=self.device_ul.device_id, entity_type=self.device_ul.entity_type + ) self.assertEqual(payload, entity.temperature.value) - payload = randrange(0, 100, 1)/1000 - self.mqttc.publish(device_id=self.device_ul.device_id, - attribute_name="temperature", - payload=payload) + payload = randrange(0, 100, 1) / 1000 + self.mqttc.publish( + device_id=self.device_ul.device_id, + attribute_name="temperature", + payload=payload, + ) time.sleep(1) - entity = httpc.cb.get_entity(entity_id=self.device_ul.device_id, - entity_type=self.device_ul.entity_type) + entity = httpc.cb.get_entity( + entity_id=self.device_ul.device_id, entity_type=self.device_ul.entity_type + ) self.assertEqual(payload, entity.temperature.value) # These test do currently not workt due to time stamp parsing @@ -534,7 +591,7 @@ def test_publish_ultralight(self): # entity = httpc.cb.get_entity(entity_id=self.device_ul.device_id, # entity_type=self.device_ul.entity_type) # self.assertEqual(50, entity.temperature.value) -# + # # from datetime import datetime, timedelta # timestamp = datetime.now() + timedelta(days=1) # timestamp = timestamp.astimezone().isoformat() @@ -559,6 +616,8 @@ def tearDown(self) -> None: """ Cleanup test server """ - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - iota_url=[settings.IOTA_JSON_URL, settings.IOTA_UL_URL]) + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + iota_url=[settings.IOTA_JSON_URL, settings.IOTA_UL_URL], + ) diff --git a/tests/clients/test_ngsi_ld_cb.py b/tests/clients/test_ngsi_ld_cb.py index c88a6ed4..f6bc3341 100644 --- a/tests/clients/test_ngsi_ld_cb.py +++ b/tests/clients/test_ngsi_ld_cb.py @@ -1,6 +1,7 @@ """ Tests for filip.cb.client """ + import unittest import logging import pyld @@ -9,8 +10,12 @@ from urllib3.util.retry import Retry from filip.clients.ngsi_ld.cb import ContextBrokerLDClient from filip.models.base import FiwareLDHeader, core_context -from filip.models.ngsi_ld.context import ActionTypeLD, ContextLDEntity, ContextProperty, \ - NamedContextProperty +from filip.models.ngsi_ld.context import ( + ActionTypeLD, + ContextLDEntity, + ContextProperty, + NamedContextProperty, +) from tests.config import settings import requests from filip.utils.cleanup import clear_context_broker_ld @@ -18,14 +23,15 @@ # Setting up logging logging.basicConfig( - level='ERROR', - format='%(asctime)s %(name)s %(levelname)s: %(message)s') + level="ERROR", format="%(asctime)s %(name)s %(levelname)s: %(message)s" +) class TestContextBroker(unittest.TestCase): """ Test class for ContextBrokerClient """ + def setUp(self) -> None: """ Setup test data @@ -34,14 +40,12 @@ def setUp(self) -> None: """ self.resources = { "entities_url": "/ngsi-ld/v1/entities", - "types_url": "/ngsi-ld/v1/types" + "types_url": "/ngsi-ld/v1/types", } - self.attr = { - 'testtemperature': { - 'type': 'Property', - 'value': 20.0} - } - self.entity = ContextLDEntity(id='urn:ngsi-ld:my:id4', type='MyType', **self.attr) + self.attr = {"testtemperature": {"type": "Property", "value": 20.0}} + self.entity = ContextLDEntity( + id="urn:ngsi-ld:my:id4", type="MyType", **self.attr + ) self.entity_2 = ContextLDEntity(id="urn:ngsi-ld:room2", type="room") self.fiware_header = FiwareLDHeader(ngsild_tenant=settings.FIWARE_SERVICE) # Set up retry strategy @@ -49,15 +53,21 @@ def setUp(self) -> None: retry_strategy = Retry( total=5, # Maximum number of retries backoff_factor=1, # Exponential backoff (1, 2, 4, 8, etc.) - status_forcelist=[429, 500, 502, 503, 504], # Retry on these HTTP status codes + status_forcelist=[ + 429, + 500, + 502, + 503, + 504, + ], # Retry on these HTTP status codes ) # Set the HTTP adapter with retry strategy adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("https://", adapter) session.mount("http://", adapter) - self.client = ContextBrokerLDClient(fiware_header=self.fiware_header, - session=session, - url=settings.LD_CB_URL) + self.client = ContextBrokerLDClient( + fiware_header=self.fiware_header, session=session, url=settings.LD_CB_URL + ) clear_context_broker_ld(cb_ld_client=self.client) def tearDown(self) -> None: @@ -67,7 +77,6 @@ def tearDown(self) -> None: clear_context_broker_ld(cb_ld_client=self.client) self.client.close() - @unittest.skip("Only for local testing environment") def test_not_existing_tenant(self): """ @@ -77,50 +86,55 @@ def test_not_existing_tenant(self): """ # create uuid for the tenant import uuid - tenant = str(uuid.uuid4()).split('-')[0] + + tenant = str(uuid.uuid4()).split("-")[0] fiware_header = FiwareLDHeader(ngsild_tenant=tenant) - client = ContextBrokerLDClient(fiware_header=fiware_header, - url=settings.LD_CB_URL) + client = ContextBrokerLDClient( + fiware_header=fiware_header, url=settings.LD_CB_URL + ) entities = client.get_entity_list() self.assertEqual(len(entities), 0) - def test_get_entities_pagination(self): """ Test pagination of get entities """ init_numb = 2000 - entities_a = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:TypeA') for i in - range(0, init_numb)] - - self.client.entity_batch_operation(action_type=ActionTypeLD.CREATE, - entities=entities_a) - + entities_a = [ + ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", type=f"filip:object:TypeA") + for i in range(0, init_numb) + ] + + self.client.entity_batch_operation( + action_type=ActionTypeLD.CREATE, entities=entities_a + ) + entity_list = self.client.get_entity_list(limit=1) - self.assertEqual(len(entity_list),1) - + self.assertEqual(len(entity_list), 1) + entity_list = self.client.get_entity_list(limit=400) - self.assertEqual(len(entity_list),400) - + self.assertEqual(len(entity_list), 400) + entity_list = self.client.get_entity_list(limit=800) - self.assertEqual(len(entity_list),800) - + self.assertEqual(len(entity_list), 800) + entity_list = self.client.get_entity_list(limit=1000) - self.assertEqual(len(entity_list),1000) + self.assertEqual(len(entity_list), 1000) # currently, there is a limit of 1000 entities per delete request - self.client.entity_batch_operation(action_type=ActionTypeLD.DELETE, - entities=entities_a[0:800]) - self.client.entity_batch_operation(action_type=ActionTypeLD.DELETE, - entities=entities_a[800:1600]) + self.client.entity_batch_operation( + action_type=ActionTypeLD.DELETE, entities=entities_a[0:800] + ) + self.client.entity_batch_operation( + action_type=ActionTypeLD.DELETE, entities=entities_a[800:1600] + ) entity_list = self.client.get_entity_list(limit=1000) self.assertEqual(len(entity_list), init_numb - 1600) def test_get_entites(self): """ Retrieve a set of entities which matches a specific query from an NGSI-LD system - Args: + Args: - id(string): Comma separated list of URIs to be retrieved - idPattern(string): Regular expression that must be matched by Entity ids - type(string): Comma separated list of Entity type names to be retrieved @@ -139,7 +153,8 @@ def test_get_entites(self): self.client.post_entity(entity=self.entity) entity_list_idpattern = self.client.get_entity_list( - id_pattern="urn:ngsi-ld:my*") + id_pattern="urn:ngsi-ld:my*" + ) self.assertEqual(len(entity_list_idpattern), 1) self.assertEqual(entity_list_idpattern[0].id, self.entity.id) @@ -163,15 +178,15 @@ def test_post_entity(self): modifiedAt: string($date_time) <*>: Property{} Relationship{} - GeoProperty{} + GeoProperty{} } - Returns: + Returns: - (201) Created. Contains the resource URI of the created Entity - (400) Bad request. - (409) Already exists. - (422) Unprocessable Entity. Tests: - - Post an entity -> Does it return 201? + - Post an entity -> Does it return 201? - Post an entity again -> Does it return 409? - Post an entity without requires args -> Does it return 422? """ @@ -181,8 +196,9 @@ def test_post_entity(self): self.assertEqual(len(entity_list), 1) self.assertEqual(entity_list[0].id, self.entity.id) self.assertEqual(entity_list[0].type, self.entity.type) - self.assertEqual(entity_list[0].testtemperature.value, - self.entity.testtemperature.value) + self.assertEqual( + entity_list[0].testtemperature.value, self.entity.testtemperature.value + ) # existed entity self.entity_identical = self.entity.model_copy() @@ -192,34 +208,36 @@ def test_post_entity(self): self.assertEqual(response.status_code, 409) entity_list = self.client.get_entity_list( - entity_type=self.entity_identical.type) + entity_type=self.entity_identical.type + ) self.assertEqual(len(entity_list), 1) # append new attribute to existed entity self.entity_append = self.entity.model_copy() - self.entity_append.delete_properties(['testtemperature']) + self.entity_append.delete_properties(["testtemperature"]) self.entity_append.add_properties( - {'humidity': ContextProperty(**{ - 'type': 'Property', - 'value': 50})}) + {"humidity": ContextProperty(**{"type": "Property", "value": 50})} + ) self.client.post_entity(entity=self.entity_append, append=True) entity_append_res = self.client.get_entity(entity_id=self.entity_append.id) - self.assertEqual(entity_append_res.humidity.value, - self.entity_append.humidity.value) - self.assertEqual(entity_append_res.testtemperature.value, - self.entity.testtemperature.value) + self.assertEqual( + entity_append_res.humidity.value, self.entity_append.humidity.value + ) + self.assertEqual( + entity_append_res.testtemperature.value, self.entity.testtemperature.value + ) # override existed entity - new_attr = {'newattr': - {'type': 'Property', 'value': 999} - } + new_attr = {"newattr": {"type": "Property", "value": 999}} self.entity_override = ContextLDEntity( - id=self.entity.id, type=self.entity.type, **new_attr) + id=self.entity.id, type=self.entity.type, **new_attr + ) self.client.post_entity(entity=self.entity_override, update=True) entity_override_res = self.client.get_entity(entity_id=self.entity.id) - self.assertEqual(entity_override_res.newattr.value, - self.entity_override.newattr.value) - self.assertNotIn('testtemperature', entity_override_res.model_dump()) + self.assertEqual( + entity_override_res.newattr.value, self.entity_override.newattr.value + ) + self.assertNotIn("testtemperature", entity_override_res.model_dump()) # post without entity type is not allowed with self.assertRaises(Exception): @@ -228,8 +246,9 @@ def test_post_entity(self): self.assertNotIn("room2", entity_list) """delete""" - self.client.entity_batch_operation(entities=entity_list, - action_type=ActionTypeLD.DELETE) + self.client.entity_batch_operation( + entities=entity_list, action_type=ActionTypeLD.DELETE + ) def test_get_entity(self): """ @@ -243,10 +262,10 @@ def test_get_entity(self): - (200) Entity - (400) Bad request - (404) Not found - Tests for get entity: + Tests for get entity: - Post entity and see if get entity with the same ID returns the entity with the correct values - - Get entity with an ID that does not exit. See if Not found error is + - Get entity with an ID that does not exit. See if Not found error is raised """ @@ -267,12 +286,15 @@ def test_get_entity(self): """Test1""" self.client.post_entity(entity=self.entity) ret_entity = self.client.get_entity(entity_id=self.entity.id) - ret_entity_with_type = self.client.get_entity(entity_id=self.entity.id, - entity_type=self.entity.type) - ret_entity_keyValues = self.client.get_entity(entity_id=self.entity.id, - options="keyValues") - ret_entity_sysAttrs = self.client.get_entity(entity_id=self.entity.id, - options="sysAttrs") + ret_entity_with_type = self.client.get_entity( + entity_id=self.entity.id, entity_type=self.entity.type + ) + ret_entity_keyValues = self.client.get_entity( + entity_id=self.entity.id, options="keyValues" + ) + ret_entity_sysAttrs = self.client.get_entity( + entity_id=self.entity.id, options="sysAttrs" + ) self.assertEqual(ret_entity.id, self.entity.id) self.assertEqual(ret_entity.type, self.entity.type) @@ -303,77 +325,81 @@ def test_different_context(self): temperature_sensor_dict = { "id": "urn:ngsi-ld:temperatureSensor", "type": "TemperatureSensor", - "temperature": { - "type": "Property", - "value": 23, - "unitCode": "CEL" - } + "temperature": {"type": "Property", "value": 23, "unitCode": "CEL"}, } # client with custom context - custom_context = "https://n5geh.github.io/n5geh.test-context.io/context_saref.jsonld" + custom_context = ( + "https://n5geh.github.io/n5geh.test-context.io/context_saref.jsonld" + ) custom_header = FiwareLDHeader( ngsild_tenant=settings.FIWARE_SERVICE, ) custom_header.set_context(custom_context) client_custom_context = ContextBrokerLDClient( - fiware_header=custom_header, - url=settings.LD_CB_URL) + fiware_header=custom_header, url=settings.LD_CB_URL + ) # default context temperature_sensor = ContextLDEntity(**temperature_sensor_dict) self.client.post_entity(entity=temperature_sensor) entity_default = self.client.get_entity(entity_id=temperature_sensor.id) - self.assertEqual(entity_default.context, - core_context) - self.assertEqual(entity_default.model_dump(exclude_unset=True, - exclude={"context"}), - temperature_sensor_dict) + self.assertEqual(entity_default.context, core_context) + self.assertEqual( + entity_default.model_dump(exclude_unset=True, exclude={"context"}), + temperature_sensor_dict, + ) entity_custom_context = client_custom_context.get_entity( - entity_id=temperature_sensor.id) - self.assertEqual(entity_custom_context.context, - custom_context) - self.assertEqual(entity_custom_context.model_dump(exclude_unset=True, - exclude={"context"}), - temperature_sensor_dict) + entity_id=temperature_sensor.id + ) + self.assertEqual(entity_custom_context.context, custom_context) + self.assertEqual( + entity_custom_context.model_dump(exclude_unset=True, exclude={"context"}), + temperature_sensor_dict, + ) self.client.delete_entity_by_id(entity_id=temperature_sensor.id) # custom context in client temperature_sensor = ContextLDEntity(**temperature_sensor_dict) client_custom_context.post_entity(entity=temperature_sensor) - entity_custom = client_custom_context.get_entity(entity_id=temperature_sensor.id) - self.assertEqual(entity_custom.context, - custom_context) - self.assertEqual(entity_custom.model_dump(exclude_unset=True, - exclude={"context"}), - temperature_sensor_dict) + entity_custom = client_custom_context.get_entity( + entity_id=temperature_sensor.id + ) + self.assertEqual(entity_custom.context, custom_context) + self.assertEqual( + entity_custom.model_dump(exclude_unset=True, exclude={"context"}), + temperature_sensor_dict, + ) entity_default_context = self.client.get_entity(entity_id=temperature_sensor.id) - self.assertEqual(entity_default_context.context, - core_context) + self.assertEqual(entity_default_context.context, core_context) self.assertNotEqual( - entity_default_context.model_dump(exclude_unset=True, - exclude={"context"}), - temperature_sensor_dict) + entity_default_context.model_dump(exclude_unset=True, exclude={"context"}), + temperature_sensor_dict, + ) client_custom_context.delete_entity_by_id(entity_id=temperature_sensor.id) # custom context in entity temperature_sensor = ContextLDEntity( - context=["https://n5geh.github.io/n5geh.test-context.io/context_saref.jsonld"], - **temperature_sensor_dict) + context=[ + "https://n5geh.github.io/n5geh.test-context.io/context_saref.jsonld" + ], + **temperature_sensor_dict, + ) self.client.post_entity(entity=temperature_sensor) - entity_custom = client_custom_context.get_entity(entity_id=temperature_sensor.id) - self.assertEqual(entity_custom.context, - custom_context) - self.assertEqual(entity_custom.model_dump(exclude_unset=True, - exclude={"context"}), - temperature_sensor_dict) + entity_custom = client_custom_context.get_entity( + entity_id=temperature_sensor.id + ) + self.assertEqual(entity_custom.context, custom_context) + self.assertEqual( + entity_custom.model_dump(exclude_unset=True, exclude={"context"}), + temperature_sensor_dict, + ) entity_default_context = self.client.get_entity(entity_id=temperature_sensor.id) - self.assertEqual(entity_default_context.context, - core_context) + self.assertEqual(entity_default_context.context, core_context) self.assertNotEqual( - entity_default_context.model_dump(exclude_unset=True, - exclude={"context"}), - temperature_sensor_dict) + entity_default_context.model_dump(exclude_unset=True, exclude={"context"}), + temperature_sensor_dict, + ) self.client.delete_entity_by_id(entity_id=temperature_sensor.id) def test_delete_entity(self): @@ -382,7 +408,7 @@ def test_delete_entity(self): Args: - entityID(string): Entity ID; required - type(string): Entity Type - Returns: + Returns: - (204) No Content. The entity was removed successfully. - (400) Bad request. - (404) Not found. @@ -444,11 +470,11 @@ def test_delete_entity(self): def test_add_attributes_entity(self): """ Append new Entity attributes to an existing Entity within an NGSI-LD system. - Args: + Args: - entityID(string): Entity ID; required - - options(string): Indicates that no attribute overwrite shall be performed. + - options(string): Indicates that no attribute overwrite shall be performed. Available values: noOverwrite - Returns: + Returns: - (204) No Content - (207) Partial Success. Only the attributes included in the response payload were successfully appended. - (400) Bad Request @@ -479,7 +505,7 @@ def test_add_attributes_entity(self): """ """Test 1""" self.client.post_entity(self.entity) - attr = ContextProperty(**{'value': 20, 'unitCode': 'Number'}) + attr = ContextProperty(**{"value": 20, "unitCode": "Number"}) self.entity.add_properties({"test_value": attr}) self.client.append_entity_attributes(self.entity) @@ -489,7 +515,7 @@ def test_add_attributes_entity(self): self.client.delete_entity_by_id(entity_id=entity.id) """Test 2""" - attr = ContextProperty(**{'value': 20, 'type': 'Property'}) + attr = ContextProperty(**{"value": 20, "type": "Property"}) with self.assertRaises(Exception): self.entity.add_properties({"test_value": attr}) self.client.append_entity_attributes(self.entity) @@ -497,8 +523,8 @@ def test_add_attributes_entity(self): """Test 3""" self.client.post_entity(self.entity) # What makes an property/ attribute unique ??? - attr = ContextProperty(**{'value': 20, 'type': 'Property'}) - attr_same = ContextProperty(**{'value': 40, 'type': 'Property'}) + attr = ContextProperty(**{"value": 20, "type": "Property"}) + attr_same = ContextProperty(**{"value": 40, "type": "Property"}) self.entity.add_properties({"test_value": attr}) self.client.append_entity_attributes(self.entity) @@ -533,13 +559,14 @@ def test_patch_entity_attrs(self): Raise Error """ """Test1""" - new_prop = {'new_prop': ContextProperty(value=25)} - newer_prop = NamedContextProperty(value=40, name='new_prop') + new_prop = {"new_prop": ContextProperty(value=25)} + newer_prop = NamedContextProperty(value=40, name="new_prop") self.entity.add_properties(new_prop) self.client.post_entity(entity=self.entity) - self.client.update_entity_attribute(entity_id=self.entity.id, attr=newer_prop, - attr_name='new_prop') + self.client.update_entity_attribute( + entity_id=self.entity.id, attr=newer_prop, attr_name="new_prop" + ) entity = self.client.get_entity(entity_id=self.entity.id, options="keyValues") prop_dict = entity.model_dump() self.assertIn("new_prop", prop_dict) @@ -568,13 +595,14 @@ def test_patch_entity_attrs_contextprop(self): Raise Error """ """Test1""" - new_prop = {'new_prop': ContextProperty(value=25)} + new_prop = {"new_prop": ContextProperty(value=25)} newer_prop = ContextProperty(value=55) self.entity.add_properties(new_prop) self.client.post_entity(entity=self.entity) - self.client.update_entity_attribute(entity_id=self.entity.id, attr=newer_prop, - attr_name='new_prop') + self.client.update_entity_attribute( + entity_id=self.entity.id, attr=newer_prop, attr_name="new_prop" + ) entity = self.client.get_entity(entity_id=self.entity.id, options="keyValues") prop_dict = entity.model_dump() self.assertIn("new_prop", prop_dict) @@ -583,10 +611,10 @@ def test_patch_entity_attrs_contextprop(self): def test_patch_entity_attrs_attrId(self): """ Update existing Entity attribute ID within an NGSI-LD system - Args: + Args: - entityId(string): Entity Id; required - attrId(string): Attribute Id; required - Returns: + Returns: - (204) No Content - (400) Bad Request - (404) Not Found @@ -602,14 +630,14 @@ def test_patch_entity_attrs_attrId(self): Raise Error """ """Test 1""" - attr = NamedContextProperty(name="test_value", - value=20) + attr = NamedContextProperty(name="test_value", value=20) self.entity.add_properties(attrs=[attr]) self.client.post_entity(entity=self.entity) attr.value = 40 - self.client.update_entity_attribute(entity_id=self.entity.id, attr=attr, - attr_name="test_value") + self.client.update_entity_attribute( + entity_id=self.entity.id, attr=attr, attr_name="test_value" + ) entity = self.client.get_entity(entity_id=self.entity.id, options="keyValues") prop_dict = entity.model_dump() self.assertIn("test_value", prop_dict) @@ -626,10 +654,10 @@ def test_delete_entity_attribute(self): - (400) Bad Request - (404) Not Found Tests: - - Post an entity with attributes. Try to delete non existent attribute with non existent attribute - id. Then check response code. - - Post an entity with attributes. Try to delete one the attributes. Test if the attribute is really - removed by either posting the entity or by trying to delete it again. + - Post an entity with attributes. Try to delete non existent attribute with non existent attribute + id. Then check response code. + - Post an entity with attributes. Try to delete one the attributes. Test if the attribute is really + removed by either posting the entity or by trying to delete it again. """ """ Test 1: @@ -647,13 +675,13 @@ def test_delete_entity_attribute(self): """ """Test 1""" - attr = NamedContextProperty(name="test_value", - value=20) + attr = NamedContextProperty(name="test_value", value=20) self.entity.add_properties(attrs=[attr]) self.client.post_entity(entity=self.entity) with self.assertRaises(Exception): - self.client.delete_attribute(entity_id=self.entity.id, - attribute_id="does_not_exist") + self.client.delete_attribute( + entity_id=self.entity.id, attribute_id="does_not_exist" + ) entity_list = self.client.get_entity_list() @@ -661,16 +689,17 @@ def test_delete_entity_attribute(self): self.client.delete_entity_by_id(entity_id=entity.id) """Test 2""" - attr = NamedContextProperty(name="test_value", - value=20) + attr = NamedContextProperty(name="test_value", value=20) self.entity.add_properties(attrs=[attr]) self.client.post_entity(entity=self.entity) - self.client.delete_attribute(entity_id=self.entity.id, - attribute_id="test_value") + self.client.delete_attribute( + entity_id=self.entity.id, attribute_id="test_value" + ) with self.assertRaises(requests.exceptions.HTTPError) as contextmanager: - self.client.delete_attribute(entity_id=self.entity.id, - attribute_id="test_value") + self.client.delete_attribute( + entity_id=self.entity.id, attribute_id="test_value" + ) response = contextmanager.exception.response self.assertEqual(response.status_code, 404) diff --git a/tests/clients/test_ngsi_ld_entity_batch_operation.py b/tests/clients/test_ngsi_ld_entity_batch_operation.py index da1397c2..4fba6105 100644 --- a/tests/clients/test_ngsi_ld_entity_batch_operation.py +++ b/tests/clients/test_ngsi_ld_entity_batch_operation.py @@ -5,6 +5,7 @@ from pydantic import ValidationError from filip.models.base import FiwareLDHeader + # FiwareLDHeader issue with pydantic from filip.clients.ngsi_ld.cb import ContextBrokerLDClient from filip.models.ngsi_ld.context import ContextLDEntity, ActionTypeLD @@ -27,8 +28,9 @@ def setUp(self) -> None: """ self.r = Random() self.fiware_header = FiwareLDHeader(ngsild_tenant=settings.FIWARE_SERVICE) - self.cb_client = ContextBrokerLDClient(fiware_header=self.fiware_header, - url=settings.LD_CB_URL) + self.cb_client = ContextBrokerLDClient( + fiware_header=self.fiware_header, url=settings.LD_CB_URL + ) clear_context_broker_ld(cb_ld_client=self.cb_client) def tearDown(self) -> None: @@ -43,7 +45,7 @@ def test_entity_batch_operations_create(self) -> None: Batch Entity creation. Args: - Request body(Entity List); required - Returns: + Returns: - (200) Success - (400) Bad Request Tests: @@ -64,11 +66,14 @@ def test_entity_batch_operations_create(self) -> None: if not raise assert """ """Test 1""" - entities_a = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test') for i in - range(0, 10)] - self.cb_client.entity_batch_operation(entities=entities_a, action_type=ActionTypeLD.CREATE) - entity_list = self.cb_client.get_entity_list(entity_type=f'filip:object:test') + entities_a = [ + ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", type=f"filip:object:test") + for i in range(0, 10) + ] + self.cb_client.entity_batch_operation( + entities=entities_a, action_type=ActionTypeLD.CREATE + ) + entity_list = self.cb_client.get_entity_list(entity_type=f"filip:object:test") id_list = [entity.id for entity in entity_list] self.assertEqual(len(entities_a), len(entity_list)) for entity in entities_a: @@ -78,15 +83,18 @@ def test_entity_batch_operations_create(self) -> None: self.cb_client.delete_entity_by_id(entity_id=entity.id) """Test 2""" - entities_b = [ContextLDEntity(id=f"urn:ngsi-ld:test:eins", - type=f'filip:object:test'), - ContextLDEntity(id=f"urn:ngsi-ld:test:eins", - type=f'filip:object:test')] + entities_b = [ + ContextLDEntity(id=f"urn:ngsi-ld:test:eins", type=f"filip:object:test"), + ContextLDEntity(id=f"urn:ngsi-ld:test:eins", type=f"filip:object:test"), + ] entity_list_b = [] try: - self.cb_client.entity_batch_operation(entities=entities_b, action_type=ActionTypeLD.CREATE) + self.cb_client.entity_batch_operation( + entities=entities_b, action_type=ActionTypeLD.CREATE + ) entity_list_b = self.cb_client.get_entity_list( - entity_type=f'filip:object:test') + entity_type=f"filip:object:test" + ) self.assertEqual(len(entity_list), 1) except: pass @@ -96,16 +104,16 @@ def test_entity_batch_operations_create(self) -> None: def test_entity_batch_operations_update(self) -> None: """ - Batch Entity update. + Batch Entity update. Args: - options(string): Available values: noOverwrite - Request body(EntityList); required - Returns: + Returns: - (200) Success - (400) Bad Request Tests: - Post the update of batch entities. Check if each of the updated entities exists and if the updates appear. - - Try the same with the noOverwrite statement and check if the nooverwrite is acknowledged. + - Try the same with the noOverwrite statement and check if the nooverwrite is acknowledged. """ """ Test 1: @@ -125,81 +133,113 @@ def test_entity_batch_operations_update(self) -> None: """ """Test 1""" - entities_a = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test', - **{'temperature': { - 'value': self.r.randint(20,50), - "type": "Property" - }}) for i in - range(0, 5)] - - self.cb_client.entity_batch_operation(entities=entities_a, action_type=ActionTypeLD.CREATE) - - entities_update = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test', - **{'temperature': - {'value': self.r.randint(0,20), - "type": "Property" - }}) for i in - range(3, 6)] - self.cb_client.entity_batch_operation(entities=entities_update, action_type=ActionTypeLD.UPDATE) - entity_list = self.cb_client.get_entity_list(entity_type=f'filip:object:test') - self.assertEqual(len(entity_list),5) - updated = [x.model_dump(exclude_unset=True, exclude={"context"}) - for x in entity_list if int(x.id.split(':')[3]) in range(3,5)] - nupdated = [x.model_dump(exclude_unset=True, exclude={"context"}) - for x in entity_list if int(x.id.split(':')[3]) in range(0,3)] - - self.assertCountEqual([entity.model_dump(exclude_unset=True) - for entity in entities_a[0:3]], - nupdated) - - self.assertCountEqual([entity.model_dump(exclude_unset=True) - for entity in entities_update[0:2]], - updated) + entities_a = [ + ContextLDEntity( + id=f"urn:ngsi-ld:test:{str(i)}", + type=f"filip:object:test", + **{ + "temperature": {"value": self.r.randint(20, 50), "type": "Property"} + }, + ) + for i in range(0, 5) + ] + + self.cb_client.entity_batch_operation( + entities=entities_a, action_type=ActionTypeLD.CREATE + ) + + entities_update = [ + ContextLDEntity( + id=f"urn:ngsi-ld:test:{str(i)}", + type=f"filip:object:test", + **{"temperature": {"value": self.r.randint(0, 20), "type": "Property"}}, + ) + for i in range(3, 6) + ] + self.cb_client.entity_batch_operation( + entities=entities_update, action_type=ActionTypeLD.UPDATE + ) + entity_list = self.cb_client.get_entity_list(entity_type=f"filip:object:test") + self.assertEqual(len(entity_list), 5) + updated = [ + x.model_dump(exclude_unset=True, exclude={"context"}) + for x in entity_list + if int(x.id.split(":")[3]) in range(3, 5) + ] + nupdated = [ + x.model_dump(exclude_unset=True, exclude={"context"}) + for x in entity_list + if int(x.id.split(":")[3]) in range(0, 3) + ] + + self.assertCountEqual( + [entity.model_dump(exclude_unset=True) for entity in entities_a[0:3]], + nupdated, + ) + + self.assertCountEqual( + [entity.model_dump(exclude_unset=True) for entity in entities_update[0:2]], + updated, + ) """Test 2""" # presssure will be appended while the existing temperature will # not be overwritten - entities_update = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test', - **{'temperature': - {'value': self.r.randint(50, 100), - "type": "Property"}, - 'pressure': { - 'value': self.r.randint(1,100), - "type": "Property" - } - }) for i in range(0, 5)] - - self.cb_client.entity_batch_operation(entities=entities_update, - action_type=ActionTypeLD.UPDATE, - options="noOverwrite") - + entities_update = [ + ContextLDEntity( + id=f"urn:ngsi-ld:test:{str(i)}", + type=f"filip:object:test", + **{ + "temperature": { + "value": self.r.randint(50, 100), + "type": "Property", + }, + "pressure": {"value": self.r.randint(1, 100), "type": "Property"}, + }, + ) + for i in range(0, 5) + ] + + self.cb_client.entity_batch_operation( + entities=entities_update, + action_type=ActionTypeLD.UPDATE, + options="noOverwrite", + ) + previous = entity_list - previous.sort(key=lambda x: int(x.id.split(':')[3])) - - entity_list = self.cb_client.get_entity_list(entity_type=f'filip:object:test') - entity_list.sort(key=lambda x: int(x.id.split(':')[3])) - - self.assertEqual(len(entity_list),len(entities_update)) - - for (updated,entity,prev) in zip(entities_update,entity_list,previous): - self.assertEqual(updated.model_dump().get('pressure'), - entity.model_dump().get('pressure')) - self.assertNotEqual(updated.model_dump().get('temperature'), - entity.model_dump().get('temperature')) - self.assertEqual(prev.model_dump().get('temperature'), - entity.model_dump().get('temperature')) + previous.sort(key=lambda x: int(x.id.split(":")[3])) + + entity_list = self.cb_client.get_entity_list(entity_type=f"filip:object:test") + entity_list.sort(key=lambda x: int(x.id.split(":")[3])) + + self.assertEqual(len(entity_list), len(entities_update)) + + for updated, entity, prev in zip(entities_update, entity_list, previous): + self.assertEqual( + updated.model_dump().get("pressure"), + entity.model_dump().get("pressure"), + ) + self.assertNotEqual( + updated.model_dump().get("temperature"), + entity.model_dump().get("temperature"), + ) + self.assertEqual( + prev.model_dump().get("temperature"), + entity.model_dump().get("temperature"), + ) with self.assertRaises(HTTPError): - self.cb_client.entity_batch_operation(entities=[],action_type=ActionTypeLD.UPDATE) - + self.cb_client.entity_batch_operation( + entities=[], action_type=ActionTypeLD.UPDATE + ) + # according to spec, this should raise bad request data, # but pydantic is intercepting with self.assertRaises(ValidationError): - self.cb_client.entity_batch_operation(entities=[None],action_type=ActionTypeLD.UPDATE) - + self.cb_client.entity_batch_operation( + entities=[None], action_type=ActionTypeLD.UPDATE + ) + for entity in entity_list: self.cb_client.delete_entity_by_id(entity_id=entity.id) @@ -209,11 +249,11 @@ def test_entity_batch_operations_upsert(self) -> None: Args: - options(string): Available values: replace, update - Request body(EntityList); required - Returns: + Returns: - (200) Success - (400) Bad request - Tests: - - Post entity list and then post the upsert with update and replace. + Tests: + - Post entity list and then post the upsert with update and replace. - Get the entitiy list and see if the results are correct. """ @@ -233,80 +273,113 @@ def test_entity_batch_operations_upsert(self) -> None: """ """Test 1""" # create entities 1 -3 - entities_a = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test', - **{'temperature': - {'value': self.r.randint(0,20), - "type": "Property" - }}) for i in - range(1, 4)] - self.cb_client.entity_batch_operation(entities=entities_a, action_type=ActionTypeLD.CREATE) + entities_a = [ + ContextLDEntity( + id=f"urn:ngsi-ld:test:{str(i)}", + type=f"filip:object:test", + **{"temperature": {"value": self.r.randint(0, 20), "type": "Property"}}, + ) + for i in range(1, 4) + ] + self.cb_client.entity_batch_operation( + entities=entities_a, action_type=ActionTypeLD.CREATE + ) # replace entities 0 - 1 - entities_replace = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test', - **{'pressure': - {'value': self.r.randint(50,100), - "type": "Property" - }}) for i in - range(0, 2)] - self.cb_client.entity_batch_operation(entities=entities_replace, action_type=ActionTypeLD.UPSERT, - options="replace") + entities_replace = [ + ContextLDEntity( + id=f"urn:ngsi-ld:test:{str(i)}", + type=f"filip:object:test", + **{"pressure": {"value": self.r.randint(50, 100), "type": "Property"}}, + ) + for i in range(0, 2) + ] + self.cb_client.entity_batch_operation( + entities=entities_replace, + action_type=ActionTypeLD.UPSERT, + options="replace", + ) # update entities 3 - 4, # pressure will be appended for 3 # temperature will be appended for 4 - entities_update = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test', - **{'pressure': - {'value': self.r.randint(50,100), - "type": "Property" - }}) for i in - range(3, 5)] - self.cb_client.entity_batch_operation(entities=entities_update, - action_type=ActionTypeLD.UPSERT, - options="update") - + entities_update = [ + ContextLDEntity( + id=f"urn:ngsi-ld:test:{str(i)}", + type=f"filip:object:test", + **{"pressure": {"value": self.r.randint(50, 100), "type": "Property"}}, + ) + for i in range(3, 5) + ] + self.cb_client.entity_batch_operation( + entities=entities_update, action_type=ActionTypeLD.UPSERT, options="update" + ) + # 0,1 and 4 should have pressure only # 2 should have temperature only # 3 should have both # can be made modular for variable size batches entity_list = self.cb_client.get_entity_list() - self.assertEqual(len(entity_list),5) + self.assertEqual(len(entity_list), 5) for _e in entity_list: - _id = int(_e.id.split(':')[3]) - e = _e.model_dump(exclude_unset=True, exclude={'context'}) - if _id in [0,1]: - self.assertIsNone(e.get('temperature',None)) - self.assertIsNotNone(e.get('pressure',None)) - self.assertCountEqual([e], - [x.model_dump(exclude_unset=True) - for x in entities_replace if x.id == _e.id]) + _id = int(_e.id.split(":")[3]) + e = _e.model_dump(exclude_unset=True, exclude={"context"}) + if _id in [0, 1]: + self.assertIsNone(e.get("temperature", None)) + self.assertIsNotNone(e.get("pressure", None)) + self.assertCountEqual( + [e], + [ + x.model_dump(exclude_unset=True) + for x in entities_replace + if x.id == _e.id + ], + ) elif _id == 4: - self.assertIsNone(e.get('temperature',None)) - self.assertIsNotNone(e.get('pressure',None)) - self.assertCountEqual([e], - [x.model_dump(exclude_unset=True) - for x in entities_update if x.id == _e.id]) + self.assertIsNone(e.get("temperature", None)) + self.assertIsNotNone(e.get("pressure", None)) + self.assertCountEqual( + [e], + [ + x.model_dump(exclude_unset=True) + for x in entities_update + if x.id == _e.id + ], + ) elif _id == 2: - self.assertIsNone(e.get('pressure',None)) - self.assertIsNotNone(e.get('temperature',None)) - self.assertCountEqual([e], - [x.model_dump(exclude_unset=True) - for x in entities_a if x.id == _e.id]) + self.assertIsNone(e.get("pressure", None)) + self.assertIsNotNone(e.get("temperature", None)) + self.assertCountEqual( + [e], + [ + x.model_dump(exclude_unset=True) + for x in entities_a + if x.id == _e.id + ], + ) elif _id == 3: - self.assertIsNotNone(e.get('temperature',None)) - self.assertIsNotNone(e.get('pressure',None)) - self.assertCountEqual([e.get('temperature')], - [x.model_dump(exclude_unset=True).get('temperature') - for x in entities_a if x.id == _e.id]) - self.assertCountEqual([e.get('pressure')], - [x.model_dump(exclude_unset=True).get('pressure') - for x in entities_update if x.id == _e.id]) - + self.assertIsNotNone(e.get("temperature", None)) + self.assertIsNotNone(e.get("pressure", None)) + self.assertCountEqual( + [e.get("temperature")], + [ + x.model_dump(exclude_unset=True).get("temperature") + for x in entities_a + if x.id == _e.id + ], + ) + self.assertCountEqual( + [e.get("pressure")], + [ + x.model_dump(exclude_unset=True).get("pressure") + for x in entities_update + if x.id == _e.id + ], + ) + for entity in entity_list: self.cb_client.delete_entity_by_id(entity_id=entity.id) - + def test_entity_batch_operations_delete(self) -> None: """ Batch entity delete. @@ -316,8 +389,8 @@ def test_entity_batch_operations_delete(self) -> None: - (200) Success - (400) Bad request Tests: - - Try to delete non existent entity. - - Try to delete existent entity and check if it is deleted. + - Try to delete non existent entity. + - Try to delete existent entity and check if it is deleted. """ """ Test 1: @@ -334,30 +407,36 @@ def test_entity_batch_operations_delete(self) -> None: Raise Error: """ """Test 1""" - entities_delete = [ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", - type=f'filip:object:test') for i in - range(0, 1)] + entities_delete = [ + ContextLDEntity(id=f"urn:ngsi-ld:test:{str(i)}", type=f"filip:object:test") + for i in range(0, 1) + ] with self.assertRaises(Exception): - self.cb_client.entity_batch_operation(entities=entities_delete, - action_type=ActionTypeLD.DELETE) + self.cb_client.entity_batch_operation( + entities=entities_delete, action_type=ActionTypeLD.DELETE + ) """Test 2""" - entity_del_type = 'filip:object:test' - entities_ids_a = [f"urn:ngsi-ld:test:{str(i)}" for i in - range(0, 4)] - entities_a = [ContextLDEntity(id=id_a, - type=entity_del_type) for id_a in - entities_ids_a] - - self.cb_client.entity_batch_operation(entities=entities_a, action_type=ActionTypeLD.CREATE) - - entities_delete = [ContextLDEntity(id=id_a, - type=entity_del_type) for id_a in - entities_ids_a[:3]] + entity_del_type = "filip:object:test" + entities_ids_a = [f"urn:ngsi-ld:test:{str(i)}" for i in range(0, 4)] + entities_a = [ + ContextLDEntity(id=id_a, type=entity_del_type) for id_a in entities_ids_a + ] + + self.cb_client.entity_batch_operation( + entities=entities_a, action_type=ActionTypeLD.CREATE + ) + + entities_delete = [ + ContextLDEntity(id=id_a, type=entity_del_type) + for id_a in entities_ids_a[:3] + ] entities_delete_ids = [entity.id for entity in entities_delete] # send update to delete entities - self.cb_client.entity_batch_operation(entities=entities_delete, action_type=ActionTypeLD.DELETE) + self.cb_client.entity_batch_operation( + entities=entities_delete, action_type=ActionTypeLD.DELETE + ) # get list of entities which is still stored entity_list = self.cb_client.get_entity_list(entity_type=entity_del_type) diff --git a/tests/clients/test_ngsi_ld_query.py b/tests/clients/test_ngsi_ld_query.py index c8050485..3f98eae2 100644 --- a/tests/clients/test_ngsi_ld_query.py +++ b/tests/clients/test_ngsi_ld_query.py @@ -1,6 +1,7 @@ """ Tests for filip.cb.client """ + import unittest import logging import re @@ -11,8 +12,13 @@ from requests import RequestException from filip.clients.ngsi_ld.cb import ContextBrokerLDClient from filip.models.base import FiwareLDHeader -from filip.models.ngsi_ld.context import ActionTypeLD, ContextLDEntity, ContextProperty, \ - NamedContextProperty, NamedContextRelationship +from filip.models.ngsi_ld.context import ( + ActionTypeLD, + ContextLDEntity, + ContextProperty, + NamedContextProperty, + NamedContextRelationship, +) from tests.config import settings from random import Random from filip.utils.cleanup import clear_context_broker_ld @@ -20,96 +26,100 @@ # Setting up logging logging.basicConfig( - level='ERROR', - format='%(asctime)s %(name)s %(levelname)s: %(message)s') + level="ERROR", format="%(asctime)s %(name)s %(levelname)s: %(message)s" +) class TestLDQueryLanguage(unittest.TestCase): """ Test class for ContextBrokerClient """ + def setUp(self) -> None: """ Setup test data Returns: None """ - #Extra size parameters for modular testing + # Extra size parameters for modular testing self.cars_nb = 500 self.span = 3 - - #client parameters + + # client parameters self.fiware_header = FiwareLDHeader(ngsild_tenant=settings.FIWARE_SERVICE) - self.cb = ContextBrokerLDClient(fiware_header=self.fiware_header, - url=settings.LD_CB_URL) + self.cb = ContextBrokerLDClient( + fiware_header=self.fiware_header, url=settings.LD_CB_URL + ) - #Prep db + # Prep db clear_context_broker_ld(cb_ld_client=self.cb) - - #base id - self.base='urn:ngsi-ld:' - - #Some entities for relationships - self.garage = ContextLDEntity(id=f"{self.base}garage0",type=f"garage") - self.cam = ContextLDEntity(id=f"{self.base}cam0",type=f"camera") + + # base id + self.base = "urn:ngsi-ld:" + + # Some entities for relationships + self.garage = ContextLDEntity(id=f"{self.base}garage0", type=f"garage") + self.cam = ContextLDEntity(id=f"{self.base}cam0", type=f"camera") self.cb.post_entity(entity=self.garage) self.cb.post_entity(entity=self.cam) - - #Entities to post/test on - self.cars = [ContextLDEntity(id=f"{self.base}car0{i}",type=f"{self.base}car") for i in range(0,self.cars_nb-1)] - - #Some dictionaries for randomizing properties - self.brands = ["Batmobile","DeLorean","Knight 2000"] - self.timestamps = ["2020-12-24T11:00:00Z","2020-12-24T12:00:00Z","2020-12-24T13:00:00Z"] + + # Entities to post/test on + self.cars = [ + ContextLDEntity(id=f"{self.base}car0{i}", type=f"{self.base}car") + for i in range(0, self.cars_nb - 1) + ] + + # Some dictionaries for randomizing properties + self.brands = ["Batmobile", "DeLorean", "Knight 2000"] + self.timestamps = [ + "2020-12-24T11:00:00Z", + "2020-12-24T12:00:00Z", + "2020-12-24T13:00:00Z", + ] self.addresses = [ { "country": "Germany", - "street-address": { - "street":"Mathieustr.", - "number":10}, - "postal-code": 52072 + "street-address": {"street": "Mathieustr.", "number": 10}, + "postal-code": 52072, }, { "country": "USA", - "street-address": { - "street":"Goosetown Drive", - "number":810}, - "postal-code": 27320 + "street-address": {"street": "Goosetown Drive", "number": 810}, + "postal-code": 27320, }, { "country": "Nigeria", - "street-address": { - "street":"Mustapha Street", - "number":46}, - "postal-code": 65931 + "street-address": {"street": "Mustapha Street", "number": 46}, + "postal-code": 65931, }, ] - #base properties/relationships - self.humidity = NamedContextProperty(name="humidity",value=1) - self.temperature = NamedContextProperty(name="temperature",value=0) - self.isParked = NamedContextRelationship(name="isParked",object="placeholder") - self.isMonitoredBy = NamedContextRelationship(name="isMonitoredBy",object="placeholder") - - #q Expressions to test + # base properties/relationships + self.humidity = NamedContextProperty(name="humidity", value=1) + self.temperature = NamedContextProperty(name="temperature", value=0) + self.isParked = NamedContextRelationship(name="isParked", object="placeholder") + self.isMonitoredBy = NamedContextRelationship( + name="isMonitoredBy", object="placeholder" + ) + + # q Expressions to test self.qs = [ - 'temperature > 0', + "temperature > 0", 'brand != "Batmobile"', - 'isParked | isMonitoredBy', + "isParked | isMonitoredBy", 'isParked == "urn:ngsi-ld:garage0"', 'temperature < 60; isParked == "urn:ngsi-ld:garage0"', '(temperature >= 59 | humidity < 3); brand == "DeLorean"', - '(isMonitoredBy; temperature<30) | isParked', - '(temperature > 30; temperature < 90)| humidity <= 5', + "(isMonitoredBy; temperature<30) | isParked", + "(temperature > 30; temperature < 90)| humidity <= 5", 'temperature.observedAt >= "2020-12-24T12:00:00Z"', 'address[country] == "Germany"', - 'address[street-address.number] == 810', - 'address[street-address.number]', - 'address[street-address.extra]', + "address[street-address.number] == 810", + "address[street-address.number]", + "address[street-address.extra]", ] - + self.post() - def tearDown(self) -> None: """ @@ -117,182 +127,189 @@ def tearDown(self) -> None: """ clear_context_broker_ld(cb_ld_client=self.cb) self.cb.close() - + def test_ld_query_language(self): - #Itertools product actually interferes with test results here + # Itertools product actually interferes with test results here for q in self.qs: - entities = self.cb.get_entity_list(q=q,limit=1000) - tokenized,keys_dict = self.extract_keys(q) - - #Replace logical ops with python ones - tokenized = tokenized.replace("|"," or ") - tokenized = tokenized.replace(";"," and ") - size = len([x for x in self.cars if self.search_predicate(x,tokenized,keys_dict)]) - #Check we get the same number of entities - self.assertEqual(size,len(entities),msg=q) + entities = self.cb.get_entity_list(q=q, limit=1000) + tokenized, keys_dict = self.extract_keys(q) + + # Replace logical ops with python ones + tokenized = tokenized.replace("|", " or ") + tokenized = tokenized.replace(";", " and ") + size = len( + [x for x in self.cars if self.search_predicate(x, tokenized, keys_dict)] + ) + # Check we get the same number of entities + self.assertEqual(size, len(entities), msg=q) for e in entities: copy = tokenized - for token,keylist in keys_dict.items(): - copy = self.sub_key_with_val(copy,e,keylist,token) - - #Check each obtained entity obeys the q expression - self.assertTrue(eval(copy),msg=q) - - def extract_keys(self,q:str): - ''' - Extract substring from string expression that is likely to be the name of a + for token, keylist in keys_dict.items(): + copy = self.sub_key_with_val(copy, e, keylist, token) + + # Check each obtained entity obeys the q expression + self.assertTrue(eval(copy), msg=q) + + def extract_keys(self, q: str): + """ + Extract substring from string expression that is likely to be the name of a property/relationship of a given entity Returns: str,dict - ''' - #Trim empty spaces - n=q.replace(" ","") - - #Find all literals that are not logical operators or parentheses -> keys/values - res = re.findall('[^<>=)()|;!]*', n) + """ + # Trim empty spaces + n = q.replace(" ", "") + + # Find all literals that are not logical operators or parentheses -> keys/values + res = re.findall("[^<>=)()|;!]*", n) keys = {} - i=0 + i = 0 for r in res: - #Skip empty string from the regex search result + # Skip empty string from the regex search result if len(r) == 0: continue - - #Skip anything purely numeric -> Definitely a value + + # Skip anything purely numeric -> Definitely a value if r.isnumeric(): continue - #Skip anything with a double quote -> string or date + # Skip anything with a double quote -> string or date if '"' in r: try: - #replace date with unix ts - timestamp = r.replace("\"","") + # replace date with unix ts + timestamp = r.replace('"', "") date = parse(timestamp) timestamp = str(time.mktime(date.timetuple())) - n = n.replace(r,timestamp) + n = n.replace(r, timestamp) except Exception as e: - r=f'\"{r}\"' + r = f'"{r}"' continue - - #Skip keys we already encountered + + # Skip keys we already encountered if [r] in keys.values(): continue - - #Replace the key name with a custom token in the string - token=f'${i}' - n= n.replace(r,token) - i+=1 - - #Flatten composite keys by chaining them together + + # Replace the key name with a custom token in the string + token = f"${i}" + n = n.replace(r, token) + i += 1 + + # Flatten composite keys by chaining them together l = [] - #Composite of the form x[...] - if '[' in r: - idx_st = r.index('[') - idx_e = r.index(']') + # Composite of the form x[...] + if "[" in r: + idx_st = r.index("[") + idx_e = r.index("]") outer_key = r[:idx_st] l.append(outer_key) - inner_key = r[idx_st+1:idx_e] - - #Composite of the form x[y.z...] - if '.' in inner_key: - rest = inner_key.split('.') - #Composite of the form x[y] - else : + inner_key = r[idx_st + 1 : idx_e] + + # Composite of the form x[y.z...] + if "." in inner_key: + rest = inner_key.split(".") + # Composite of the form x[y] + else: rest = [inner_key] - l+=rest - #Composite of the form x.y... - elif '.' in r: - l+=r.split('.') - #Simple key + l += rest + # Composite of the form x.y... + elif "." in r: + l += r.split(".") + # Simple key else: - l=[r] - - #Finalize incomplete key presence check - idx_next = n.index(token)+len(token) - if idx_next>=len(n) or n[idx_next] not in ['>','<','=','!']: - n = n.replace(token,f'{token} != None') - - #Associate each chain of nested keys with the token it was replaced with + l = [r] + + # Finalize incomplete key presence check + idx_next = n.index(token) + len(token) + if idx_next >= len(n) or n[idx_next] not in [">", "<", "=", "!"]: + n = n.replace(token, f"{token} != None") + + # Associate each chain of nested keys with the token it was replaced with keys[token] = l - return n,keys - - def sub_key_with_val(self,q:str,entity:ContextLDEntity,keylist,token:str): - ''' + return n, keys + + def sub_key_with_val(self, q: str, entity: ContextLDEntity, keylist, token: str): + """ Substitute key names in q expression with corresponding entity property/ relationship values. All while accounting for access of nested properties Returns: str - ''' + """ obj = entity.model_dump() for key in keylist: if key in obj: obj = obj[key] - elif 'value' in obj and key in obj['value']: - obj = obj['value'][key] + elif "value" in obj and key in obj["value"]: + obj = obj["value"][key] else: obj = None break - - if isinstance(obj,Iterable): - if 'value' in obj: - obj=obj['value'] - elif 'object' in obj: - obj=obj['object'] - - if obj is not None and re.compile('[a-zA-Z]+').search(str(obj)) is not None: + + if isinstance(obj, Iterable): + if "value" in obj: + obj = obj["value"] + elif "object" in obj: + obj = obj["object"] + + if obj is not None and re.compile("[a-zA-Z]+").search(str(obj)) is not None: try: date = parse(obj) - obj = str(time.mktime(date.timetuple())) #convert to unix ts + obj = str(time.mktime(date.timetuple())) # convert to unix ts except Exception as e: obj = f'"{str(obj)}"' - - - #replace key names with entity values - n = q.replace(token,str(obj)) + + # replace key names with entity values + n = q.replace(token, str(obj)) return n - - def search_predicate(self,e,tokenized,keys_dict): - ''' + + def search_predicate(self, e, tokenized, keys_dict): + """ Search function to search our posted data for checks This function is needed because , whereas the context broker will not return an entity with no nested key if that key is given as a filter, our eval attempts to compare None values using logical operators - ''' + """ copy = tokenized - for token,keylist in keys_dict.items(): - copy = self.sub_key_with_val(copy,e,keylist,token) - + for token, keylist in keys_dict.items(): + copy = self.sub_key_with_val(copy, e, keylist, token) + try: return eval(copy) except: return False def post(self): - ''' - Somewhat randomized generation of data. Can be made further random by + """ + Somewhat randomized generation of data. Can be made further random by Choosing a bigger number of cars, and a more irregular number for remainder Calculations (self.cars_nb & self.span) Returns: None - ''' + """ for i in range(len(self.cars)): - #Big number rnd generator - r = Random().randint(1,self.span) - tri_rnd = Random().randint(0,(10*self.span)**2) - r = math.trunc(tri_rnd/r) % self.span - r_2 = Random().randint(0,r) - - a=r_2*30 - b=a+30 - - #Every car will have temperature, humidity, brand and address + # Big number rnd generator + r = Random().randint(1, self.span) + tri_rnd = Random().randint(0, (10 * self.span) ** 2) + r = math.trunc(tri_rnd / r) % self.span + r_2 = Random().randint(0, r) + + a = r_2 * 30 + b = a + 30 + + # Every car will have temperature, humidity, brand and address t = self.temperature.model_copy() - t.value = Random().randint(a,b) + t.value = Random().randint(a, b) t.observedAt = self.timestamps[r] - + h = self.humidity.model_copy() - h.value = Random().randint(math.trunc(a/10),math.trunc(b/10)) - - self.cars[i].add_properties([t,h,NamedContextProperty(name="brand",value=self.brands[r]), - NamedContextProperty(name="address",value=self.addresses[r])]) + h.value = Random().randint(math.trunc(a / 10), math.trunc(b / 10)) + + self.cars[i].add_properties( + [ + t, + h, + NamedContextProperty(name="brand", value=self.brands[r]), + NamedContextProperty(name="address", value=self.addresses[r]), + ] + ) p = self.isParked.model_copy() p.object = self.garage.id @@ -300,14 +317,15 @@ def post(self): m = self.isMonitoredBy.model_copy() m.object = self.cam.id - #Every car is endowed with a set of relationships/nested key - if r==0: + # Every car is endowed with a set of relationships/nested key + if r == 0: self.cars[i].add_relationships([p]) - elif r==1: + elif r == 1: self.cars[i].add_relationships([m]) - elif r==2: - self.cars[i].add_relationships([p,m]) + elif r == 2: + self.cars[i].add_relationships([p, m]) - #Post everything - self.cb.entity_batch_operation(action_type=ActionTypeLD.CREATE, - entities=self.cars) + # Post everything + self.cb.entity_batch_operation( + action_type=ActionTypeLD.CREATE, entities=self.cars + ) diff --git a/tests/clients/test_ngsi_ld_subscription.py b/tests/clients/test_ngsi_ld_subscription.py index c097d1cb..44e54e4e 100644 --- a/tests/clients/test_ngsi_ld_subscription.py +++ b/tests/clients/test_ngsi_ld_subscription.py @@ -1,6 +1,7 @@ """ Test the endpoint for subscription related task of NGSI-LD for ContextBrokerClient """ + import json import time import urllib.parse @@ -11,13 +12,16 @@ from requests import RequestException from filip.clients.ngsi_ld.cb import ContextBrokerLDClient from filip.models.base import FiwareLDHeader -from filip.models.ngsi_ld.context import \ - NamedContextProperty, \ - ContextLDEntity, ActionTypeLD -from filip.models.ngsi_ld.subscriptions import \ - Endpoint, \ - NotificationParams, \ - SubscriptionLD +from filip.models.ngsi_ld.context import ( + NamedContextProperty, + ContextLDEntity, + ActionTypeLD, +) +from filip.models.ngsi_ld.subscriptions import ( + Endpoint, + NotificationParams, + SubscriptionLD, +) from tests.config import settings from filip.utils.cleanup import clear_context_broker_ld @@ -34,21 +38,28 @@ def setUp(self) -> None: None """ self.fiware_header = FiwareLDHeader(ngsild_tenant=settings.FIWARE_SERVICE) - self.cb_client = ContextBrokerLDClient(fiware_header=self.fiware_header, - url=settings.LD_CB_URL) + self.cb_client = ContextBrokerLDClient( + fiware_header=self.fiware_header, url=settings.LD_CB_URL + ) clear_context_broker_ld(cb_ld_client=self.cb_client) - self.mqtt_topic = ''.join([settings.FIWARE_SERVICE, - settings.FIWARE_SERVICEPATH]) - self.endpoint_mqtt = Endpoint(**{ - "uri": str(settings.LD_MQTT_BROKER_URL) + "/my/test/topic", - "accept": "application/json", - }) - self.endpoint_http = Endpoint(**{ - "uri": urllib.parse.urljoin(str(settings.LD_CB_URL), - "/ngsi-ld/v1/subscriptions"), - "accept": "application/json" - }) + self.mqtt_topic = "".join( + [settings.FIWARE_SERVICE, settings.FIWARE_SERVICEPATH] + ) + self.endpoint_mqtt = Endpoint( + **{ + "uri": str(settings.LD_MQTT_BROKER_URL) + "/my/test/topic", + "accept": "application/json", + } + ) + self.endpoint_http = Endpoint( + **{ + "uri": urllib.parse.urljoin( + str(settings.LD_CB_URL), "/ngsi-ld/v1/subscriptions" + ), + "accept": "application/json", + } + ) def tearDown(self) -> None: clear_context_broker_ld(cb_ld_client=self.cb_client) @@ -66,12 +77,19 @@ def test_post_subscription_http(self): """ attr_id = "attr" id = "urn:ngsi-ld:Subscription:" + "test_sub0" - notification_param = NotificationParams(attributes=[attr_id], endpoint=self.endpoint_http) - sub = SubscriptionLD(id=id, notification=notification_param, entities=[{"type": "Room"}]) + notification_param = NotificationParams( + attributes=[attr_id], endpoint=self.endpoint_http + ) + sub = SubscriptionLD( + id=id, notification=notification_param, entities=[{"type": "Room"}] + ) self.cb_client.post_subscription(sub) - sub_list = [x for x in self.cb_client.get_subscription_list() - if x.id == 'urn:ngsi-ld:Subscription:test_sub0'] - self.assertEqual(len(sub_list),1) + sub_list = [ + x + for x in self.cb_client.get_subscription_list() + if x.id == "urn:ngsi-ld:Subscription:test_sub0" + ] + self.assertEqual(len(sub_list), 1) def test_post_subscription_http_check_broker(self): """ @@ -93,21 +111,27 @@ def test_get_subscription(self): Returns the subscription if it exists. Args: - subscriptionId(string): required - Returns: - - (200) subscription or empty list if successful + Returns: + - (200) subscription or empty list if successful - Error Code Tests: - Get Subscription and check if the subscription is the same as the one posted """ attr_id = "attr" id = "urn:ngsi-ld:Subscription:" + "test_sub0" - notification_param = NotificationParams(attributes=[attr_id], endpoint=self.endpoint_http) - sub = SubscriptionLD(id=id, notification=notification_param, entities=[{"type": "Room"}]) + notification_param = NotificationParams( + attributes=[attr_id], endpoint=self.endpoint_http + ) + sub = SubscriptionLD( + id=id, notification=notification_param, entities=[{"type": "Room"}] + ) self.cb_client.post_subscription(sub) sub_get = self.cb_client.get_subscription(subscription_id=id) self.assertEqual(sub.entities, sub_get.entities) self.assertEqual(sub.notification.attributes, sub_get.notification.attributes) - self.assertEqual(sub.notification.endpoint.uri, sub_get.notification.endpoint.uri) + self.assertEqual( + sub.notification.endpoint.uri, sub_get.notification.endpoint.uri + ) def test_get_subscription_list(self): """ @@ -124,8 +148,12 @@ def test_get_subscription_list(self): for i in range(10): attr_id = "attr" + str(i) id = "urn:ngsi-ld:Subscription:" + "test_sub" + str(i) - notification_param = NotificationParams(attributes=[attr_id], endpoint=self.endpoint_http) - sub = SubscriptionLD(id=id, notification=notification_param, entities=[{"type": "Room"}]) + notification_param = NotificationParams( + attributes=[attr_id], endpoint=self.endpoint_http + ) + sub = SubscriptionLD( + id=id, notification=notification_param, entities=[{"type": "Room"}] + ) sub_post_list.append(sub) self.cb_client.post_subscription(sub) @@ -141,29 +169,29 @@ def test_get_subscription_list(self): def test_delete_subscription(self): """ - Cancels subscription. - Args: + Cancels subscription. + Args: - subscriptionID(string): required Returns: - - Successful: 204, no content + - Successful: 204, no content Tests: - Post and delete subscription then get all subscriptions and check whether deleted subscription is still there. """ - for i in range(10): + for i in range(10): attr_id = "attr" + str(i) notification_param = NotificationParams( - attributes=[attr_id], endpoint=self.endpoint_http) + attributes=[attr_id], endpoint=self.endpoint_http + ) id = "urn:ngsi-ld:Subscription:" + "test_sub" + str(i) - sub = SubscriptionLD(id=id, - notification=notification_param, - entities=[{"type": "Room"}] - ) + sub = SubscriptionLD( + id=id, notification=notification_param, entities=[{"type": "Room"}] + ) - if i == 0: + if i == 0: del_sub = sub del_id = id self.cb_client.post_subscription(sub) - + sub_list = self.cb_client.get_subscription_list(limit=10) sub_id_list = [sub.id for sub in sub_list] self.assertIn(del_sub.id, sub_id_list) @@ -175,7 +203,7 @@ def test_delete_subscription(self): for sub in sub_list: self.cb_client.delete_subscription(subscription_id=sub.id) - + def test_update_subscription(self): """ Update a subscription. @@ -185,29 +213,36 @@ def test_update_subscription(self): - body(body): required Returns: - Successful: 204, no content - Tests: + Tests: - Patch existing subscription and read out if the subscription got patched. - Try to patch non-existent subscriptions. - Try to patch more than one subscription at once. """ attr_id = "attr" id = "urn:ngsi-ld:Subscription:" + "test_sub77" - notification_param = NotificationParams(attributes=[attr_id], endpoint=self.endpoint_http) - sub = SubscriptionLD(id=id, notification=notification_param, entities=[{"type": "Room"}]) + notification_param = NotificationParams( + attributes=[attr_id], endpoint=self.endpoint_http + ) + sub = SubscriptionLD( + id=id, notification=notification_param, entities=[{"type": "Room"}] + ) self.cb_client.post_subscription(sub) sub_list = self.cb_client.get_subscription_list() self.assertEqual(len(sub_list), 1) print(self.endpoint_http.model_dump()) - sub_changed = SubscriptionLD(id=id, notification=notification_param, entities=[{"type": "House"}]) + sub_changed = SubscriptionLD( + id=id, notification=notification_param, entities=[{"type": "House"}] + ) self.cb_client.update_subscription(sub_changed) - u_sub= self.cb_client.get_subscription(subscription_id=id) - self.assertNotEqual(u_sub,sub_list[0]) + u_sub = self.cb_client.get_subscription(subscription_id=id) + self.assertNotEqual(u_sub, sub_list[0]) self.maxDiff = None - self.assertDictEqual(sub_changed.model_dump(), - u_sub.model_dump()) - non_sub = SubscriptionLD(id="urn:ngsi-ld:Subscription:nonexist", - notification=notification_param, - entities=[{"type":"house"}]) + self.assertDictEqual(sub_changed.model_dump(), u_sub.model_dump()) + non_sub = SubscriptionLD( + id="urn:ngsi-ld:Subscription:nonexist", + notification=notification_param, + entities=[{"type": "house"}], + ) with self.assertRaises(Exception): self.cb_client.update_subscription(non_sub) @@ -217,8 +252,9 @@ class TestSubsCheckBroker(TestCase): These tests are more oriented towards testing the actual broker. Some functionality in Orion LD may not be consistent at times. """ + def timeout_func(self): - self.last_test_timeout =[False] + self.last_test_timeout = [False] def cleanup(self): """ @@ -226,14 +262,15 @@ def cleanup(self): """ sub_list = self.cb_client.get_subscription_list() for sub in sub_list: - if sub.id.startswith('urn:ngsi-ld:Subscription:test_sub'): + if sub.id.startswith("urn:ngsi-ld:Subscription:test_sub"): self.cb_client.delete_subscription(sub.id) try: entity_list = True while entity_list: entity_list = self.cb_client.get_entity_list(limit=100) - self.cb_client.entity_batch_operation(action_type=ActionTypeLD.DELETE, - entities=entity_list) + self.cb_client.entity_batch_operation( + action_type=ActionTypeLD.DELETE, entities=entity_list + ) except RequestException: pass @@ -244,76 +281,61 @@ def setUp(self) -> None: None """ self.entity_dict = { - 'id': 'urn:ngsi-ld:Entity:test_entity03', - 'type': 'Room', - 'temperature': { - 'type': 'Property', - 'value': 30 - } + "id": "urn:ngsi-ld:Entity:test_entity03", + "type": "Room", + "temperature": {"type": "Property", "value": 30}, } - + self.sub_dict = { - 'description': 'Test Subscription', - 'id': 'urn:ngsi-ld:Subscription:test_sub25', - 'type': 'Subscription', - 'entities': [ - { - 'type': 'Room' - } - ], - 'watchedAttributes': [ - 'temperature' - ], - 'q': 'temperature<30', - 'notification': { - 'attributes': [ - 'temperature' - ], - 'format': 'normalized', - 'endpoint': { - 'uri': f'mqtt://' # change uri - f'{settings.LD_MQTT_BROKER_URL_INTERNAL.host}:' - f'{settings.LD_MQTT_BROKER_URL_INTERNAL.port}/my/test/topic', - 'Accept': 'application/json' + "description": "Test Subscription", + "id": "urn:ngsi-ld:Subscription:test_sub25", + "type": "Subscription", + "entities": [{"type": "Room"}], + "watchedAttributes": ["temperature"], + "q": "temperature<30", + "notification": { + "attributes": ["temperature"], + "format": "normalized", + "endpoint": { + "uri": f"mqtt://" # change uri + f"{settings.LD_MQTT_BROKER_URL_INTERNAL.host}:" + f"{settings.LD_MQTT_BROKER_URL_INTERNAL.port}/my/test/topic", + "Accept": "application/json", }, - 'notifierInfo': [ - { - "key": "MQTT-Version", - "value": "mqtt5.0" - } - ] - } + "notifierInfo": [{"key": "MQTT-Version", "value": "mqtt5.0"}], + }, } - self.fiware_header = FiwareLDHeader( - ngsild_tenant=settings.FIWARE_SERVICE) - self.cb_client = ContextBrokerLDClient(url=settings.LD_CB_URL, - fiware_header=self.fiware_header) + self.fiware_header = FiwareLDHeader(ngsild_tenant=settings.FIWARE_SERVICE) + self.cb_client = ContextBrokerLDClient( + url=settings.LD_CB_URL, fiware_header=self.fiware_header + ) # initial tenant - self.cb_client.post_entity(ContextLDEntity(id="Dummy:1", type="Dummy"), - update=True) + self.cb_client.post_entity( + ContextLDEntity(id="Dummy:1", type="Dummy"), update=True + ) self.cb_client.delete_entity_by_id("Dummy:1") self.mqtt_client = mqtt.Client(callback_api_version=CallbackAPIVersion.VERSION2) - #on_message callbacks differ from test to test, but on connect callbacks dont - def on_connect_fail(client,userdata): + + # on_message callbacks differ from test to test, but on connect callbacks dont + def on_connect_fail(client, userdata): self.fail("Test failed due to broker being down") - def on_connect(client,userdata,flags,reason_code,properties): + def on_connect(client, userdata, flags, reason_code, properties): self.mqtt_client.subscribe("my/test/topic") self.mqtt_client.on_connect_fail = on_connect_fail self.mqtt_client.on_connect = on_connect self.cleanup() - #posting one single entity to check subscription existence/triggers + # posting one single entity to check subscription existence/triggers self.cb_client.post_entity(entity=ContextLDEntity(**self.entity_dict)) - #All broker tests rely on awaiting a message. This timer helps with: + # All broker tests rely on awaiting a message. This timer helps with: # -Avoiding hang ups in the case of a lost connection # -Avoid ending the tests early, in the case a notification takes longer - self.timeout = 5 # in seconds + self.timeout = 5 # in seconds self.last_test_timeout = [True] - self.timeout_proc = threading.Timer(self.timeout, - self.timeout_func) + self.timeout_proc = threading.Timer(self.timeout, self.timeout_func) def tearDown(self) -> None: self.cleanup() @@ -324,39 +346,41 @@ def test_post_subscription_mqtt(self): Tests: - Subscribe using an mqtt topic as endpoint and see if notification is received """ - def on_message(client,userdata,msg): - #the callback cancels the timer if a message comes through + + def on_message(client, userdata, msg): + # the callback cancels the timer if a message comes through self.timeout_proc.cancel() updated_entity = self.entity_dict.copy() - updated_entity.update({'temperature':{'type':'Property','value':25}}) + updated_entity.update({"temperature": {"type": "Property", "value": 25}}) self.mqtt_client.loop_stop() self.mqtt_client.disconnect() - #extra sanity check on the contents of the notification(in case we are - #catching a rogue one) - self.assertEqual(updated_entity, - json.loads(msg.payload.decode())['body']['data'][0]) + # extra sanity check on the contents of the notification(in case we are + # catching a rogue one) + self.assertEqual( + updated_entity, json.loads(msg.payload.decode())["body"]["data"][0] + ) self.mqtt_client.on_message = on_message - self.mqtt_client.connect(settings.LD_MQTT_BROKER_URL.host, - settings.LD_MQTT_BROKER_URL.port, - 60) + self.mqtt_client.connect( + settings.LD_MQTT_BROKER_URL.host, settings.LD_MQTT_BROKER_URL.port, 60 + ) self.mqtt_client.loop_start() - #post subscription then start timer + # post subscription then start timer self.cb_client.post_subscription(subscription=SubscriptionLD(**self.sub_dict)) self.timeout_proc.start() - #update entity to (ideally) get a notification - self.cb_client.update_entity_attribute(entity_id='urn:ngsi-ld:Entity:test_entity03', - attr=NamedContextProperty(type="Property", - value=25, - name='temperature'), - attr_name='temperature') - #this loop is necessary otherwise the test does not fail when the time runs out - while(self.timeout_proc.is_alive()): + # update entity to (ideally) get a notification + self.cb_client.update_entity_attribute( + entity_id="urn:ngsi-ld:Entity:test_entity03", + attr=NamedContextProperty(type="Property", value=25, name="temperature"), + attr_name="temperature", + ) + # this loop is necessary otherwise the test does not fail when the time runs out + while self.timeout_proc.is_alive(): continue - #if all goes well, the callback is triggered, and cancels the timer before - #it gets to change the timeout variable to False, making the following assertion True - self.assertTrue(self.last_test_timeout[0],"Operation timed out") - + # if all goes well, the callback is triggered, and cancels the timer before + # it gets to change the timeout variable to False, making the following assertion True + self.assertTrue(self.last_test_timeout[0], "Operation timed out") + def test_update_subscription_check_broker(self): """ Update a subscription and check changes in received messages. @@ -370,65 +394,73 @@ def test_update_subscription_check_broker(self): Steps: - Create Subscription with q = x - Update entity to trigger sub with valid condition x - - Update subscription to q = x̄ + - Update subscription to q = x̄ - Update entity to trigger sub with opposite condition x̄ """ - current_vals = [25,33] + current_vals = [25, 33] - #re-assigning a variable inside an inline function does not work => hence generator + # re-assigning a variable inside an inline function does not work => hence generator def idx_generator(n): - while(n<2): + while n < 2: yield n - n+=1 - + n += 1 + gen = idx_generator(0) - def on_message(client,userdata,msg): + def on_message(client, userdata, msg): idx = next(gen) self.timeout_proc.cancel() - print(json.loads(msg.payload.decode()) - ['body']['data'][0]['temperature']['value']) - self.assertEqual(current_vals[idx], - json.loads(msg.payload.decode()) - ['body']['data'][0]['temperature']['value']) + print( + json.loads(msg.payload.decode())["body"]["data"][0]["temperature"][ + "value" + ] + ) + self.assertEqual( + current_vals[idx], + json.loads(msg.payload.decode())["body"]["data"][0]["temperature"][ + "value" + ], + ) self.mqtt_client.on_message = on_message - - self.mqtt_client.connect(settings.LD_MQTT_BROKER_URL.host, - settings.LD_MQTT_BROKER_URL.port, - 60) + + self.mqtt_client.connect( + settings.LD_MQTT_BROKER_URL.host, settings.LD_MQTT_BROKER_URL.port, 60 + ) self.mqtt_client.loop_start() self.cb_client.post_subscription(subscription=SubscriptionLD(**self.sub_dict)) self.timeout_proc.start() - self.cb_client.update_entity_attribute(entity_id='urn:ngsi-ld:Entity:test_entity03', - attr=NamedContextProperty(type="Property", - value=current_vals[0], - name='temperature'), - attr_name='temperature') - while(self.timeout_proc.is_alive()): + self.cb_client.update_entity_attribute( + entity_id="urn:ngsi-ld:Entity:test_entity03", + attr=NamedContextProperty( + type="Property", value=current_vals[0], name="temperature" + ), + attr_name="temperature", + ) + while self.timeout_proc.is_alive(): continue - self.assertTrue(self.last_test_timeout[0], - "Operation timed out") + self.assertTrue(self.last_test_timeout[0], "Operation timed out") self.last_test_timeout = [True] - self.timeout_proc = threading.Timer(self.timeout,self.timeout_func) - - self.sub_dict.update({'q':'temperature>30'}) + self.timeout_proc = threading.Timer(self.timeout, self.timeout_func) + + self.sub_dict.update({"q": "temperature>30"}) self.cb_client.update_subscription(subscription=SubscriptionLD(**self.sub_dict)) time.sleep(5) - updated = self.cb_client.get_subscription(self.sub_dict['id']) - self.assertEqual(updated.q,'temperature>30') + updated = self.cb_client.get_subscription(self.sub_dict["id"]) + self.assertEqual(updated.q, "temperature>30") self.timeout_proc.start() - self.cb_client.update_entity_attribute(entity_id='urn:ngsi-ld:Entity:test_entity03', - attr=NamedContextProperty(type="Property", - value=current_vals[1], - name='temperature'), - attr_name='temperature') - while(self.timeout_proc.is_alive()): + self.cb_client.update_entity_attribute( + entity_id="urn:ngsi-ld:Entity:test_entity03", + attr=NamedContextProperty( + type="Property", value=current_vals[1], name="temperature" + ), + attr_name="temperature", + ) + while self.timeout_proc.is_alive(): continue - self.assertTrue(self.last_test_timeout[0], - "Operation timed out") + self.assertTrue(self.last_test_timeout[0], "Operation timed out") self.mqtt_client.loop_stop() self.mqtt_client.disconnect() @@ -441,7 +473,7 @@ def test_delete_subscription_check_broker(self): - Successful: 204, no content Tests: - Post and delete subscription then see if the broker still gets subscribed values. - + """ pass diff --git a/tests/clients/test_ngsi_v2_cb.py b/tests/clients/test_ngsi_v2_cb.py index c38e8dec..d967ae85 100644 --- a/tests/clients/test_ngsi_v2_cb.py +++ b/tests/clients/test_ngsi_v2_cb.py @@ -1,6 +1,7 @@ """ Tests for filip.cb.client """ + import copy import unittest import logging @@ -18,25 +19,31 @@ from filip.utils.simple_ql import QueryString from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient from filip.clients.ngsi_v2 import HttpClient, HttpClientConfig -from filip.models.ngsi_v2.context import \ - ContextEntity, \ - ContextAttribute, \ - NamedContextAttribute, \ - NamedCommand, \ - Query, \ - ActionType, \ - ContextEntityKeyValues - -from filip.models.ngsi_v2.base import AttrsFormat, EntityPattern, Status, \ - NamedMetadata -from filip.models.ngsi_v2.subscriptions import Mqtt, Message, Subscription, Condition, \ - Notification -from filip.models.ngsi_v2.iot import \ - Device, \ - DeviceCommand, \ - DeviceAttribute, \ - ServiceGroup, \ - StaticDeviceAttribute +from filip.models.ngsi_v2.context import ( + ContextEntity, + ContextAttribute, + NamedContextAttribute, + NamedCommand, + Query, + ActionType, + ContextEntityKeyValues, +) + +from filip.models.ngsi_v2.base import AttrsFormat, EntityPattern, Status, NamedMetadata +from filip.models.ngsi_v2.subscriptions import ( + Mqtt, + Message, + Subscription, + Condition, + Notification, +) +from filip.models.ngsi_v2.iot import ( + Device, + DeviceCommand, + DeviceAttribute, + ServiceGroup, + StaticDeviceAttribute, +) from filip.utils.cleanup import clear_all, clean_test from tests.config import settings @@ -55,66 +62,55 @@ def setUp(self) -> None: None """ self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) self.resources = { "entities_url": "/v2/entities", "types_url": "/v2/types", "subscriptions_url": "/v2/subscriptions", - "registrations_url": "/v2/registrations" + "registrations_url": "/v2/registrations", } - self.attr = {'temperature': {'value': 20.0, - 'type': 'Number'}} - self.entity = ContextEntity(id='MyId', type='MyType', **self.attr) + self.attr = {"temperature": {"value": 20.0, "type": "Number"}} + self.entity = ContextEntity(id="MyId", type="MyType", **self.attr) self.iotac = IoTAClient( - url=settings.IOTA_JSON_URL, - fiware_header=self.fiware_header) + url=settings.IOTA_JSON_URL, fiware_header=self.fiware_header + ) self.client = ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) - self.subscription = Subscription.model_validate({ - "description": "One subscription to rule them all", - "subject": { - "entities": [ - { - "idPattern": ".*", - "type": "Room" - } - ], - "condition": { - "attrs": [ - "temperature" - ], - "expression": { - "q": "temperature>40" - } - } - }, - "notification": { - "http": { - "url": "http://localhost:1234" + url=settings.CB_URL, fiware_header=self.fiware_header + ) + self.subscription = Subscription.model_validate( + { + "description": "One subscription to rule them all", + "subject": { + "entities": [{"idPattern": ".*", "type": "Room"}], + "condition": { + "attrs": ["temperature"], + "expression": {"q": "temperature>40"}, + }, }, - "attrs": [ - "temperature", - "humidity" - ] - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) + "notification": { + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], + }, + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) def test_management_endpoints(self): """ Test management functions of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: self.assertIsNotNone(client.get_version()) self.assertEqual(client.get_resources(), self.resources) @@ -123,71 +119,77 @@ def test_statistics(self): Test statistics of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: self.assertIsNotNone(client.get_statistics()) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_pagination(self): """ Test pagination of context broker client Test pagination. only works if enough entities are available """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: - entities_a = [ContextEntity(id=str(i), - type=f'filip:object:TypeA') for i in - range(0, 1000)] + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: + entities_a = [ + ContextEntity(id=str(i), type=f"filip:object:TypeA") + for i in range(0, 1000) + ] client.update(action_type=ActionType.APPEND, entities=entities_a) - entities_b = [ContextEntity(id=str(i), - type=f'filip:object:TypeB') for i in - range(1000, 2001)] + entities_b = [ + ContextEntity(id=str(i), type=f"filip:object:TypeB") + for i in range(1000, 2001) + ] client.update(action_type=ActionType.APPEND, entities=entities_b) self.assertLessEqual(len(client.get_entity_list(limit=1)), 1) self.assertLessEqual(len(client.get_entity_list(limit=999)), 999) self.assertLessEqual(len(client.get_entity_list(limit=1001)), 1001) self.assertLessEqual(len(client.get_entity_list(limit=2001)), 2001) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_entity_filtering(self): """ Test filter operations of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: # test patterns with self.assertRaises(ValueError): - client.get_entity_list(id_pattern='(&()?') + client.get_entity_list(id_pattern="(&()?") with self.assertRaises(ValueError): - client.get_entity_list(type_pattern='(&()?') - entities_a = [ContextEntity(id=str(i), - type=f'filip:object:TypeA') for i in - range(0, 5)] + client.get_entity_list(type_pattern="(&()?") + entities_a = [ + ContextEntity(id=str(i), type=f"filip:object:TypeA") + for i in range(0, 5) + ] client.update(action_type=ActionType.APPEND, entities=entities_a) - entities_b = [ContextEntity(id=str(i), - type=f'filip:object:TypeB') for i in - range(6, 10)] + entities_b = [ + ContextEntity(id=str(i), type=f"filip:object:TypeB") + for i in range(6, 10) + ] client.update(action_type=ActionType.APPEND, entities=entities_b) entities_all = client.get_entity_list() - entities_by_id_pattern = client.get_entity_list( - id_pattern='.*[1-5]') + entities_by_id_pattern = client.get_entity_list(id_pattern=".*[1-5]") self.assertLess(len(entities_by_id_pattern), len(entities_all)) - entities_by_type_pattern = client.get_entity_list( - type_pattern=".*TypeA$") + entities_by_type_pattern = client.get_entity_list(type_pattern=".*TypeA$") self.assertLess(len(entities_by_type_pattern), len(entities_all)) - qs = QueryString(qs=[('presentValue', '>', 0)]) + qs = QueryString(qs=[("presentValue", ">", 0)]) entities_by_query = client.get_entity_list(q=qs) self.assertLess(len(entities_by_query), len(entities_all)) @@ -195,55 +197,62 @@ def test_entity_filtering(self): for opt in list(AttrsFormat): entities_by_option = client.get_entity_list(response_format=opt) self.assertEqual(len(entities_by_option), len(entities_all)) - self.assertEqual(client.get_entity( - entity_id='0', - response_format=opt), - entities_by_option[0]) + self.assertEqual( + client.get_entity(entity_id="0", response_format=opt), + entities_by_option[0], + ) with self.assertRaises(ValueError): - client.get_entity_list(response_format='not in AttrFormat') + client.get_entity_list(response_format="not in AttrFormat") client.update(action_type=ActionType.DELETE, entities=entities_a) client.update(action_type=ActionType.DELETE, entities=entities_b) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_entity_operations(self): """ Test entity operations of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: client.post_entity(entity=self.entity, update=True) res_entity = client.get_entity(entity_id=self.entity.id) - self.assertEqual(res_entity, - client.get_entity( - entity_id=self.entity.id, - attrs=list(res_entity.get_attribute_names()))) - self.assertEqual(client.get_entity_attributes( - entity_id=self.entity.id), res_entity.get_properties( - response_format='dict')) + self.assertEqual( + res_entity, + client.get_entity( + entity_id=self.entity.id, + attrs=list(res_entity.get_attribute_names()), + ), + ) + self.assertEqual( + client.get_entity_attributes(entity_id=self.entity.id), + res_entity.get_properties(response_format="dict"), + ) res_entity.temperature.value = 25 client.update_entity(entity=res_entity) - self.assertEqual(client.get_entity(entity_id=self.entity.id), - res_entity) - res_entity.add_attributes({'pressure': ContextAttribute( - type='Number', value=1050)}) + self.assertEqual(client.get_entity(entity_id=self.entity.id), res_entity) + res_entity.add_attributes( + {"pressure": ContextAttribute(type="Number", value=1050)} + ) client.update_entity(entity=res_entity) - self.assertEqual(client.get_entity(entity_id=self.entity.id), - res_entity) + self.assertEqual(client.get_entity(entity_id=self.entity.id), res_entity) # delete attribute - res_entity.delete_attributes(attrs={'pressure': ContextAttribute( - type='Number', value=1050)}) + res_entity.delete_attributes( + attrs={"pressure": ContextAttribute(type="Number", value=1050)} + ) client.post_entity(entity=res_entity, update=True) - self.assertEqual(client.get_entity(entity_id=self.entity.id), - res_entity) + self.assertEqual(client.get_entity(entity_id=self.entity.id), res_entity) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_entity_update(self): """ Test different ways (post, update, override, patch) to update entity @@ -256,19 +265,16 @@ def test_entity_update(self): """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: entity_init = self.entity.model_copy(deep=True) attr_init = entity_init.get_attribute("temperature") attr_init.metadata = { - "metadata_init": { - "type": "Text", - "value": "something"} + "metadata_init": {"type": "Text", "value": "something"} } - attr_append = NamedContextAttribute(**{ - "name": 'pressure', - "type": 'Number', - "value": 1050}) + attr_append = NamedContextAttribute( + **{"name": "pressure", "type": "Number", "value": 1050} + ) entity_init.update_attribute(attrs=[attr_init]) # Post @@ -278,24 +284,25 @@ def test_entity_update(self): # 1) append attribute entity_post.add_attributes(attrs=[attr_append]) client.post_entity(entity=entity_post, patch=True) - self.assertEqual(client.get_entity(entity_id=entity_post.id), - entity_post) + self.assertEqual( + client.get_entity(entity_id=entity_post.id), entity_post + ) # 2) update existing attribute value - attr_append_update = NamedContextAttribute(**{ - "name": 'pressure', - "type": 'Number', - "value": 2050}) + attr_append_update = NamedContextAttribute( + **{"name": "pressure", "type": "Number", "value": 2050} + ) entity_post.update_attribute(attrs=[attr_append_update]) client.post_entity(entity=entity_post, patch=True) - self.assertEqual(client.get_entity(entity_id=entity_post.id), - entity_post) + self.assertEqual( + client.get_entity(entity_id=entity_post.id), entity_post + ) # 3) delete attribute entity_post.delete_attributes(attrs=[attr_append]) client.post_entity(entity=entity_post, update=True) - self.assertEqual(client.get_entity(entity_id=entity_post.id), - entity_post) - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL) + self.assertEqual( + client.get_entity(entity_id=entity_post.id), entity_post + ) + clear_all(fiware_header=self.fiware_header, cb_url=settings.CB_URL) # update_entity() if "update_entity": @@ -306,40 +313,41 @@ def test_entity_update(self): # change the value of existing attributes entity_update.temperature.value = 30 with self.assertRaises(requests.RequestException): - client.update_entity(entity=entity_update, - append_strict=True) + client.update_entity(entity=entity_update, append_strict=True) entity_updated = client.get_entity(entity_id=entity_update.id) - self.assertEqual(entity_updated.get_attribute_names(), - entity_update.get_attribute_names()) - self.assertNotEqual(entity_updated.temperature.value, - entity_update.temperature.value) + self.assertEqual( + entity_updated.get_attribute_names(), + entity_update.get_attribute_names(), + ) + self.assertNotEqual( + entity_updated.temperature.value, entity_update.temperature.value + ) # change back the value entity_update.temperature.value = 20.0 # 2) update existing attribute value - attr_append_update = NamedContextAttribute(**{ - "name": 'pressure', - "type": 'Number', - "value": 2050}) + attr_append_update = NamedContextAttribute( + **{"name": "pressure", "type": "Number", "value": 2050} + ) entity_update.update_attribute(attrs=[attr_append_update]) - client.update_entity(entity=ContextEntity( - **{ - "id": entity_update.id, - "type": entity_update.type, - "pressure": { - "type": 'Number', - "value": 2050 + client.update_entity( + entity=ContextEntity( + **{ + "id": entity_update.id, + "type": entity_update.type, + "pressure": {"type": "Number", "value": 2050}, } - } - )) - self.assertEqual(client.get_entity(entity_id=entity_update.id), - entity_update) + ) + ) + self.assertEqual( + client.get_entity(entity_id=entity_update.id), entity_update + ) # 3) delete attribute entity_update.delete_attributes(attrs=[attr_append]) client.update_entity(entity=entity_update) - self.assertNotEqual(client.get_entity(entity_id=entity_update.id), - entity_update) - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL) + self.assertNotEqual( + client.get_entity(entity_id=entity_update.id), entity_update + ) + clear_all(fiware_header=self.fiware_header, cb_url=settings.CB_URL) # override_entity() if "override_entity": @@ -348,24 +356,25 @@ def test_entity_update(self): # 1) append attribute entity_override.add_attributes(attrs=[attr_append]) client.override_entity(entity=entity_override) - self.assertEqual(client.get_entity(entity_id=entity_override.id), - entity_override) + self.assertEqual( + client.get_entity(entity_id=entity_override.id), entity_override + ) # 2) update existing attribute value - attr_append_update = NamedContextAttribute(**{ - "name": 'pressure', - "type": 'Number', - "value": 2050}) + attr_append_update = NamedContextAttribute( + **{"name": "pressure", "type": "Number", "value": 2050} + ) entity_override.update_attribute(attrs=[attr_append_update]) client.override_entity(entity=entity_override) - self.assertEqual(client.get_entity(entity_id=entity_override.id), - entity_override) + self.assertEqual( + client.get_entity(entity_id=entity_override.id), entity_override + ) # 3) delete attribute entity_override.delete_attributes(attrs=[attr_append]) client.override_entity(entity=entity_override) - self.assertEqual(client.get_entity(entity_id=entity_override.id), - entity_override) - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL) + self.assertEqual( + client.get_entity(entity_id=entity_override.id), entity_override + ) + clear_all(fiware_header=self.fiware_header, cb_url=settings.CB_URL) # patch_entity if "patch_entity": @@ -374,126 +383,135 @@ def test_entity_update(self): # 1) append attribute entity_patch.add_attributes(attrs=[attr_append]) client.patch_entity(entity=entity_patch) - self.assertEqual(client.get_entity(entity_id=entity_patch.id), - entity_patch) + self.assertEqual( + client.get_entity(entity_id=entity_patch.id), entity_patch + ) # 2) update existing attribute value - attr_append_update = NamedContextAttribute(**{ - "name": 'pressure', - "type": 'Number', - "value": 2050}) + attr_append_update = NamedContextAttribute( + **{"name": "pressure", "type": "Number", "value": 2050} + ) entity_patch.update_attribute(attrs=[attr_append_update]) client.patch_entity(entity=entity_patch) - self.assertEqual(client.get_entity(entity_id=entity_patch.id), - entity_patch) + self.assertEqual( + client.get_entity(entity_id=entity_patch.id), entity_patch + ) # 3) delete attribute entity_patch.delete_attributes(attrs=[attr_append]) client.patch_entity(entity=entity_patch) - self.assertEqual(client.get_entity(entity_id=entity_patch.id), - entity_patch) - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL) + self.assertEqual( + client.get_entity(entity_id=entity_patch.id), entity_patch + ) + clear_all(fiware_header=self.fiware_header, cb_url=settings.CB_URL) # 4) update only property or relationship if "update_entity_properties" or "update_entity_relationship": # post entity with a relationship attribute entity_init = self.entity.model_copy(deep=True) attrs = [ - NamedContextAttribute(name='in', type='Relationship', value='dummy1')] + NamedContextAttribute( + name="in", type="Relationship", value="dummy1" + ) + ] entity_init.add_attributes(attrs=attrs) client.post_entity(entity=entity_init, update=True) # create entity that differs in both attributes entity_update = entity_init.model_copy(deep=True) - attrs = [NamedContextAttribute(name='temperature', - type='Number', - value=21), - NamedContextAttribute(name='in', type='Relationship', - value='dummy2')] + attrs = [ + NamedContextAttribute(name="temperature", type="Number", value=21), + NamedContextAttribute( + name="in", type="Relationship", value="dummy2" + ), + ] entity_update.update_attribute(attrs=attrs) # update only properties and compare client.update_entity_properties(entity_update) entity_db = client.get_entity(entity_update.id) - db_attrs = entity_db.get_attribute(attribute_name='temperature') - update_attrs = entity_update.get_attribute(attribute_name='temperature') + db_attrs = entity_db.get_attribute(attribute_name="temperature") + update_attrs = entity_update.get_attribute(attribute_name="temperature") self.assertEqual(db_attrs, update_attrs) - db_attrs = entity_db.get_attribute(attribute_name='in') - update_attrs = entity_update.get_attribute(attribute_name='in') + db_attrs = entity_db.get_attribute(attribute_name="in") + update_attrs = entity_update.get_attribute(attribute_name="in") self.assertNotEqual(db_attrs, update_attrs) # update only relationship and compare attrs = [ - NamedContextAttribute(name='temperature', type='Number', value=22)] + NamedContextAttribute(name="temperature", type="Number", value=22) + ] entity_update.update_attribute(attrs=attrs) client.update_entity_relationships(entity_update) entity_db = client.get_entity(entity_update.id) - self.assertEqual(entity_db.get_attribute(attribute_name='in'), - entity_update.get_attribute(attribute_name='in')) - self.assertNotEqual(entity_db.get_attribute(attribute_name='temperature'), - entity_update.get_attribute( - attribute_name='temperature')) + self.assertEqual( + entity_db.get_attribute(attribute_name="in"), + entity_update.get_attribute(attribute_name="in"), + ) + self.assertNotEqual( + entity_db.get_attribute(attribute_name="temperature"), + entity_update.get_attribute(attribute_name="temperature"), + ) # change both, update both, compare - attrs = [NamedContextAttribute(name='temperature', - type='Number', - value=23), - NamedContextAttribute(name='in', type='Relationship', - value='dummy3')] + attrs = [ + NamedContextAttribute(name="temperature", type="Number", value=23), + NamedContextAttribute( + name="in", type="Relationship", value="dummy3" + ), + ] entity_update.update_attribute(attrs=attrs) client.update_entity(entity_update) entity_db = client.get_entity(entity_update.id) - db_attrs = entity_db.get_attribute(attribute_name='in') - update_attrs = entity_update.get_attribute(attribute_name='in') + db_attrs = entity_db.get_attribute(attribute_name="in") + update_attrs = entity_update.get_attribute(attribute_name="in") self.assertEqual(db_attrs, update_attrs) - db_attrs = entity_db.get_attribute(attribute_name='temperature') - update_attrs = entity_update.get_attribute(attribute_name='temperature') + db_attrs = entity_db.get_attribute(attribute_name="temperature") + update_attrs = entity_update.get_attribute(attribute_name="temperature") self.assertEqual(db_attrs, update_attrs) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_attribute_operations(self): """ Test attribute operations of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: entity = self.entity - attr_txt = NamedContextAttribute(name='attr_txt', - type='Text', - value="Test") - attr_bool = NamedContextAttribute(name='attr_bool', - type='Boolean', - value=True) - attr_float = NamedContextAttribute(name='attr_float', - type='Number', - value=round(random.random(), 5)) - attr_list = NamedContextAttribute(name='attr_list', - type='StructuredValue', - value=[1, 2, 3]) - attr_dict = NamedContextAttribute(name='attr_dict', - type='StructuredValue', - value={'key': 'value'}) - entity.add_attributes([attr_txt, - attr_bool, - attr_float, - attr_list, - attr_dict]) - - self.assertIsNotNone(client.post_entity(entity=entity, - update=True)) + attr_txt = NamedContextAttribute(name="attr_txt", type="Text", value="Test") + attr_bool = NamedContextAttribute( + name="attr_bool", type="Boolean", value=True + ) + attr_float = NamedContextAttribute( + name="attr_float", type="Number", value=round(random.random(), 5) + ) + attr_list = NamedContextAttribute( + name="attr_list", type="StructuredValue", value=[1, 2, 3] + ) + attr_dict = NamedContextAttribute( + name="attr_dict", type="StructuredValue", value={"key": "value"} + ) + entity.add_attributes( + [attr_txt, attr_bool, attr_float, attr_list, attr_dict] + ) + + self.assertIsNotNone(client.post_entity(entity=entity, update=True)) res_entity = client.get_entity(entity_id=entity.id) for attr in entity.get_properties(): self.assertIn(attr, res_entity.get_properties()) - res_attr = client.get_attribute(entity_id=entity.id, - attr_name=attr.name) + res_attr = client.get_attribute( + entity_id=entity.id, attr_name=attr.name + ) self.assertEqual(type(res_attr.value), type(attr.value)) self.assertEqual(res_attr.value, attr.value) - value = client.get_attribute_value(entity_id=entity.id, - attr_name=attr.name) + value = client.get_attribute_value( + entity_id=entity.id, attr_name=attr.name + ) # unfortunately FIWARE returns an int for 20.0 although float # is expected if isinstance(value, int) and not isinstance(value, bool): @@ -502,13 +520,15 @@ def test_attribute_operations(self): self.assertEqual(value, attr.value) for attr_name, attr in entity.get_properties( - response_format='dict').items(): + response_format="dict" + ).items(): - client.update_entity_attribute(entity_id=entity.id, - attr_name=attr_name, - attr=attr) - value = client.get_attribute_value(entity_id=entity.id, - attr_name=attr_name) + client.update_entity_attribute( + entity_id=entity.id, attr_name=attr_name, attr=attr + ) + value = client.get_attribute_value( + entity_id=entity.id, attr_name=attr_name + ) # unfortunately FIWARE returns an int for 20.0 although float # is expected if isinstance(value, int) and not isinstance(value, bool): @@ -517,84 +537,75 @@ def test_attribute_operations(self): self.assertEqual(value, attr.value) new_value = 1337.0 - client.update_attribute_value(entity_id=entity.id, - attr_name='temperature', - value=new_value) - attr_value = client.get_attribute_value(entity_id=entity.id, - attr_name='temperature') + client.update_attribute_value( + entity_id=entity.id, attr_name="temperature", value=new_value + ) + attr_value = client.get_attribute_value( + entity_id=entity.id, attr_name="temperature" + ) self.assertEqual(attr_value, new_value) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_type_operations(self): """ Test type operations of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: - self.assertIsNotNone(client.post_entity(entity=self.entity, - update=True)) + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: + self.assertIsNotNone(client.post_entity(entity=self.entity, update=True)) client.get_entity_types() - client.get_entity_types(options='count') - client.get_entity_types(options='values') - client.get_entity_type(entity_type='MyType') - client.delete_entity(entity_id=self.entity.id, - entity_type=self.entity.type) + client.get_entity_types(options="count") + client.get_entity_types(options="values") + client.get_entity_type(entity_type="MyType") + client.delete_entity(entity_id=self.entity.id, entity_type=self.entity.type) # @unittest.skip('Does currently not reliably work in CI') - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_subscriptions(self): """ Test subscription operations of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: - sub_id = client.post_subscription(subscription=self.subscription, - skip_initial_notification=True) + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: + sub_id = client.post_subscription( + subscription=self.subscription, skip_initial_notification=True + ) sub_res = client.get_subscription(subscription_id=sub_id) time.sleep(1) sub_update = sub_res.model_copy( - update={'expires': datetime.now() + timedelta(days=2), - 'throttling': 1}, + update={"expires": datetime.now() + timedelta(days=2), "throttling": 1}, ) client.update_subscription(subscription=sub_update) sub_res_updated = client.get_subscription(subscription_id=sub_id) self.assertNotEqual(sub_res.expires, sub_res_updated.expires) self.assertEqual(sub_res.id, sub_res_updated.id) self.assertGreaterEqual(sub_res_updated.expires, sub_res.expires) - self.assertEqual(sub_res_updated.throttling, - sub_update.throttling) - - sub_with_nans = Subscription.model_validate({ - "description": "Test subscription with empty values", - "subject": { - "entities": [ - { - "idPattern": ".*", - "type": "Device" - } - ] - }, - "notification": { - "http": { - "url": "http://localhost:1234" - } - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) + self.assertEqual(sub_res_updated.throttling, sub_update.throttling) + + sub_with_nans = Subscription.model_validate( + { + "description": "Test subscription with empty values", + "subject": {"entities": [{"idPattern": ".*", "type": "Device"}]}, + "notification": {"http": {"url": "http://localhost:1234"}}, + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) sub_with_empty_list = sub_with_nans.model_copy(deep=True) sub_with_empty_list.notification.attrs = [] - test_subscriptions = [sub_with_empty_list, - sub_with_nans, - self.subscription] + test_subscriptions = [sub_with_empty_list, sub_with_nans, self.subscription] for _sub_raw in test_subscriptions: # test duplicate prevention and update @@ -607,24 +618,24 @@ def test_subscriptions(self): id2 = client.post_subscription(sub, update=False) self.assertEqual(id1, id2) sub_second_version = client.get_subscription(id2) - self.assertEqual(sub_first_version.description, - sub_second_version.description) + self.assertEqual( + sub_first_version.description, sub_second_version.description + ) # update=True, should override the existing id2 = client.post_subscription(sub, update=True) self.assertEqual(id1, id2) sub_second_version = client.get_subscription(id2) - self.assertNotEqual(sub_first_version.description, - sub_second_version.description) + self.assertNotEqual( + sub_first_version.description, sub_second_version.description + ) # test that duplicate prevention does not prevent to much sub2 = _sub_raw.model_copy() sub2.description = "Take this subscription to Fiware" sub2.subject.entities = [ - EntityPattern.model_validate({ - "idPattern": ".*", - "type": "Building" - } + EntityPattern.model_validate( + {"idPattern": ".*", "type": "Building"} ) ] id3 = client.post_subscription(sub2) @@ -634,43 +645,49 @@ def test_subscriptions(self): client.delete_subscription(id1) client.delete_subscription(id3) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_subscription_set_status(self): """ Test subscription operations of context broker client """ sub = self.subscription.model_copy( - update={'expires': datetime.now() + timedelta(days=2)}) + update={"expires": datetime.now() + timedelta(days=2)} + ) with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: sub_id = client.post_subscription(subscription=sub) sub_res = client.get_subscription(subscription_id=sub_id) self.assertEqual(sub_res.status, Status.ACTIVE) - sub_inactive = sub_res.model_copy(update={'status': Status.INACTIVE}) + sub_inactive = sub_res.model_copy(update={"status": Status.INACTIVE}) client.update_subscription(subscription=sub_inactive) sub_res_inactive = client.get_subscription(subscription_id=sub_id) self.assertEqual(sub_res_inactive.status, Status.INACTIVE) - sub_active = sub_res_inactive.model_copy(update={'status': Status.ACTIVE}) + sub_active = sub_res_inactive.model_copy(update={"status": Status.ACTIVE}) client.update_subscription(subscription=sub_active) sub_res_active = client.get_subscription(subscription_id=sub_id) self.assertEqual(sub_res_active.status, Status.ACTIVE) sub_expired = sub_res_active.model_copy( - update={'expires': datetime.now() - timedelta(days=365)}) + update={"expires": datetime.now() - timedelta(days=365)} + ) client.update_subscription(subscription=sub_expired) sub_res_expired = client.get_subscription(subscription_id=sub_id) self.assertEqual(sub_res_expired.status, Status.EXPIRED) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_subscription_alterationtypes(self): """ Test behavior of subscription alterationtypes since Orion 3.7.0 @@ -679,12 +696,14 @@ def test_subscription_alterationtypes(self): sub.subject.condition = Condition(alterationTypes=[]) sub.notification = Notification( mqtt=Mqtt( - url=settings.MQTT_BROKER_URL_INTERNAL, - topic="test/alterationtypes")) - test_entity = ContextEntity(id="test:alterationtypes", type="Room", - temperature={"type": "Number", - "value": 25.0} - ) + url=settings.MQTT_BROKER_URL_INTERNAL, topic="test/alterationtypes" + ) + ) + test_entity = ContextEntity( + id="test:alterationtypes", + type="Room", + temperature={"type": "Number", "value": 25.0}, + ) # test default with empty alterationTypes, triggered during actual change self.client.post_entity(test_entity) @@ -718,10 +737,11 @@ def test_subscription_alterationtypes(self): self.client.delete_subscription(sub_id_change) # test entityCreate - test_entity_create = ContextEntity(id="test:alterationtypes2", type="Room", - temperature={"type": "Number", - "value": 25.0} - ) + test_entity_create = ContextEntity( + id="test:alterationtypes2", + type="Room", + temperature={"type": "Number", "value": 25.0}, + ) sub.subject.condition.alterationTypes = ["entityCreate"] sub_id_create = self.client.post_subscription(subscription=sub) self.client.post_entity(test_entity_create) @@ -762,23 +782,27 @@ def test_subscription_alterationtypes(self): self.assertEqual(sub_result_update.notification.timesSent, 2) self.client.delete_subscription(sub_id_update) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_mqtt_subscriptions(self): mqtt_url = settings.MQTT_BROKER_URL mqtt_url_internal = settings.MQTT_BROKER_URL_INTERNAL - mqtt_topic = ''.join([settings.FIWARE_SERVICE, - settings.FIWARE_SERVICEPATH]) + mqtt_topic = "".join([settings.FIWARE_SERVICE, settings.FIWARE_SERVICEPATH]) notification = self.subscription.notification.model_copy( - update={'http': None, 'mqtt': Mqtt(url=mqtt_url_internal, - topic=mqtt_topic)}) + update={"http": None, "mqtt": Mqtt(url=mqtt_url_internal, topic=mqtt_topic)} + ) subscription = self.subscription.model_copy( - update={'notification': notification, - 'description': 'MQTT test subscription', - 'expires': None}) - entity = ContextEntity(id='myID', type='Room', **self.attr) + update={ + "notification": notification, + "description": "MQTT test subscription", + "expires": None, + } + ) + entity = ContextEntity(id="myID", type="Room", **self.attr) self.client.post_entity(entity=entity) sub_id = self.client.post_subscription(subscription) @@ -787,12 +811,12 @@ def test_mqtt_subscriptions(self): def on_connect(client, userdata, flags, reasonCode, properties=None): if reasonCode != 0: - logger.error(f"Connection failed with error code: " - f"'{reasonCode}'") + logger.error(f"Connection failed with error code: " f"'{reasonCode}'") raise ConnectionError else: - logger.info("Successfully, connected with result code " + str( - reasonCode)) + logger.info( + "Successfully, connected with result code " + str(reasonCode) + ) client.subscribe(mqtt_topic) def on_subscribe(client, userdata, mid, granted_qos, properties=None): @@ -804,14 +828,16 @@ def on_message(client, userdata, msg): sub_message = Message.model_validate_json(msg.payload) def on_disconnect(client, userdata, flags, reasonCode, properties=None): - logger.info("MQTT client disconnected with reasonCode " - + str(reasonCode)) + logger.info("MQTT client disconnected with reasonCode " + str(reasonCode)) import paho.mqtt.client as mqtt - mqtt_client = mqtt.Client(userdata=None, - protocol=mqtt.MQTTv5, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - transport="tcp") + + mqtt_client = mqtt.Client( + userdata=None, + protocol=mqtt.MQTTv5, + callback_api_version=mqtt.CallbackAPIVersion.VERSION2, + transport="tcp", + ) # add our callbacks to the client mqtt_client.on_connect = on_connect mqtt_client.on_subscribe = on_subscribe @@ -819,23 +845,27 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): mqtt_client.on_disconnect = on_disconnect # connect to the server - mqtt_client.connect(host=mqtt_url.host, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqtt_client.connect( + host=mqtt_url.host, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # create a non-blocking thread for mqtt communication mqtt_client.loop_start() new_value = 50 time.sleep(1) - self.client.update_attribute_value(entity_id=entity.id, - attr_name='temperature', - value=new_value, - entity_type=entity.type) + self.client.update_attribute_value( + entity_id=entity.id, + attr_name="temperature", + value=new_value, + entity_type=entity.type, + ) time.sleep(1) # test if the subscriptions arrives and the content aligns with updates @@ -846,9 +876,11 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): mqtt_client.disconnect() time.sleep(1) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_override_entity_keyvalues(self): entity1 = self.entity.model_copy(deep=True) # initial entity @@ -856,33 +888,37 @@ def test_override_entity_keyvalues(self): # entity with key value entity1_key_value = self.client.get_entity( - entity_id=entity1.id, - response_format=AttrsFormat.KEY_VALUES) + entity_id=entity1.id, response_format=AttrsFormat.KEY_VALUES + ) # override entity with ContextEntityKeyValues entity1_key_value.temperature = 30 self.client.override_entity(entity=entity1_key_value, key_values=True) - self.assertEqual(entity1_key_value, - self.client.get_entity( - entity_id=entity1.id, - response_format=AttrsFormat.KEY_VALUES) - ) + self.assertEqual( + entity1_key_value, + self.client.get_entity( + entity_id=entity1.id, response_format=AttrsFormat.KEY_VALUES + ), + ) # test replace all attributes entity1_key_value_dict = entity1_key_value.model_dump() entity1_key_value_dict["temp"] = 40 entity1_key_value_dict["humidity"] = 50 self.client.override_entity( - entity=ContextEntityKeyValues(**entity1_key_value_dict), - key_values=True) - self.assertEqual(entity1_key_value_dict, - self.client.get_entity( - entity_id=entity1.id, - response_format=AttrsFormat.KEY_VALUES).model_dump() - ) - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + entity=ContextEntityKeyValues(**entity1_key_value_dict), key_values=True + ) + self.assertEqual( + entity1_key_value_dict, + self.client.get_entity( + entity_id=entity1.id, response_format=AttrsFormat.KEY_VALUES + ).model_dump(), + ) + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_update_entity_keyvalues(self): entity1 = self.entity.model_copy(deep=True) # initial entity @@ -890,47 +926,48 @@ def test_update_entity_keyvalues(self): # key value entity1_key_value = self.client.get_entity( - entity_id=entity1.id, - response_format=AttrsFormat.KEY_VALUES) + entity_id=entity1.id, response_format=AttrsFormat.KEY_VALUES + ) # update entity with ContextEntityKeyValues entity1_key_value.temperature = 30 self.client.update_entity(entity=entity1_key_value, key_values=True) - self.assertEqual(entity1_key_value, - self.client.get_entity( - entity_id=entity1.id, - response_format=AttrsFormat.KEY_VALUES) - ) + self.assertEqual( + entity1_key_value, + self.client.get_entity( + entity_id=entity1.id, response_format=AttrsFormat.KEY_VALUES + ), + ) entity2 = self.client.get_entity(entity_id=entity1.id) - self.assertEqual(entity1.temperature.type, - entity2.temperature.type) + self.assertEqual(entity1.temperature.type, entity2.temperature.type) # update entity with dictionary entity1_key_value_dict = entity1_key_value.model_dump() entity1_key_value_dict["temperature"] = 40 - self.client.update_entity(entity=entity1_key_value_dict, - key_values=True) - self.assertEqual(entity1_key_value_dict, - self.client.get_entity( - entity_id=entity1.id, - response_format=AttrsFormat.KEY_VALUES).model_dump() - ) + self.client.update_entity(entity=entity1_key_value_dict, key_values=True) + self.assertEqual( + entity1_key_value_dict, + self.client.get_entity( + entity_id=entity1.id, response_format=AttrsFormat.KEY_VALUES + ).model_dump(), + ) entity3 = self.client.get_entity(entity_id=entity1.id) - self.assertEqual(entity1.temperature.type, - entity3.temperature.type) + self.assertEqual(entity1.temperature.type, entity3.temperature.type) # if attribute not existing, will be created entity1_key_value_dict.update({"humidity": 50}) - self.client.update_entity(entity=entity1_key_value_dict, - key_values=True) - self.assertEqual(entity1_key_value_dict, - self.client.get_entity( - entity_id=entity1.id, - response_format=AttrsFormat.KEY_VALUES).model_dump() - ) - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + self.client.update_entity(entity=entity1_key_value_dict, key_values=True) + self.assertEqual( + entity1_key_value_dict, + self.client.get_entity( + entity_id=entity1.id, response_format=AttrsFormat.KEY_VALUES + ).model_dump(), + ) + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_update_attributes_keyvalues(self): entity1 = self.entity.model_copy(deep=True) # initial entity @@ -938,19 +975,23 @@ def test_update_attributes_keyvalues(self): # update existing attributes self.client.update_or_append_entity_attributes( - entity_id=entity1.id, - attrs={"temperature": 30}, - key_values=True) - self.assertEqual(30, self.client.get_attribute_value(entity_id=entity1.id, - attr_name="temperature")) + entity_id=entity1.id, attrs={"temperature": 30}, key_values=True + ) + self.assertEqual( + 30, + self.client.get_attribute_value( + entity_id=entity1.id, attr_name="temperature" + ), + ) # update not existing attributes self.client.update_or_append_entity_attributes( - entity_id=entity1.id, - attrs={"humidity": 40}, - key_values=True) - self.assertEqual(40, self.client.get_attribute_value(entity_id=entity1.id, - attr_name="humidity")) + entity_id=entity1.id, attrs={"humidity": 40}, key_values=True + ) + self.assertEqual( + 40, + self.client.get_attribute_value(entity_id=entity1.id, attr_name="humidity"), + ) # update both existing and not existing attributes with self.assertRaises(RequestException): @@ -958,209 +999,157 @@ def test_update_attributes_keyvalues(self): entity_id=entity1.id, attrs={"humidity": 50, "co2": 300}, append_strict=True, - key_values=True) + key_values=True, + ) self.client.update_or_append_entity_attributes( - entity_id=entity1.id, - attrs={"humidity": 50, "co2": 300}, - key_values=True) - self.assertEqual(50, self.client.get_attribute_value(entity_id=entity1.id, - attr_name="humidity")) - self.assertEqual(300, self.client.get_attribute_value(entity_id=entity1.id, - attr_name="co2")) - self.assertEqual(30, self.client.get_attribute_value(entity_id=entity1.id, - attr_name="temperature")) - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + entity_id=entity1.id, attrs={"humidity": 50, "co2": 300}, key_values=True + ) + self.assertEqual( + 50, + self.client.get_attribute_value(entity_id=entity1.id, attr_name="humidity"), + ) + self.assertEqual( + 300, self.client.get_attribute_value(entity_id=entity1.id, attr_name="co2") + ) + self.assertEqual( + 30, + self.client.get_attribute_value( + entity_id=entity1.id, attr_name="temperature" + ), + ) + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_notification(self): mqtt_url = settings.MQTT_BROKER_URL mqtt_url_internal = settings.MQTT_BROKER_URL_INTERNAL - entity = ContextEntity.model_validate({ - "id": "Test:001", - "type": "Test", - "temperature": { - "type": "Number", - "value": 0 - }, - "humidity": { - "type": "Number", - "value": 0 - }, - "co2": { - "type": "Number", - "value": 0 + entity = ContextEntity.model_validate( + { + "id": "Test:001", + "type": "Test", + "temperature": {"type": "Number", "value": 0}, + "humidity": {"type": "Number", "value": 0}, + "co2": {"type": "Number", "value": 0}, } - }) + ) mqtt_topic = "notification/test" - sub_with_empty_notification = Subscription.model_validate({ - "description": "Test notification with empty values", - "subject": { - "entities": [ - { - "id": "Test:001", - "type": "Test" - } - ] - }, - "notification": { - "mqtt": { - "url": mqtt_url_internal, - "topic": mqtt_topic + sub_with_empty_notification = Subscription.model_validate( + { + "description": "Test notification with empty values", + "subject": {"entities": [{"id": "Test:001", "type": "Test"}]}, + "notification": { + "mqtt": {"url": mqtt_url_internal, "topic": mqtt_topic}, + "attrs": [], # empty attrs list }, - "attrs": [] # empty attrs list - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) - sub_with_none_notification = Subscription.model_validate({ - "description": "Test notification with none values", - "subject": { - "entities": [ - { - "id": "Test:001", - "type": "Test" - } - ] - }, - "notification": { - "mqtt": { - "url": mqtt_url_internal, - "topic": mqtt_topic + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) + sub_with_none_notification = Subscription.model_validate( + { + "description": "Test notification with none values", + "subject": {"entities": [{"id": "Test:001", "type": "Test"}]}, + "notification": { + "mqtt": {"url": mqtt_url_internal, "topic": mqtt_topic}, + "attrs": None, # attrs = None }, - "attrs": None # attrs = None - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) - sub_with_single_attr_notification = Subscription.model_validate({ - "description": "Test notification with single attribute", - "subject": { - "entities": [ - { - "id": "Test:001", - "type": "Test" - } - ] - }, - "notification": { - "mqtt": { - "url": mqtt_url_internal, - "topic": mqtt_topic + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) + sub_with_single_attr_notification = Subscription.model_validate( + { + "description": "Test notification with single attribute", + "subject": {"entities": [{"id": "Test:001", "type": "Test"}]}, + "notification": { + "mqtt": {"url": mqtt_url_internal, "topic": mqtt_topic}, + "attrs": ["temperature"], }, - "attrs": ["temperature"] - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) mqtt_custom_topic = "notification/custom" - sub_with_mqtt_custom_notification_payload = Subscription.model_validate({ - "description": "Test mqtt custom notification with payload message", - "subject": { - "entities": [ - { - "id": "Test:001", - "type": "Test" - } - ] - }, - "notification": { - "mqttCustom": { - "url": mqtt_url_internal, - "topic": mqtt_custom_topic, - "payload": "The value of the %22temperature%22 attribute %28of the device ${id}, ${type}%29 is" - " ${temperature}. Humidity is ${humidity} and CO2 is ${co2}." - }, - "attrs": ["temperature", "humidity", "co2"], - "onlyChangedAttrs": False - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) - - sub_with_mqtt_custom_notification_json = Subscription.model_validate({ - "description": "Test mqtt custom notification with json message", - "subject": { - "entities": [ - { - "id": "Test:001", - "type": "Test" - } - ] - }, - "notification": { - "mqttCustom": { - "url": mqtt_url_internal, - "topic": mqtt_custom_topic, - "json": { - "t": "${temperature}", - "h": "${humidity}", - "c": "${co2}" - } + sub_with_mqtt_custom_notification_payload = Subscription.model_validate( + { + "description": "Test mqtt custom notification with payload message", + "subject": {"entities": [{"id": "Test:001", "type": "Test"}]}, + "notification": { + "mqttCustom": { + "url": mqtt_url_internal, + "topic": mqtt_custom_topic, + "payload": "The value of the %22temperature%22 attribute %28of the device ${id}, ${type}%29 is" + " ${temperature}. Humidity is ${humidity} and CO2 is ${co2}.", + }, + "attrs": ["temperature", "humidity", "co2"], + "onlyChangedAttrs": False, }, - "attrs": ["temperature", "humidity", "co2"], - "onlyChangedAttrs": False - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) - - sub_with_mqtt_custom_notification_ngsi = Subscription.model_validate({ - "description": "Test mqtt custom notification with ngsi message", - "subject": { - "entities": [ - { - "id": "Test:001", - "type": "Test" - } - ] - }, - "notification": { - "mqttCustom": { - "url": mqtt_url_internal, - "topic": mqtt_custom_topic, - "ngsi": { - "id": "prefix:${id}", - "type": "newType", - "temperature": { - "value": 123, - "type": "Number" + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) + + sub_with_mqtt_custom_notification_json = Subscription.model_validate( + { + "description": "Test mqtt custom notification with json message", + "subject": {"entities": [{"id": "Test:001", "type": "Test"}]}, + "notification": { + "mqttCustom": { + "url": mqtt_url_internal, + "topic": mqtt_custom_topic, + "json": { + "t": "${temperature}", + "h": "${humidity}", + "c": "${co2}", }, - "co2_new": { - "value": "${co2}", - "type": "Number" - } + }, + "attrs": ["temperature", "humidity", "co2"], + "onlyChangedAttrs": False, + }, + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) - } + sub_with_mqtt_custom_notification_ngsi = Subscription.model_validate( + { + "description": "Test mqtt custom notification with ngsi message", + "subject": {"entities": [{"id": "Test:001", "type": "Test"}]}, + "notification": { + "mqttCustom": { + "url": mqtt_url_internal, + "topic": mqtt_custom_topic, + "ngsi": { + "id": "prefix:${id}", + "type": "newType", + "temperature": {"value": 123, "type": "Number"}, + "co2_new": {"value": "${co2}", "type": "Number"}, + }, + }, + "onlyChangedAttrs": False, }, - "onlyChangedAttrs": False - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) - - sub_with_covered_attrs_notification = Subscription.model_validate({ - "description": "Test notification with covered attributes", - "subject": { - "entities": [ - { - "id": "Test:001", - "type": "Test" - } - ] - }, - "notification": { - "mqtt": { - "url": mqtt_url_internal, - "topic": mqtt_topic + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) + + sub_with_covered_attrs_notification = Subscription.model_validate( + { + "description": "Test notification with covered attributes", + "subject": {"entities": [{"id": "Test:001", "type": "Test"}]}, + "notification": { + "mqtt": {"url": mqtt_url_internal, "topic": mqtt_topic}, + "attrs": ["temperature", "not_exist_attr"], + "covered": True, }, - "attrs": ["temperature", "not_exist_attr"], - "covered": True - }, - "expires": datetime.now() + timedelta(days=1), - "throttling": 0 - }) + "expires": datetime.now() + timedelta(days=1), + "throttling": 0, + } + ) # MQTT settings custom_sub_message = None @@ -1169,12 +1158,12 @@ def test_notification(self): def on_connect(client, userdata, flags, reasonCode, properties=None): if reasonCode != 0: - logger.error(f"Connection failed with error code: " - f"'{reasonCode}'") + logger.error(f"Connection failed with error code: " f"'{reasonCode}'") raise ConnectionError else: - logger.info("Successfully, connected with result code " + str( - reasonCode)) + logger.info( + "Successfully, connected with result code " + str(reasonCode) + ) client.subscribe(mqtt_topic) client.subscribe(mqtt_custom_topic) @@ -1182,8 +1171,7 @@ def on_subscribe(client, userdata, mid, granted_qos, properties=None): logger.info("Successfully subscribed to with QoS: %s", granted_qos) def on_message(client, userdata, msg): - logger.info("Received MQTT message: " + msg.topic + " " + str( - msg.payload)) + logger.info("Received MQTT message: " + msg.topic + " " + str(msg.payload)) nonlocal sub_message nonlocal custom_sub_message if msg.topic == mqtt_topic: @@ -1193,43 +1181,47 @@ def on_message(client, userdata, msg): custom_sub_message = msg.payload def on_disconnect(client, userdata, flags, reasonCode, properties=None): - logger.info("MQTT client disconnected with reasonCode " - + str(reasonCode)) + logger.info("MQTT client disconnected with reasonCode " + str(reasonCode)) import paho.mqtt.client as mqtt - mqtt_client = mqtt.Client(userdata=None, - protocol=mqtt.MQTTv5, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - transport="tcp") + + mqtt_client = mqtt.Client( + userdata=None, + protocol=mqtt.MQTTv5, + callback_api_version=mqtt.CallbackAPIVersion.VERSION2, + transport="tcp", + ) # add our callbacks to the client mqtt_client.on_connect = on_connect mqtt_client.on_subscribe = on_subscribe mqtt_client.on_message = on_message mqtt_client.on_disconnect = on_disconnect # connect to the server - mqtt_client.connect(host=mqtt_url.host, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqtt_client.connect( + host=mqtt_url.host, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # create a non-blocking thread for mqtt communication mqtt_client.loop_start() with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: client.post_entity(entity=entity) # test1 notification with empty attrs sub_id_1 = client.post_subscription( - subscription=sub_with_empty_notification) + subscription=sub_with_empty_notification + ) time.sleep(1) - client.update_attribute_value(entity_id=entity.id, - attr_name="temperature", - value=10 - ) + client.update_attribute_value( + entity_id=entity.id, attr_name="temperature", value=10 + ) # check the notified entities time.sleep(1) sub_1 = client.get_subscription(sub_id_1) @@ -1238,22 +1230,19 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): # test2 notification with None attrs, which should be identical to # the previous one - sub_id_2 = client.post_subscription( - subscription=sub_with_none_notification) + sub_id_2 = client.post_subscription(subscription=sub_with_none_notification) time.sleep(1) subscription_list = client.get_subscription_list() self.assertEqual(sub_id_1, sub_id_2) self.assertEqual(len(subscription_list), 1) - client.update_attribute_value(entity_id=entity.id, - attr_name="humidity", - value=20 - ) + client.update_attribute_value( + entity_id=entity.id, attr_name="humidity", value=20 + ) time.sleep(1) sub_1 = client.get_subscription(sub_id_1) self.assertEqual(sub_1.notification.timesSent, 2) - self.assertEqual( - sub_message.data[0].get_attribute("humidity").value, 20) + self.assertEqual(sub_message.data[0].get_attribute("humidity").value, 20) # test3 notification with single attribute, which should create a # new subscription @@ -1266,64 +1255,63 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): self.assertEqual(len(subscription_list), 2) # both sub1 and sub3 will be triggered by this update - client.update_attribute_value(entity_id=entity.id, - attr_name="co2", - value=30 - ) + client.update_attribute_value( + entity_id=entity.id, attr_name="co2", value=30 + ) time.sleep(1) sub_1 = client.get_subscription(sub_id_1) sub_3 = client.get_subscription(sub_id_3) self.assertEqual(sub_1.notification.timesSent, 3) self.assertEqual(sub_3.notification.timesSent, 1) + self.assertEqual(len(sub_messages[sub_id_1].data[0].get_attributes()), 3) self.assertEqual( - len(sub_messages[sub_id_1].data[0].get_attributes()), 3) - self.assertEqual( - sub_messages[sub_id_1].data[0].get_attribute("co2").value, 30) - self.assertEqual( - len(sub_messages[sub_id_3].data[0].get_attributes()), 1) + sub_messages[sub_id_1].data[0].get_attribute("co2").value, 30 + ) + self.assertEqual(len(sub_messages[sub_id_3].data[0].get_attributes()), 1) self.assertEqual( - sub_messages[sub_id_3].data[0].get_attribute( - "temperature").value, 10) + sub_messages[sub_id_3].data[0].get_attribute("temperature").value, 10 + ) # test4 notification with mqtt custom notification (payload) sub_id_4 = client.post_subscription( - subscription=sub_with_mqtt_custom_notification_payload) + subscription=sub_with_mqtt_custom_notification_payload + ) time.sleep(1) - client.update_attribute_value(entity_id=entity.id, - attr_name="temperature", - value=44 - ) + client.update_attribute_value( + entity_id=entity.id, attr_name="temperature", value=44 + ) time.sleep(1) sub_4 = client.get_subscription(sub_id_4) - self.assertEqual(first=custom_sub_message, - second=b'The value of the "temperature" attribute (of the device Test:001, Test) is 44. ' - b'Humidity is 20 and CO2 is 30.') + self.assertEqual( + first=custom_sub_message, + second=b'The value of the "temperature" attribute (of the device Test:001, Test) is 44. ' + b"Humidity is 20 and CO2 is 30.", + ) self.assertEqual(sub_4.notification.timesSent, 1) client.delete_subscription(sub_id_4) # test5 notification with mqtt custom notification (json) sub_id_5 = client.post_subscription( - subscription=sub_with_mqtt_custom_notification_json) + subscription=sub_with_mqtt_custom_notification_json + ) time.sleep(1) - client.update_attribute_value(entity_id=entity.id, - attr_name="humidity", - value=67 - ) + client.update_attribute_value( + entity_id=entity.id, attr_name="humidity", value=67 + ) time.sleep(1) sub_5 = client.get_subscription(sub_id_5) - self.assertEqual(first=custom_sub_message, - second=b'{"t":44,"h":67,"c":30}') + self.assertEqual(first=custom_sub_message, second=b'{"t":44,"h":67,"c":30}') self.assertEqual(sub_5.notification.timesSent, 1) client.delete_subscription(sub_id_5) # test6 notification with mqtt custom notification (ngsi) sub_id_6 = client.post_subscription( - subscription=sub_with_mqtt_custom_notification_ngsi) + subscription=sub_with_mqtt_custom_notification_ngsi + ) time.sleep(1) - client.update_attribute_value(entity_id=entity.id, - attr_name="co2", - value=78 - ) + client.update_attribute_value( + entity_id=entity.id, attr_name="co2", value=78 + ) time.sleep(1) sub_6 = client.get_subscription(sub_id_6) sub_message = Message.model_validate_json(custom_sub_message) @@ -1333,24 +1321,24 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): self.assertEqual(sub_message.data[0].type, "newType") self.assertEqual(sub_message.data[0].get_attribute("co2").value, 78) self.assertEqual(sub_message.data[0].get_attribute("co2_new").value, 78) - self.assertEqual(sub_message.data[0].get_attribute("temperature").value, 123) + self.assertEqual( + sub_message.data[0].get_attribute("temperature").value, 123 + ) client.delete_subscription(sub_id_6) # test7 notification with covered attributes sub_id_7 = client.post_subscription( subscription=sub_with_covered_attrs_notification, - ) + ) time.sleep(1) - client.update_attribute_value(entity_id=entity.id, - attr_name="temperature", - value=40 - ) + client.update_attribute_value( + entity_id=entity.id, attr_name="temperature", value=40 + ) time.sleep(1) sub_4 = client.get_subscription(sub_id_7) self.assertEqual(sub_4.notification.timesSent, 1) notified_attr_names = sub_messages[sub_id_7].data[0].get_attribute_names() - self.assertEqual( - len(notified_attr_names), 2) + self.assertEqual(len(notified_attr_names), 2) self.assertIn("temperature", notified_attr_names) self.assertIn("not_exist_attr", notified_attr_names) with self.assertRaises(KeyError): @@ -1358,44 +1346,57 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): # so it will not be taken as an attribute by filip sub_messages[sub_id_7].data[0].get_attribute("not_exist_attr") - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_batch_operations(self): """ Test batch operations of context broker client """ with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: - entities = [ContextEntity(id=str(i), - type=f'filip:object:TypeA') for i in - range(0, 1000)] + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: + entities = [ + ContextEntity(id=str(i), type=f"filip:object:TypeA") + for i in range(0, 1000) + ] client.update(entities=entities, action_type=ActionType.APPEND) - entities = [ContextEntity(id=str(i), - type=f'filip:object:TypeB') for i in - range(0, 1000)] + entities = [ + ContextEntity(id=str(i), type=f"filip:object:TypeB") + for i in range(0, 1000) + ] client.update(entities=entities, action_type=ActionType.APPEND) entity = EntityPattern(idPattern=".*", typePattern=".*TypeA$") query = Query.model_validate( - {"entities": [entity.model_dump(exclude_unset=True)]}) - self.assertEqual(1000, - len(client.query(query=query, - response_format='keyValues'))) + {"entities": [entity.model_dump(exclude_unset=True)]} + ) + self.assertEqual( + 1000, len(client.query(query=query, response_format="keyValues")) + ) # update with keyValues - entities_keyvalues = [ContextEntityKeyValues(id=str(i), - type=f'filip:object:TypeC', - attr1="text attribute", - attr2=1 - ) for i in range(0, 1000)] - client.update(entities=entities_keyvalues, - update_format="keyValues", - action_type=ActionType.APPEND) + entities_keyvalues = [ + ContextEntityKeyValues( + id=str(i), + type=f"filip:object:TypeC", + attr1="text attribute", + attr2=1, + ) + for i in range(0, 1000) + ] + client.update( + entities=entities_keyvalues, + update_format="keyValues", + action_type=ActionType.APPEND, + ) entity_keyvalues = EntityPattern(idPattern=".*", typePattern=".*TypeC$") query_keyvalues = Query.model_validate( - {"entities": [entity_keyvalues.model_dump(exclude_unset=True)]}) - entities_keyvalues_query = client.query(query=query_keyvalues, - response_format='keyValues') + {"entities": [entity_keyvalues.model_dump(exclude_unset=True)]} + ) + entities_keyvalues_query = client.query( + query=query_keyvalues, response_format="keyValues" + ) self.assertEqual(1000, len(entities_keyvalues_query)) self.assertEqual(1000, sum([e.attr2 for e in entities_keyvalues_query])) @@ -1406,148 +1407,132 @@ def test_force_update_option(self): entity_dict = { "id": "TestForceupdate:001", "type": "Test", - "temperature": {"type": "Number", - "value": 20}, - "humidity": {"type": "Number", - "value": 50}, + "temperature": {"type": "Number", "value": 20}, + "humidity": {"type": "Number", "value": 50}, } entity = ContextEntity(**entity_dict) self.client.post_entity(entity=entity) # test with only changed attrs - sub_only_changed_attrs = Subscription(**{ - "description": "One subscription to rule them all", - "subject": { - "entities": [ - { - "id": entity.id, - } - ] - }, - "notification": { - "http": { - "url": "http://localhost:1234" + sub_only_changed_attrs = Subscription( + **{ + "description": "One subscription to rule them all", + "subject": { + "entities": [ + { + "id": entity.id, + } + ] + }, + "notification": { + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], + "onlyChangedAttrs": True, }, - "attrs": [ - "temperature", - "humidity" - ], - "onlyChangedAttrs": True } - }) - sub_id_1 = self.client.post_subscription( - subscription=sub_only_changed_attrs) + ) + sub_id_1 = self.client.post_subscription(subscription=sub_only_changed_attrs) time_sent_1 = 0 # not activate self.client.update_attribute_value( - entity_id=entity.id, - attr_name="temperature", - value=20) + entity_id=entity.id, attr_name="temperature", value=20 + ) time.sleep(1) sub_1 = self.client.get_subscription(sub_id_1) - time_sent_1_is = sub_1.notification.timesSent if \ - sub_1.notification.timesSent else 0 + time_sent_1_is = ( + sub_1.notification.timesSent if sub_1.notification.timesSent else 0 + ) self.assertEqual(time_sent_1_is, time_sent_1) # activate because value changed self.client.update_attribute_value( - entity_id=entity.id, - attr_name="temperature", - value=21) + entity_id=entity.id, attr_name="temperature", value=21 + ) time_sent_1 += 1 # should be activated time.sleep(1) sub_1 = self.client.get_subscription(sub_id_1) - time_sent_1_is = sub_1.notification.timesSent if \ - sub_1.notification.timesSent else 0 + time_sent_1_is = ( + sub_1.notification.timesSent if sub_1.notification.timesSent else 0 + ) self.assertEqual(time_sent_1_is, time_sent_1) # activate because forceUpdate self.client.update_attribute_value( - entity_id=entity.id, - attr_name="temperature", - value=21, - forcedUpdate=True + entity_id=entity.id, attr_name="temperature", value=21, forcedUpdate=True ) time_sent_1 += 1 # should be activated time.sleep(1) sub_1 = self.client.get_subscription(sub_id_1) - time_sent_1_is = sub_1.notification.timesSent if \ - sub_1.notification.timesSent else 0 + time_sent_1_is = ( + sub_1.notification.timesSent if sub_1.notification.timesSent else 0 + ) self.assertEqual(time_sent_1_is, time_sent_1) # test with conditions - sub_with_conditions = Subscription(**{ - "description": "One subscription to rule them all", - "subject": { - "entities": [ - { - "id": entity.id, - } - ], - "condition": { - "attrs": [ - "temperature" + sub_with_conditions = Subscription( + **{ + "description": "One subscription to rule them all", + "subject": { + "entities": [ + { + "id": entity.id, + } ], - "expression": { - "q": "temperature>40" - } - } - }, - "notification": { - "http": { - "url": "http://localhost:1234" + "condition": { + "attrs": ["temperature"], + "expression": {"q": "temperature>40"}, + }, + }, + "notification": { + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], }, - "attrs": [ - "temperature", - "humidity" - ] } - }) - sub_id_2 = self.client.post_subscription( - subscription=sub_with_conditions) + ) + sub_id_2 = self.client.post_subscription(subscription=sub_with_conditions) time_sent_2 = 0 # not activate self.client.update_attribute_value( - entity_id=entity.id, - attr_name="temperature", - value=20) + entity_id=entity.id, attr_name="temperature", value=20 + ) time.sleep(1) sub_2 = self.client.get_subscription(sub_id_2) - time_sent_2_is = sub_2.notification.timesSent if \ - sub_2.notification.timesSent else 0 + time_sent_2_is = ( + sub_2.notification.timesSent if sub_2.notification.timesSent else 0 + ) self.assertEqual(time_sent_2_is, time_sent_2) # activate because condition fulfilled self.client.update_attribute_value( - entity_id=entity.id, - attr_name="temperature", - value=41) + entity_id=entity.id, attr_name="temperature", value=41 + ) time_sent_2 += 1 # should be activated time.sleep(1) sub_2 = self.client.get_subscription(sub_id_2) - time_sent_2_is = sub_2.notification.timesSent if \ - sub_2.notification.timesSent else 0 + time_sent_2_is = ( + sub_2.notification.timesSent if sub_2.notification.timesSent else 0 + ) self.assertEqual(time_sent_2_is, time_sent_2) # not activate even with forceUpdate self.client.update_attribute_value( - entity_id=entity.id, - attr_name="temperature", - value=20, - forcedUpdate=True + entity_id=entity.id, attr_name="temperature", value=20, forcedUpdate=True ) time.sleep(1) sub_2 = self.client.get_subscription(sub_id_2) - time_sent_2_is = sub_2.notification.timesSent if \ - sub_2.notification.timesSent else 0 + time_sent_2_is = ( + sub_2.notification.timesSent if sub_2.notification.timesSent else 0 + ) self.assertEqual(time_sent_2_is, time_sent_2) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_command_with_mqtt(self): """ Test if a command can be send to a device in FIWARE @@ -1563,46 +1548,43 @@ def test_command_with_mqtt(self): """ mqtt_broker_url = settings.MQTT_BROKER_URL - device_attr1 = DeviceAttribute(name='temperature', - object_id='t', - type="Number", - metadata={ - "unit": - {"type": "Unit", - "value": { - "name": { - "type": "Text", - "value": "degree " - "Celsius" - } - }} - }) + device_attr1 = DeviceAttribute( + name="temperature", + object_id="t", + type="Number", + metadata={ + "unit": { + "type": "Unit", + "value": {"name": {"type": "Text", "value": "degree " "Celsius"}}, + } + }, + ) # creating a static attribute that holds additional information - static_device_attr = StaticDeviceAttribute(name='info', - type="Text", - value="Filip example for " - "virtual IoT device") + static_device_attr = StaticDeviceAttribute( + name="info", type="Text", value="Filip example for " "virtual IoT device" + ) # creating a command that the IoT device will liston to - device_command = DeviceCommand(name='heater', type="Boolean") - - device = Device(device_id='MyDevice', - entity_name='MyDevice', - entity_type='Thing2', - protocol='IoTA-JSON', - transport='MQTT', - apikey=settings.FIWARE_SERVICEPATH.strip('/'), - attributes=[device_attr1], - static_attributes=[static_device_attr], - commands=[device_command]) - - device_attr2 = DeviceAttribute(name='humidity', - object_id='h', - type="Number", - metadata={ - "unitText": - {"value": "percent", - "type": "Text"}}) + device_command = DeviceCommand(name="heater", type="Boolean") + + device = Device( + device_id="MyDevice", + entity_name="MyDevice", + entity_type="Thing2", + protocol="IoTA-JSON", + transport="MQTT", + apikey=settings.FIWARE_SERVICEPATH.strip("/"), + attributes=[device_attr1], + static_attributes=[static_device_attr], + commands=[device_command], + ) + + device_attr2 = DeviceAttribute( + name="humidity", + object_id="h", + type="Number", + metadata={"unitText": {"value": "percent", "type": "Text"}}, + ) device.add_attribute(attribute=device_attr2) @@ -1611,13 +1593,15 @@ def test_command_with_mqtt(self): service_group = ServiceGroup( service=self.fiware_header.service, subservice=self.fiware_header.service_path, - apikey=settings.FIWARE_SERVICEPATH.strip('/'), - resource='/iot/json') + apikey=settings.FIWARE_SERVICEPATH.strip("/"), + resource="/iot/json", + ) # create the Http client node that once sent the device cannot be posted # again and you need to use the update command - config = HttpClientConfig(cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + config = HttpClientConfig( + cb_url=settings.CB_URL, iota_url=settings.IOTA_JSON_URL + ) client = HttpClient(fiware_header=self.fiware_header, config=config) client.iota.post_group(service_group=service_group, update=True) client.iota.post_device(device=device, update=True) @@ -1630,8 +1614,9 @@ def test_command_with_mqtt(self): device = client.iota.get_device(device_id=device.device_id) # check if the data entity is created in the context broker - entity = client.cb.get_entity(entity_id=device.device_id, - entity_type=device.entity_type) + entity = client.cb.get_entity( + entity_id=device.device_id, entity_type=device.entity_type + ) # create a mqtt client that we use as representation of an IoT device # following the official documentation of Paho-MQTT. @@ -1653,18 +1638,21 @@ def on_subscribe(client, userdata, mid, granted_qos, properties=None): def on_message(client, userdata, msg): data = json.loads(msg.payload) res = {k: v for k, v in data.items()} - client.publish(topic=f"/json/{service_group.apikey}" - f"/{device.device_id}/cmdexe", - payload=json.dumps(res)) + client.publish( + topic=f"/json/{service_group.apikey}" f"/{device.device_id}/cmdexe", + payload=json.dumps(res), + ) def on_disconnect(client, userdata, flags, reasonCode, properties=None): pass - mqtt_client = mqtt.Client(client_id="filip-test", - userdata=None, - protocol=mqtt.MQTTv5, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - transport="tcp") + mqtt_client = mqtt.Client( + client_id="filip-test", + userdata=None, + protocol=mqtt.MQTTv5, + callback_api_version=mqtt.CallbackAPIVersion.VERSION2, + transport="tcp", + ) # add our callbacks to the client mqtt_client.on_connect = on_connect @@ -1673,36 +1661,40 @@ def on_disconnect(client, userdata, flags, reasonCode, properties=None): mqtt_client.on_disconnect = on_disconnect # extract the form the environment - mqtt_client.connect(host=mqtt_broker_url.host, - port=mqtt_broker_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqtt_client.connect( + host=mqtt_broker_url.host, + port=mqtt_broker_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # create a non-blocking thread for mqtt communication mqtt_client.loop_start() for attr in device.attributes: mqtt_client.publish( topic=f"/json/{service_group.apikey}/{device.device_id}/attrs", - payload=json.dumps({attr.object_id: random.randint(0, 9)})) + payload=json.dumps({attr.object_id: random.randint(0, 9)}), + ) time.sleep(5) - entity = client.cb.get_entity(entity_id=device.device_id, - entity_type=device.entity_type) + entity = client.cb.get_entity( + entity_id=device.device_id, entity_type=device.entity_type + ) # create and send a command via the context broker - context_command = NamedCommand(name=device_command.name, - value=False) - client.cb.post_command(entity_id=entity.id, - entity_type=entity.type, - command=context_command) + context_command = NamedCommand(name=device_command.name, value=False) + client.cb.post_command( + entity_id=entity.id, entity_type=entity.type, command=context_command + ) time.sleep(5) # check the entity the command attribute should now show OK - entity = client.cb.get_entity(entity_id=device.device_id, - entity_type=device.entity_type) + entity = client.cb.get_entity( + entity_id=device.device_id, entity_type=device.entity_type + ) # The main part of this test, for all this setup was done self.assertEqual("OK", entity.heater_status.value) @@ -1723,17 +1715,14 @@ def test_patch_entity(self) -> None: # setup test-entity entity = ContextEntity(id="test_id1", type="test_type1") attr1 = NamedContextAttribute(name="attr1", value="1") - attr1.metadata["m1"] = \ - NamedMetadata(name="meta1", type="metatype", value="2") + attr1.metadata["m1"] = NamedMetadata(name="meta1", type="metatype", value="2") attr2 = NamedContextAttribute(name="attr2", value="2") - attr1.metadata["m2"] = \ - NamedMetadata(name="meta2", type="metatype", value="3") + attr1.metadata["m2"] = NamedMetadata(name="meta2", type="metatype", value="3") entity.add_attributes([attr1, attr2]) # sub-Test1: Post new. No old entity not exist or is provided! self.client.patch_entity(entity=entity) - self.assertEqual(entity, - self.client.get_entity(entity_id=entity.id)) + self.assertEqual(entity, self.client.get_entity(entity_id=entity.id)) self.tearDown() # sub-Test2: ID/type of old_entity changed. Old entity is provided and @@ -1742,8 +1731,7 @@ def test_patch_entity(self) -> None: test_entity = ContextEntity(id="newID", type="newType") test_entity.add_attributes([attr1, attr2]) self.client.patch_entity(test_entity, old_entity=entity) - self.assertEqual(test_entity, - self.client.get_entity(entity_id=test_entity.id)) + self.assertEqual(test_entity, self.client.get_entity(entity_id=test_entity.id)) # assert that former entity_id is freed again with self.assertRaises(RequestException): self.client.get_entity(entity_id=entity.id) @@ -1770,14 +1758,14 @@ def test_patch_entity(self) -> None: self.assertEqual(test_entity.id, entity.id) self.assertEqual(test_entity.type, entity.type) attr1_changed = NamedContextAttribute(name="attr1", value="2") - attr1_changed.metadata["m4"] = \ - NamedMetadata(name="meta3", type="metatype5", value="4") + attr1_changed.metadata["m4"] = NamedMetadata( + name="meta3", type="metatype5", value="4" + ) attr3 = NamedContextAttribute(name="attr3", value="3") test_entity.add_attributes([attr1_changed, attr3]) self.client.patch_entity(test_entity) - self.assertEqual(test_entity, - self.client.get_entity(entity_id=entity.id)) + self.assertEqual(test_entity, self.client.get_entity(entity_id=entity.id)) self.tearDown() # sub-Test6: Attr changes, concurrent changes in Fiware, @@ -1800,8 +1788,7 @@ def test_patch_entity(self) -> None: result_entity = concurrent_entity result_entity.add_attributes([attr2, attr3]) - self.assertEqual(result_entity, - self.client.get_entity(entity_id=entity.id)) + self.assertEqual(result_entity, self.client.get_entity(entity_id=entity.id)) self.tearDown() def test_delete_entity_devices(self): @@ -1827,21 +1814,25 @@ def test_delete_entity_devices(self): service_path=settings.FIWARE_SERVICEPATH, device_id=device_id, entity_type=entity_type, - entity_name=entity_id + entity_name=entity_id, ) devices.append(device) self.iotac.post_devices(devices=devices) while devices: device = devices.pop() - self.client.delete_entity(entity_id=device.entity_name, - entity_type=device.entity_type, - delete_devices=True, - iota_url=settings.IOTA_JSON_URL) + self.client.delete_entity( + entity_id=device.entity_name, + entity_type=device.entity_type, + delete_devices=True, + iota_url=settings.IOTA_JSON_URL, + ) self.assertEqual(len(self.iotac.get_device_list()), len(devices)) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_send_receive_string(self): # test updating a string value entity = ContextEntity(id="string_test", type="test_type1") @@ -1850,11 +1841,13 @@ def test_send_receive_string(self): self.client.post_entity(entity=entity) testData = "hello_test" - self.client.update_attribute_value(entity_id="string_test", attr_name="data", - value=testData) + self.client.update_attribute_value( + entity_id="string_test", attr_name="data", value=testData + ) - readback = self.client.get_attribute_value(entity_id="string_test", - attr_name="data") + readback = self.client.get_attribute_value( + entity_id="string_test", attr_name="data" + ) self.assertEqual(testData, readback) @@ -1872,33 +1865,36 @@ def test_optional_entity_type(self): self.client.post_entity(entity=entity) # test post_command - device_command = DeviceCommand(name='heater', type="Boolean") - device = Device(device_id='MyDevice', - entity_name='MyDevice', - entity_type='Thing', - protocol='IoTA-JSON', - transport='MQTT', - apikey=settings.FIWARE_SERVICEPATH.strip('/'), - commands=[device_command]) + device_command = DeviceCommand(name="heater", type="Boolean") + device = Device( + device_id="MyDevice", + entity_name="MyDevice", + entity_type="Thing", + protocol="IoTA-JSON", + transport="MQTT", + apikey=settings.FIWARE_SERVICEPATH.strip("/"), + commands=[device_command], + ) self.iotac.post_device(device=device) - test_command = NamedCommand(name='heater', value=True) + test_command = NamedCommand(name="heater", value=True) self.client.post_command(entity_id="MyDevice", command=test_command) # update_or_append_entity_attributes entityAttr.value = "value1" attr_data2 = NamedContextAttribute(name="data2", value="value2") - self.client.update_or_append_entity_attributes(entity_id=test_entity_id, - attrs=[entityAttr, - attr_data2]) + self.client.update_or_append_entity_attributes( + entity_id=test_entity_id, attrs=[entityAttr, attr_data2] + ) # update_existing_entity_attributes - self.client.update_existing_entity_attributes(entity_id=test_entity_id, - attrs=[entityAttr, - attr_data2]) + self.client.update_existing_entity_attributes( + entity_id=test_entity_id, attrs=[entityAttr, attr_data2] + ) # replace_entity_attributes - self.client.replace_entity_attributes(entity_id=test_entity_id, - attrs=[entityAttr, attr_data2]) + self.client.replace_entity_attributes( + entity_id=test_entity_id, attrs=[entityAttr, attr_data2] + ) # delete entity self.client.delete_entity(entity_id=test_entity_id) @@ -1915,20 +1911,20 @@ def test_optional_entity_type(self): attr_data2 = NamedContextAttribute(name="data2", value="value2") with self.assertRaises(requests.HTTPError): self.client.update_or_append_entity_attributes( - entity_id=test_entity_id, - attrs=[entityAttr, attr_data2]) + entity_id=test_entity_id, attrs=[entityAttr, attr_data2] + ) # update_existing_entity_attributes with self.assertRaises(requests.HTTPError): self.client.update_existing_entity_attributes( - entity_id=test_entity_id, - attrs=[entityAttr, attr_data2]) + entity_id=test_entity_id, attrs=[entityAttr, attr_data2] + ) # replace_entity_attributes with self.assertRaises(requests.HTTPError): self.client.replace_entity_attributes( - entity_id=test_entity_id, - attrs=[entityAttr, attr_data2]) + entity_id=test_entity_id, attrs=[entityAttr, attr_data2] + ) # delete entity with self.assertRaises(requests.HTTPError): @@ -1939,14 +1935,11 @@ def test_does_entity_exist(self): entity = ContextEntity(id=str(_id), type="test_type1") self.assertFalse( - self.client.does_entity_exist( - entity_id=entity.id, - entity_type=entity.type)) + self.client.does_entity_exist(entity_id=entity.id, entity_type=entity.type) + ) self.client.post_entity(entity=entity) self.assertTrue( - self.client.does_entity_exist( - entity_id=entity.id, - entity_type=entity.type) + self.client.does_entity_exist(entity_id=entity.id, entity_type=entity.type) ) def tearDown(self) -> None: @@ -1954,6 +1947,8 @@ def tearDown(self) -> None: Cleanup test server """ self.client.close() - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) diff --git a/tests/clients/test_ngsi_v2_client.py b/tests/clients/test_ngsi_v2_client.py index bf6c1a89..73009af7 100644 --- a/tests/clients/test_ngsi_v2_client.py +++ b/tests/clients/test_ngsi_v2_client.py @@ -1,6 +1,7 @@ """ Test for filip.core.client """ + import unittest import json import requests @@ -24,8 +25,9 @@ def setUp(self) -> None: Returns: None """ - self.fh = FiwareHeader(service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) + self.fh = FiwareHeader( + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) self.create_json_file() with open(self.get_json_path()) as f: self.config = json.load(f) @@ -35,9 +37,9 @@ def create_json_file(self) -> None: Create a json settings file based on the current environment settings """ content = { - "cb_url": str(settings.CB_URL), - "iota_url": str(settings.IOTA_JSON_URL), - "ql_url": str(settings.QL_URL) + "cb_url": str(settings.CB_URL), + "iota_url": str(settings.IOTA_JSON_URL), + "ql_url": str(settings.QL_URL), } with open(self.get_json_path(), "w") as file: file.write(json.dumps(content, indent=4)) @@ -52,7 +54,7 @@ def get_json_path() -> str: # Match the needed path to the config file in both cases path = Path(__file__).parent.resolve() - return str(path.joinpath('test_ngsi_v2_client.json')) + return str(path.joinpath("test_ngsi_v2_client.json")) @staticmethod def get_env_path() -> str: @@ -62,7 +64,7 @@ def get_env_path() -> str: # Test if the testcase was run directly or over in a global test-run. # Match the needed path to the config file in both cases path = Path(__file__).parent.resolve() - return str(path.joinpath('.env.filip')) + return str(path.joinpath(".env.filip")) def _test_change_of_headers(self, client: HttpClient): """ @@ -73,39 +75,57 @@ def _test_change_of_headers(self, client: HttpClient): Returns: None """ - self.assertEqual(id(client.fiware_service_path), - id(client.cb.fiware_service_path), - 'FIWARE Service path out of sync') - self.assertEqual(id(client.fiware_service_path), - id(client.iota.fiware_service_path), - 'FIWARE Service path out of sync') - self.assertEqual(id(client.fiware_service_path), - id(client.timeseries.fiware_service_path), - 'FIWARE Service path out of sync') - - client.fiware_service = 'filip_other' - - self.assertEqual(client.fiware_service_path, - client.cb.fiware_service_path, - 'FIWARE service out of sync') - self.assertEqual(client.fiware_service_path, - client.iota.fiware_service_path, - 'FIWARE service out of sync') - self.assertEqual(client.fiware_service_path, - client.timeseries.fiware_service_path, - 'FIWARE service out of sync') + self.assertEqual( + id(client.fiware_service_path), + id(client.cb.fiware_service_path), + "FIWARE Service path out of sync", + ) + self.assertEqual( + id(client.fiware_service_path), + id(client.iota.fiware_service_path), + "FIWARE Service path out of sync", + ) + self.assertEqual( + id(client.fiware_service_path), + id(client.timeseries.fiware_service_path), + "FIWARE Service path out of sync", + ) + + client.fiware_service = "filip_other" + + self.assertEqual( + client.fiware_service_path, + client.cb.fiware_service_path, + "FIWARE service out of sync", + ) + self.assertEqual( + client.fiware_service_path, + client.iota.fiware_service_path, + "FIWARE service out of sync", + ) + self.assertEqual( + client.fiware_service_path, + client.timeseries.fiware_service_path, + "FIWARE service out of sync", + ) client.fiware_service_path = generate_servicepath() - self.assertEqual(client.fiware_service_path, - client.cb.fiware_service_path, - 'FIWARE Service path out of sync') - self.assertEqual(client.fiware_service_path, - client.iota.fiware_service_path, - 'FIWARE Service path out of sync') - self.assertEqual(client.fiware_service_path, - client.timeseries.fiware_service_path, - 'FIWARE Service path out of sync') + self.assertEqual( + client.fiware_service_path, + client.cb.fiware_service_path, + "FIWARE Service path out of sync", + ) + self.assertEqual( + client.fiware_service_path, + client.iota.fiware_service_path, + "FIWARE Service path out of sync", + ) + self.assertEqual( + client.fiware_service_path, + client.timeseries.fiware_service_path, + "FIWARE Service path out of sync", + ) @staticmethod def _test_connections(client: HttpClient): @@ -164,26 +184,23 @@ def test_session_handling(self): None """ # with new session object - with HttpClient(config=self.config, - fiware_header=self.fh) as client: + with HttpClient(config=self.config, fiware_header=self.fh) as client: self.assertIsNotNone(client.session) self._test_connections(client=client) self._test_change_of_headers(client=client) # with external session with requests.Session() as s: - client = HttpClient(config=self.config, - session=s, - fiware_header=self.fh) + client = HttpClient(config=self.config, session=s, fiware_header=self.fh) self.assertEqual(client.session, s) self._test_connections(client=client) self._test_change_of_headers(client=client) # with external session but unnecessary 'with'-statement with requests.Session() as s: - with HttpClient(config=self.config, - session=s, - fiware_header=self.fh) as client: + with HttpClient( + config=self.config, session=s, fiware_header=self.fh + ) as client: self.assertEqual(client.session, s) self._test_connections(client=client) self._test_change_of_headers(client=client) @@ -198,6 +215,7 @@ def tearDown(self) -> None: # remove create json and env config file import os + os.remove(self.get_json_path()) try: os.remove(self.get_env_path()) diff --git a/tests/clients/test_ngsi_v2_iota.py b/tests/clients/test_ngsi_v2_iota.py index 8518a285..1bcfbacd 100644 --- a/tests/clients/test_ngsi_v2_iota.py +++ b/tests/clients/test_ngsi_v2_iota.py @@ -1,6 +1,7 @@ """ Test for iota http client """ + import copy import unittest import logging @@ -9,21 +10,22 @@ from uuid import uuid4 from filip.models.base import FiwareHeader, DataType -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient -from filip.models.ngsi_v2.iot import \ - ServiceGroup, \ - Device, \ - DeviceAttribute, \ - DeviceCommand, \ - LazyDeviceAttribute, \ - StaticDeviceAttribute, ExpressionLanguage -from filip.utils.cleanup import \ - clear_all, \ - clean_test, \ - clear_context_broker, \ - clear_iot_agent +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient +from filip.models.ngsi_v2.iot import ( + ServiceGroup, + Device, + DeviceAttribute, + DeviceCommand, + LazyDeviceAttribute, + StaticDeviceAttribute, + ExpressionLanguage, +) +from filip.utils.cleanup import ( + clear_all, + clean_test, + clear_context_broker, + clear_iot_agent, +) from tests.config import settings logger = logging.getLogger(__name__) @@ -33,87 +35,92 @@ class TestAgent(unittest.TestCase): def setUp(self) -> None: self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) - self.service_group1 = ServiceGroup(entity_type='Thing', - resource='/iot/json', - apikey=str(uuid4())) - self.service_group2 = ServiceGroup(entity_type='OtherThing', - resource='/iot/json', - apikey=str(uuid4())) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) + self.service_group1 = ServiceGroup( + entity_type="Thing", resource="/iot/json", apikey=str(uuid4()) + ) + self.service_group2 = ServiceGroup( + entity_type="OtherThing", resource="/iot/json", apikey=str(uuid4()) + ) self.device = { "device_id": "test_device", "service": self.fiware_header.service, "service_path": self.fiware_header.service_path, "entity_name": "test_entity", "entity_type": "test_entity_type", - "timezone": 'Europe/Berlin', + "timezone": "Europe/Berlin", "timestamp": None, "apikey": "1234", "endpoint": None, - "transport": 'HTTP', - "expressionLanguage": ExpressionLanguage.JEXL + "transport": "HTTP", + "expressionLanguage": ExpressionLanguage.JEXL, } self.client = IoTAClient( - url=settings.IOTA_JSON_URL, - fiware_header=self.fiware_header) + url=settings.IOTA_JSON_URL, fiware_header=self.fiware_header + ) def test_get_version(self): with IoTAClient( - url=settings.IOTA_JSON_URL, - fiware_header=self.fiware_header) as client: + url=settings.IOTA_JSON_URL, fiware_header=self.fiware_header + ) as client: self.assertIsNotNone(client.get_version()) def test_service_group_model(self): pass - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + iota_url=settings.IOTA_JSON_URL, + ) def test_service_group_endpoints(self): - self.client.post_groups(service_groups=[self.service_group1, - self.service_group2]) + self.client.post_groups( + service_groups=[self.service_group1, self.service_group2] + ) groups = self.client.get_group_list() with self.assertRaises(requests.RequestException): self.client.post_groups(groups, update=False) - self.client.get_group(resource=self.service_group1.resource, - apikey=self.service_group1.apikey) + self.client.get_group( + resource=self.service_group1.resource, apikey=self.service_group1.apikey + ) def test_device_model(self): device = Device(**self.device) - self.assertEqual(self.device, - device.model_dump(exclude_unset=True)) - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + self.assertEqual(self.device, device.model_dump(exclude_unset=True)) + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_device_endpoints(self): """ Test device creation """ with IoTAClient( - url=settings.IOTA_JSON_URL, - fiware_header=self.fiware_header) as client: + url=settings.IOTA_JSON_URL, fiware_header=self.fiware_header + ) as client: client.get_device_list() device = Device(**self.device) - attr = DeviceAttribute(name='temperature', - object_id='t', - type='Number', - entity_name='test') - attr_command = DeviceCommand(name='open') - attr_lazy = LazyDeviceAttribute(name='pressure', - object_id='p', - type='Text', - entity_name='pressure') - attr_static = StaticDeviceAttribute(name='hasRoom', - type='Relationship', - value='my_partner_id') + attr = DeviceAttribute( + name="temperature", object_id="t", type="Number", entity_name="test" + ) + attr_command = DeviceCommand(name="open") + attr_lazy = LazyDeviceAttribute( + name="pressure", object_id="p", type="Text", entity_name="pressure" + ) + attr_static = StaticDeviceAttribute( + name="hasRoom", type="Relationship", value="my_partner_id" + ) device.add_attribute(attr) device.add_attribute(attr_command) device.add_attribute(attr_lazy) @@ -121,20 +128,19 @@ def test_device_endpoints(self): client.post_device(device=device) device_res = client.get_device(device_id=device.device_id) - self.assertEqual(device.model_dump(exclude={'service', - 'service_path', - 'timezone'}), - device_res.model_dump(exclude={'service', - 'service_path', - 'timezone'})) + self.assertEqual( + device.model_dump(exclude={"service", "service_path", "timezone"}), + device_res.model_dump(exclude={"service", "service_path", "timezone"}), + ) self.assertEqual(self.fiware_header.service, device_res.service) - self.assertEqual(self.fiware_header.service_path, - device_res.service_path) - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + self.assertEqual(self.fiware_header.service_path, device_res.service_path) + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_metadata(self): """ Test for metadata works but the api of iot agent-json seems not @@ -142,69 +148,79 @@ def test_metadata(self): Returns: None """ - metadata = {"accuracy": {"type": "Text", - "value": "+-5%"}} - attr = DeviceAttribute(name="temperature", - object_id="temperature", - type="Number", - metadata=metadata) + metadata = {"accuracy": {"type": "Text", "value": "+-5%"}} + attr = DeviceAttribute( + name="temperature", + object_id="temperature", + type="Number", + metadata=metadata, + ) device = Device(**self.device) device.device_id = "device_with_meta" device.add_attribute(attribute=attr) logger.info(device.model_dump_json(indent=2)) with IoTAClient( - url=settings.IOTA_JSON_URL, - fiware_header=self.fiware_header) as client: + url=settings.IOTA_JSON_URL, fiware_header=self.fiware_header + ) as client: client.post_device(device=device) - logger.info(client.get_device(device_id=device.device_id).model_dump_json( - indent=2, exclude_unset=True)) + logger.info( + client.get_device(device_id=device.device_id).model_dump_json( + indent=2, exclude_unset=True + ) + ) with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) as client: - logger.info(client.get_entity(entity_id=device.entity_name).model_dump_json( - indent=2)) - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + url=settings.CB_URL, fiware_header=self.fiware_header + ) as client: + logger.info( + client.get_entity(entity_id=device.entity_name).model_dump_json( + indent=2 + ) + ) + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_deletions(self): """ Test the deletion of a context entity/device if the state is always correctly cleared """ - device_id = 'device_id' - entity_id = 'entity_id' + device_id = "device_id" + entity_id = "entity_id" - device = Device(device_id=device_id, - entity_name=entity_id, - entity_type='Thing2', - protocol='IoTA-JSON', - transport='HTTP', - apikey='filip-iot-test-device') + device = Device( + device_id=device_id, + entity_name=entity_id, + entity_type="Thing2", + protocol="IoTA-JSON", + transport="HTTP", + apikey="filip-iot-test-device", + ) - cb_client = ContextBrokerClient(url=settings.CB_URL, - fiware_header=self.fiware_header) + cb_client = ContextBrokerClient( + url=settings.CB_URL, fiware_header=self.fiware_header + ) # Test 1: Only delete device # delete without optional parameter -> entity needs to continue existing self.client.post_device(device=device) self.client.delete_device(device_id=device_id, cb_url=settings.CB_URL) - self.assertTrue( - len(cb_client.get_entity_list(entity_ids=[entity_id])) == 1) - cb_client.delete_entity(entity_id=entity_id, entity_type='Thing2') + self.assertTrue(len(cb_client.get_entity_list(entity_ids=[entity_id])) == 1) + cb_client.delete_entity(entity_id=entity_id, entity_type="Thing2") # Test 2:Delete device and corresponding entity # delete with optional parameter -> entity needs to be deleted self.client.post_device(device=device) - self.client.delete_device(device_id=device_id, - cb_url=settings.CB_URL, - delete_entity=True) - self.assertTrue( - len(cb_client.get_entity_list(entity_ids=[entity_id])) == 0) + self.client.delete_device( + device_id=device_id, cb_url=settings.CB_URL, delete_entity=True + ) + self.assertTrue(len(cb_client.get_entity_list(entity_ids=[entity_id])) == 0) # Test 3:Delete device and corresponding entity, # that is linked to multiple devices @@ -215,17 +231,16 @@ def test_deletions(self): device2.device_id = "device_id2" self.client.post_device(device=device2) with self.assertRaises(Exception): - self.client.delete_device(device_id=device_id, - delete_entity=True, - cb_url=settings.CB_URL) - self.assertTrue( - len(cb_client.get_entity_list(entity_ids=[entity_id])) == 1) + self.client.delete_device( + device_id=device_id, delete_entity=True, cb_url=settings.CB_URL + ) + self.assertTrue(len(cb_client.get_entity_list(entity_ids=[entity_id])) == 1) self.client.delete_device(device_id=device2.device_id) # Test 4: Only delete entity # delete without optional parameter -> device needs to continue existing self.client.post_device(device=device) - cb_client.delete_entity(entity_id=entity_id, entity_type='Thing2') + cb_client.delete_entity(entity_id=entity_id, entity_type="Thing2") self.client.get_device(device_id=device_id) self.client.delete_device(device_id=device_id) @@ -235,9 +250,12 @@ def test_deletions(self): device2 = copy.deepcopy(device) device2.device_id = "device_id2" self.client.post_device(device=device2) - cb_client.delete_entity(entity_id=entity_id, delete_devices=True, - entity_type='Thing2', - iota_url=settings.IOTA_JSON_URL) + cb_client.delete_entity( + entity_id=entity_id, + delete_devices=True, + entity_type="Thing2", + iota_url=settings.IOTA_JSON_URL, + ) self.assertEqual(len(self.client.get_device_list()), 0) def test_update_device(self): @@ -249,19 +267,23 @@ def test_update_device(self): device.endpoint = "http://test.com" device.transport = "MQTT" - device.add_attribute(DeviceAttribute( - name="Att1", object_id="o1", type=DataType.STRUCTUREDVALUE)) - device.add_attribute(StaticDeviceAttribute( - name="Stat1", value="test", type=DataType.TEXT)) - device.add_attribute(StaticDeviceAttribute( - name="Stat2", value="test", type=DataType.TEXT)) + device.add_attribute( + DeviceAttribute(name="Att1", object_id="o1", type=DataType.STRUCTUREDVALUE) + ) + device.add_attribute( + StaticDeviceAttribute(name="Stat1", value="test", type=DataType.TEXT) + ) + device.add_attribute( + StaticDeviceAttribute(name="Stat2", value="test", type=DataType.TEXT) + ) device.add_command(DeviceCommand(name="Com1")) # use update_device to post self.client.update_device(device=device, add=True) - cb_client = ContextBrokerClient(url=settings.CB_URL, - fiware_header=self.fiware_header) + cb_client = ContextBrokerClient( + url=settings.CB_URL, fiware_header=self.fiware_header + ) # test if attributes exists correctly live_entity = cb_client.get_entity(entity_id=device.entity_name) @@ -276,10 +298,12 @@ def test_update_device(self): device.delete_attribute(device.get_attribute("Stat2")) device.delete_attribute(device.get_attribute("Att1")) device.delete_attribute(device.get_attribute("Com1")) - device.add_attribute(DeviceAttribute( - name="Att2", object_id="o1", type=DataType.STRUCTUREDVALUE)) - device.add_attribute(StaticDeviceAttribute( - name="Stat3", value="test3", type=DataType.TEXT)) + device.add_attribute( + DeviceAttribute(name="Att2", object_id="o1", type=DataType.STRUCTUREDVALUE) + ) + device.add_attribute( + StaticDeviceAttribute(name="Stat3", value="test3", type=DataType.TEXT) + ) device.add_command(DeviceCommand(name="Com2")) # device.endpoint = "http://localhost:8080" @@ -305,26 +329,30 @@ def test_update_device(self): def test_patch_device(self): """ - Test the methode: patch_device of the iota client + Test the methode: patch_device of the iota client """ device = Device(**self.device) device.endpoint = "http://test.com" device.transport = "MQTT" - device.add_attribute(DeviceAttribute( - name="Att1", object_id="o1", type=DataType.STRUCTUREDVALUE)) - device.add_attribute(StaticDeviceAttribute( - name="Stat1", value="test", type=DataType.TEXT)) - device.add_attribute(StaticDeviceAttribute( - name="Stat2", value="test", type=DataType.TEXT)) + device.add_attribute( + DeviceAttribute(name="Att1", object_id="o1", type=DataType.STRUCTUREDVALUE) + ) + device.add_attribute( + StaticDeviceAttribute(name="Stat1", value="test", type=DataType.TEXT) + ) + device.add_attribute( + StaticDeviceAttribute(name="Stat2", value="test", type=DataType.TEXT) + ) device.add_command(DeviceCommand(name="Com1")) # use patch_device to post self.client.patch_device(device=device) - cb_client = ContextBrokerClient(url=settings.CB_URL, - fiware_header=self.fiware_header) + cb_client = ContextBrokerClient( + url=settings.CB_URL, fiware_header=self.fiware_header + ) # test if attributes exists correctly live_entity = cb_client.get_entity(entity_id=device.entity_name) @@ -339,10 +367,12 @@ def test_patch_device(self): device.delete_attribute(device.get_attribute("Stat2")) device.delete_attribute(device.get_attribute("Att1")) device.delete_attribute(device.get_attribute("Com1")) - device.add_attribute(DeviceAttribute( - name="Att2", object_id="o1", type=DataType.STRUCTUREDVALUE)) - device.add_attribute(StaticDeviceAttribute( - name="Stat3", value="test3", type=DataType.TEXT)) + device.add_attribute( + DeviceAttribute(name="Att2", object_id="o1", type=DataType.STRUCTUREDVALUE) + ) + device.add_attribute( + StaticDeviceAttribute(name="Stat3", value="test3", type=DataType.TEXT) + ) device.add_command(DeviceCommand(name="Com2")) self.client.patch_device(device=device, cb_url=settings.CB_URL) @@ -362,22 +392,25 @@ def test_patch_device(self): live_entity.get_attribute("Att2") # test update where device information were changed - new_device_dict = {"endpoint": "http://localhost:7071/", - "device_id": "new_id", - "entity_name": "new_name", - "entity_type": "new_type", - "timestamp": False, - "apikey": "zuiop", - "protocol": "HTTP", - "transport": "HTTP"} + new_device_dict = { + "endpoint": "http://localhost:7071/", + "device_id": "new_id", + "entity_name": "new_name", + "entity_type": "new_type", + "timestamp": False, + "apikey": "zuiop", + "protocol": "HTTP", + "transport": "HTTP", + } new_device = Device(**new_device_dict) for key, value in new_device_dict.items(): device.__setattr__(key, value) self.client.patch_device(device=device) live_device = self.client.get_device(device_id=device.device_id) - self.assertEqual(live_device.__getattribute__(key), - new_device.__getattribute__(key)) + self.assertEqual( + live_device.__getattribute__(key), new_device.__getattribute__(key) + ) cb_client.close() def test_service_group(self): @@ -385,21 +418,36 @@ def test_service_group(self): Test of querying service group based on apikey and resource. """ # Create dummy service groups - group_base = ServiceGroup(service=settings.FIWARE_SERVICE, - subservice=settings.FIWARE_SERVICEPATH, - resource="/iot/json", apikey="base") - group1 = ServiceGroup(service=settings.FIWARE_SERVICE, - subservice=settings.FIWARE_SERVICEPATH, - resource="/iot/json", apikey="test1") - group2 = ServiceGroup(service=settings.FIWARE_SERVICE, - subservice=settings.FIWARE_SERVICEPATH, - resource="/iot/json", apikey="test2") + group_base = ServiceGroup( + service=settings.FIWARE_SERVICE, + subservice=settings.FIWARE_SERVICEPATH, + resource="/iot/json", + apikey="base", + ) + group1 = ServiceGroup( + service=settings.FIWARE_SERVICE, + subservice=settings.FIWARE_SERVICEPATH, + resource="/iot/json", + apikey="test1", + ) + group2 = ServiceGroup( + service=settings.FIWARE_SERVICE, + subservice=settings.FIWARE_SERVICEPATH, + resource="/iot/json", + apikey="test2", + ) self.client.post_groups([group_base, group1, group2], update=True) # get service group - self.assertEqual(group_base, self.client.get_group(resource="/iot/json", apikey="base")) - self.assertEqual(group1, self.client.get_group(resource="/iot/json", apikey="test1")) - self.assertEqual(group2, self.client.get_group(resource="/iot/json", apikey="test2")) + self.assertEqual( + group_base, self.client.get_group(resource="/iot/json", apikey="base") + ) + self.assertEqual( + group1, self.client.get_group(resource="/iot/json", apikey="test1") + ) + self.assertEqual( + group2, self.client.get_group(resource="/iot/json", apikey="test2") + ) with self.assertRaises(KeyError): self.client.get_group(resource="/iot/json", apikey="not_exist") @@ -410,34 +458,48 @@ def test_update_service_group(self): Test for updating service group """ attributes = [DeviceAttribute(name="temperature", type="Number")] - group_base = ServiceGroup(service=settings.FIWARE_SERVICE, subservice=settings.FIWARE_SERVICEPATH, - resource="/iot/json", apikey="base", - entity_type="Sensor", - attributes=attributes) + group_base = ServiceGroup( + service=settings.FIWARE_SERVICE, + subservice=settings.FIWARE_SERVICEPATH, + resource="/iot/json", + apikey="base", + entity_type="Sensor", + attributes=attributes, + ) self.client.post_group(service_group=group_base) - self.assertEqual(group_base, self.client.get_group(resource="/iot/json", apikey="base")) + self.assertEqual( + group_base, self.client.get_group(resource="/iot/json", apikey="base") + ) # # boolean attribute group_base.autoprovision = False self.client.update_group(service_group=group_base) - self.assertEqual(group_base, self.client.get_group(resource="/iot/json", apikey="base")) + self.assertEqual( + group_base, self.client.get_group(resource="/iot/json", apikey="base") + ) # entity type group_base.entity_type = "TemperatureSensor" self.client.update_group(service_group=group_base) - self.assertEqual(group_base, self.client.get_group(resource="/iot/json", apikey="base")) + self.assertEqual( + group_base, self.client.get_group(resource="/iot/json", apikey="base") + ) # attributes humidity = DeviceAttribute(name="humidity", type="Number") group_base.attributes.append(humidity) self.client.update_group(service_group=group_base) - self.assertEqual(group_base, self.client.get_group(resource="/iot/json", apikey="base")) - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - iota_url=settings.IOTA_JSON_URL, - cb_url=settings.CB_URL) + self.assertEqual( + group_base, self.client.get_group(resource="/iot/json", apikey="base") + ) + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + iota_url=settings.IOTA_JSON_URL, + cb_url=settings.CB_URL, + ) def test_clear_iot_agent(self): """ Test for clearing iot agent AFTER clearing context broker @@ -446,13 +508,13 @@ def test_clear_iot_agent(self): Returns: None """ - cb_client = ContextBrokerClient(url=settings.CB_URL, - fiware_header=self.fiware_header) + cb_client = ContextBrokerClient( + url=settings.CB_URL, fiware_header=self.fiware_header + ) device = Device(**self.device) device.add_command(DeviceCommand(name="dummy_cmd")) self.client.post_device(device=device) - clear_context_broker(settings.CB_URL, - self.fiware_header) + clear_context_broker(settings.CB_URL, self.fiware_header) self.assertEqual(len(cb_client.get_registration_list()), 1) clear_iot_agent(settings.IOTA_JSON_URL, self.fiware_header) @@ -464,7 +526,8 @@ def tearDown(self) -> None: """ self.client.close() - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) - + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) diff --git a/tests/clients/test_ngsi_v2_timeseries.py b/tests/clients/test_ngsi_v2_timeseries.py index d5243431..1d980f70 100644 --- a/tests/clients/test_ngsi_v2_timeseries.py +++ b/tests/clients/test_ngsi_v2_timeseries.py @@ -1,15 +1,14 @@ """ Tests for time series api client aka QuantumLeap """ + import logging import unittest from random import random import requests import time from typing import List -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ContextBrokerClient, QuantumLeapClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.context import ContextEntity from filip.models.ngsi_v2.subscriptions import Message @@ -27,16 +26,18 @@ def create_entities() -> List[ContextEntity]: Returns: """ + def create_attr(): - return {'temperature': {'value': random(), - 'type': 'Number'}, - 'humidity': {'value': random(), - 'type': 'Number'}, - 'co2': {'value': random(), - 'type': 'Number'}} + return { + "temperature": {"value": random(), "type": "Number"}, + "humidity": {"value": random(), "type": "Number"}, + "co2": {"value": random(), "type": "Number"}, + } - return [ContextEntity(id='Kitchen', type='Room', **create_attr()), - ContextEntity(id='LivingRoom', type='Room', **create_attr())] + return [ + ContextEntity(id="Kitchen", type="Room", **create_attr()), + ContextEntity(id="LivingRoom", type="Room", **create_attr()), + ] def create_time_series_data(num_records: int = 50000): @@ -44,15 +45,16 @@ def create_time_series_data(num_records: int = 50000): creates large testing data sets that should remain on the server. This is mainly to reduce time for testings """ - fiware_header = FiwareHeader(service=settings.FIWARE_SERVICE, - service_path="/static") + fiware_header = FiwareHeader( + service=settings.FIWARE_SERVICE, service_path="/static" + ) - with QuantumLeapClient(url=settings.QL_URL, fiware_header=fiware_header) \ - as client: + with QuantumLeapClient(url=settings.QL_URL, fiware_header=fiware_header) as client: for i in range(num_records): - notification_message = Message(data=create_entities(), - subscriptionId="test") + notification_message = Message( + data=create_entities(), subscriptionId="test" + ) client.post_notification(notification_message) @@ -68,14 +70,14 @@ def setUp(self) -> None: None """ self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) self.ql_client = QuantumLeapClient( - url=settings.QL_URL, - fiware_header=self.fiware_header) + url=settings.QL_URL, fiware_header=self.fiware_header + ) self.cb_client = ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) + url=settings.CB_URL, fiware_header=self.fiware_header + ) def test_meta_endpoints(self) -> None: """ @@ -84,16 +86,17 @@ def test_meta_endpoints(self) -> None: None """ with QuantumLeapClient( - url=settings.QL_URL, - fiware_header=self.fiware_header) \ - as client: + url=settings.QL_URL, fiware_header=self.fiware_header + ) as client: self.assertIsNotNone(client.get_version()) self.assertIsNotNone(client.get_health()) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - ql_url=settings.QL_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ql_url=settings.QL_URL, + ) def test_insert_data(self) -> None: """ Test insert data directly into QuantumLeap @@ -104,17 +107,19 @@ def test_insert_data(self) -> None: for entity in entities: self.cb_client.post_entity(entity) - with QuantumLeapClient(url=settings.QL_URL, - fiware_header=self.fiware_header) as client: - notification_message = Message(data=entities, - subscriptionId="test") + with QuantumLeapClient( + url=settings.QL_URL, fiware_header=self.fiware_header + ) as client: + notification_message = Message(data=entities, subscriptionId="test") client.post_notification(notification_message) time.sleep(1) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - ql_url=settings.QL_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ql_url=settings.QL_URL, + ) def test_entity_context(self) -> None: """ Test entities endpoint @@ -123,11 +128,9 @@ def test_entity_context(self) -> None: """ entities = create_entities() with QuantumLeapClient( - url=settings.QL_URL, - fiware_header=self.fiware_header) \ - as client: - notification_message = Message(data=entities, - subscriptionId="test") + url=settings.QL_URL, fiware_header=self.fiware_header + ) as client: + notification_message = Message(data=entities, subscriptionId="test") client.post_notification(notification_message) time.sleep(1) @@ -143,37 +146,40 @@ def test_query_endpoints_by_id(self) -> None: None """ with QuantumLeapClient( - url=settings.QL_URL, - fiware_header=self.fiware_header.model_copy( - update={'service_path': '/static'})) \ - as client: + url=settings.QL_URL, + fiware_header=self.fiware_header.model_copy( + update={"service_path": "/static"} + ), + ) as client: entities = create_entities() with self.assertRaises(requests.RequestException): - client.get_entity_by_id(entity_id=entities[0].id, - entity_type='MyType') + client.get_entity_by_id(entity_id=entities[0].id, entity_type="MyType") for entity in entities: # get by id - attrs_id = client.get_entity_by_id(entity_id=entity.id, - aggr_period='minute', - aggr_method='avg', - attrs='temperature,co2') + attrs_id = client.get_entity_by_id( + entity_id=entity.id, + aggr_period="minute", + aggr_method="avg", + attrs="temperature,co2", + ) logger.debug(attrs_id.model_dump_json(indent=2)) logger.debug(attrs_id.to_pandas()) - attrs_values_id = client.get_entity_values_by_id( - entity_id=entity.id) + attrs_values_id = client.get_entity_values_by_id(entity_id=entity.id) logger.debug(attrs_values_id.to_pandas()) self.assertEqual(len(attrs_values_id.index), 10000) attr_id = client.get_entity_attr_by_id( - entity_id=entity.id, attr_name="temperature") + entity_id=entity.id, attr_name="temperature" + ) logger.debug(attr_id.to_pandas()) self.assertEqual(len(attr_id.index), 10000) attr_values_id = client.get_entity_attr_values_by_id( - entity_id=entity.id, attr_name="temperature") + entity_id=entity.id, attr_name="temperature" + ) logger.debug(attr_values_id.to_pandas()) self.assertEqual(len(attrs_values_id.index), 10000) @@ -185,10 +191,11 @@ def test_query_endpoints_by_type(self) -> None: None """ with QuantumLeapClient( - url=settings.QL_URL, - fiware_header=self.fiware_header.model_copy( - update={'service_path': '/static'})) \ - as client: + url=settings.QL_URL, + fiware_header=self.fiware_header.model_copy( + update={"service_path": "/static"} + ), + ) as client: entities = create_entities() @@ -202,9 +209,9 @@ def test_query_endpoints_by_type(self) -> None: logger.debug(entity_id.to_pandas()) # the limit 10000 will be shared by the two entities - self.assertEqual(sum([len(entity_id.index) for - entity_id in attrs_type]), - 10000) + self.assertEqual( + sum([len(entity_id.index) for entity_id in attrs_type]), 10000 + ) attrs_values_type = client.get_entity_values_by_type( entity_type=entity.type, @@ -212,30 +219,33 @@ def test_query_endpoints_by_type(self) -> None: ) for entity_id in attrs_values_type: logger.debug(entity_id.to_pandas()) - self.assertEqual(sum([len(entity_id.index) for - entity_id in attrs_values_type]), - 10000) + self.assertEqual( + sum([len(entity_id.index) for entity_id in attrs_values_type]), + 10000, + ) attr_type = client.get_entity_attr_by_type( - entity_type=entity.type, attr_name="temperature", + entity_type=entity.type, + attr_name="temperature", limit=10000, ) for entity_id in attr_type: logger.debug(entity_id.to_pandas()) - self.assertEqual(sum([len(entity_id.index) for - entity_id in attr_type]), - 10000) + self.assertEqual( + sum([len(entity_id.index) for entity_id in attr_type]), 10000 + ) attr_values_type = client.get_entity_attr_values_by_type( - entity_type=entity.type, attr_name="temperature", + entity_type=entity.type, + attr_name="temperature", limit=10000, ) for entity_id in attr_values_type: logger.debug(entity_id.to_pandas()) - self.assertEqual(sum([len(entity_id.index) for - entity_id in attr_values_type]), - 10000) - + self.assertEqual( + sum([len(entity_id.index) for entity_id in attr_values_type]), 10000 + ) + def test_query_endpoints_by_id_pattern(self) -> None: """ Test queries by id_pattern with default values @@ -243,85 +253,102 @@ def test_query_endpoints_by_id_pattern(self) -> None: Returns: None """ - + with QuantumLeapClient( - url=settings.QL_URL, - fiware_header=self.fiware_header.model_copy( - update={'service_path': '/static'})) \ - as client: + url=settings.QL_URL, + fiware_header=self.fiware_header.model_copy( + update={"service_path": "/static"} + ), + ) as client: # check version, skip if version < 1.0.0 - if version.parse(client.get_version().get("version")) < version.parse("1.0.0"): + if version.parse(client.get_version().get("version")) < version.parse( + "1.0.0" + ): logger.info("Skip test of id_pattern for QL < 1.0.0") return None entity = create_entities()[0] # 'expression': expected results - re_patterns = {'.*[^mn]$': 0, # not end with "m" or "n" -> no entities will match - '.{1,3}i': 2, # has at least one "i" - '.*[R]': 1, # has at least one "R" - '.{5}[^g]': 1 # the sixth letter is not "g" - } - + re_patterns = { + ".*[^mn]$": 0, # not end with "m" or "n" -> no entities will match + ".{1,3}i": 2, # has at least one "i" + ".*[R]": 1, # has at least one "R" + ".{5}[^g]": 1, # the sixth letter is not "g" + } + for expression, expected_result in re_patterns.items(): if expected_result == 0: - self.assertRaises(requests.exceptions.HTTPError, client.get_entities, - id_pattern=expression) - self.assertRaises(requests.exceptions.HTTPError, - client.get_entity_by_type, - entity_type=entity.type, - id_pattern=expression) - self.assertRaises(requests.exceptions.HTTPError, - client.get_entity_values_by_type, - entity_type=entity.type, - id_pattern=expression) - self.assertRaises(requests.exceptions.HTTPError, - client.get_entity_attr_by_type, - entity_type=entity.type, - attr_name="temperature", - id_pattern=expression) - self.assertRaises(requests.exceptions.HTTPError, - client.get_entity_attr_values_by_type, - entity_type=entity.type, - attr_name="co2", - id_pattern=expression) + self.assertRaises( + requests.exceptions.HTTPError, + client.get_entities, + id_pattern=expression, + ) + self.assertRaises( + requests.exceptions.HTTPError, + client.get_entity_by_type, + entity_type=entity.type, + id_pattern=expression, + ) + self.assertRaises( + requests.exceptions.HTTPError, + client.get_entity_values_by_type, + entity_type=entity.type, + id_pattern=expression, + ) + self.assertRaises( + requests.exceptions.HTTPError, + client.get_entity_attr_by_type, + entity_type=entity.type, + attr_name="temperature", + id_pattern=expression, + ) + self.assertRaises( + requests.exceptions.HTTPError, + client.get_entity_attr_values_by_type, + entity_type=entity.type, + attr_name="co2", + id_pattern=expression, + ) continue entities = client.get_entities(id_pattern=expression) self.assertEqual(len(entities), expected_result) attrs_type = client.get_entity_by_type( - entity_type=entity.type, - id_pattern=expression, - limit=10000) + entity_type=entity.type, id_pattern=expression, limit=10000 + ) for entity_id in attrs_type: logger.debug(entity_id.to_pandas()) self.assertEqual(len(attrs_type), expected_result) attrs_values_type = client.get_entity_values_by_type( - entity_type=entity.type, - id_pattern=expression, - limit=10000) + entity_type=entity.type, id_pattern=expression, limit=10000 + ) for entity_id in attrs_values_type: logger.debug(entity_id.to_pandas()) self.assertEqual(len(attrs_values_type), expected_result) - + attr_type = client.get_entity_attr_by_type( - entity_type=entity.type, attr_name="temperature", + entity_type=entity.type, + attr_name="temperature", id_pattern=expression, - limit=10000) + limit=10000, + ) for entity_id in attr_type: logger.debug(entity_id.to_pandas()) self.assertEqual(len(attr_type), expected_result) attr_values_type = client.get_entity_attr_values_by_type( - entity_type=entity.type, attr_name="temperature", + entity_type=entity.type, + attr_name="temperature", id_pattern=expression, - limit=10000) + limit=10000, + ) for entity_id in attr_values_type: logger.debug(entity_id.to_pandas()) self.assertEqual(len(attr_values_type), expected_result) - + @unittest.skip("Currently fails. Because data in CrateDB is not clean") def test_test_query_endpoints_with_args(self) -> None: """ @@ -331,18 +358,18 @@ def test_test_query_endpoints_with_args(self) -> None: None """ with QuantumLeapClient( - url=settings.QL_URL, - fiware_header=self.fiware_header.model_copy( - update={'service_path': '/static'})) \ - as client: + url=settings.QL_URL, + fiware_header=self.fiware_header.model_copy( + update={"service_path": "/static"} + ), + ) as client: for entity in create_entities(): # test limit for limit in range(5000, 25000, 5000): records = client.get_entity_by_id( - entity_id=entity.id, - attrs='temperature,co2', - limit=limit) + entity_id=entity.id, attrs="temperature,co2", limit=limit + ) logger.debug(records.model_dump_json(indent=2)) logger.debug(records.to_pandas()) @@ -353,26 +380,23 @@ def test_test_query_endpoints_with_args(self) -> None: limit = 15000 last_n_records = client.get_entity_by_id( entity_id=entity.id, - attrs='temperature,co2', + attrs="temperature,co2", limit=limit, - last_n=last_n) - self.assertGreater(last_n_records.index[0], - records.index[0]) - self.assertEqual(len(last_n_records.index), - min(last_n, limit)) + last_n=last_n, + ) + self.assertGreater(last_n_records.index[0], records.index[0]) + self.assertEqual(len(last_n_records.index), min(last_n, limit)) # test offset old_records = None for offset in range(5000, 25000, 5000): # with limit records = client.get_entity_by_id( - entity_id=entity.id, - attrs='temperature,co2', - offset=offset) + entity_id=entity.id, attrs="temperature,co2", offset=offset + ) if old_records: - self.assertLess(old_records.index[0], - records.index[0]) + self.assertLess(old_records.index[0], records.index[0]) old_records = records old_records = None @@ -380,14 +404,14 @@ def test_test_query_endpoints_with_args(self) -> None: # test with last_n records = client.get_entity_by_id( entity_id=entity.id, - attrs='temperature,co2', + attrs="temperature,co2", offset=offset, - last_n=5) + last_n=5, + ) if old_records: - self.assertGreater(old_records.index[0], - records.index[0]) + self.assertGreater(old_records.index[0], records.index[0]) old_records = records - + def test_attr_endpoints(self) -> None: """ Test get entity by attr/attr name endpoints @@ -395,16 +419,16 @@ def test_attr_endpoints(self) -> None: None """ with QuantumLeapClient( - url=settings.QL_URL, - fiware_header=FiwareHeader(service='filip', - service_path="/static")) \ - as client: - attr_names = ['temperature', 'humidity', 'co2'] + url=settings.QL_URL, + fiware_header=FiwareHeader(service="filip", service_path="/static"), + ) as client: + attr_names = ["temperature", "humidity", "co2"] for attr_name in attr_names: entities_by_attr_name = client.get_entity_by_attr_name( - attr_name=attr_name) + attr_name=attr_name + ) # we expect as many timeseries as there are unique ids - self.assertEqual(len(entities_by_attr_name), 2) + self.assertEqual(len(entities_by_attr_name), 2) # we expect the sizes of the index and attribute values to be the same for timeseries in entities_by_attr_name: @@ -413,7 +437,7 @@ def test_attr_endpoints(self) -> None: entities_by_attr = client.get_entity_by_attrs() # we expect as many timeseries as : n of unique ids x n of different attrs - self.assertEqual(len(entities_by_attr), 2*3) + self.assertEqual(len(entities_by_attr), 2 * 3) for timeseries in entities_by_attr: for attribute in timeseries.attributes: self.assertEqual(len(attribute.values), len(timeseries.index)) @@ -424,8 +448,10 @@ def tearDown(self) -> None: Returns: None """ - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - ql_url=settings.QL_URL) + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + ql_url=settings.QL_URL, + ) self.ql_client.close() self.cb_client.close() diff --git a/tests/config.py b/tests/config.py index 5de6f12e..7871a73d 100644 --- a/tests/config.py +++ b/tests/config.py @@ -22,70 +22,81 @@ class TestSettings(BaseSettings): Settings for the test case scenarios according to pydantic's documentaion https://pydantic-docs.helpmanual.io/usage/settings/ """ - LOG_LEVEL: LogLevel = Field(default=LogLevel.ERROR, - validation_alias=AliasChoices('LOG_LEVEL', 'LOGLEVEL')) - - CB_URL: AnyHttpUrl = Field(default="http://localhost:1026", - validation_alias=AliasChoices('ORION_URL', - 'CB_URL', - 'CB_HOST', - 'CONTEXTBROKER_URL', - 'OCB_URL')) - LD_CB_URL: AnyHttpUrl = Field(default="http://localhost:1026", - validation_alias=AliasChoices('LD_ORION_URL', - 'LD_CB_URL', - 'ORION_LD_URL', - 'SCORPIO_URL', - 'STELLIO_URL')) - IOTA_URL: AnyHttpUrl = Field(default="http://localhost:4041", - validation_alias='IOTA_URL') - IOTA_JSON_URL: AnyHttpUrl = Field(default="http://localhost:4041", - validation_alias='IOTA_JSON_URL') - - IOTA_UL_URL: AnyHttpUrl = Field(default="http://127.0.0.1:4061", - validation_alias=AliasChoices('IOTA_UL_URL')) - - QL_URL: AnyHttpUrl = Field(default="http://127.0.0.1:8668", - validation_alias=AliasChoices('QUANTUMLEAP_URL', - 'QL_URL')) - - MQTT_BROKER_URL: AnyUrl = Field(default="mqtt://127.0.0.1:1883", - validation_alias=AliasChoices( - 'MQTT_BROKER_URL', - 'MQTT_URL', - 'MQTT_BROKER')) - - MQTT_BROKER_URL_INTERNAL: AnyUrl = Field(default="mqtt://mosquitto:1883", - validation_alias=AliasChoices( - 'MQTT_BROKER_URL_INTERNAL', - 'MQTT_URL_INTERNAL')) - - LD_MQTT_BROKER_URL: AnyUrl = Field(default="mqtt://127.0.0.1:1884", - validation_alias=AliasChoices( - 'LD_MQTT_BROKER_URL', - 'LD_MQTT_URL', - 'LD_MQTT_BROKER')) - - LD_MQTT_BROKER_URL_INTERNAL: AnyUrl = Field(default="mqtt://mqtt-broker-ld:1883", - validation_alias=AliasChoices( - 'LD_MQTT_BROKER_URL_INTERNAL', - 'LD_MQTT_URL_INTERNAL')) + + LOG_LEVEL: LogLevel = Field( + default=LogLevel.ERROR, validation_alias=AliasChoices("LOG_LEVEL", "LOGLEVEL") + ) + + CB_URL: AnyHttpUrl = Field( + default="http://localhost:1026", + validation_alias=AliasChoices( + "ORION_URL", "CB_URL", "CB_HOST", "CONTEXTBROKER_URL", "OCB_URL" + ), + ) + LD_CB_URL: AnyHttpUrl = Field( + default="http://localhost:1026", + validation_alias=AliasChoices( + "LD_ORION_URL", "LD_CB_URL", "ORION_LD_URL", "SCORPIO_URL", "STELLIO_URL" + ), + ) + IOTA_URL: AnyHttpUrl = Field( + default="http://localhost:4041", validation_alias="IOTA_URL" + ) + IOTA_JSON_URL: AnyHttpUrl = Field( + default="http://localhost:4041", validation_alias="IOTA_JSON_URL" + ) + + IOTA_UL_URL: AnyHttpUrl = Field( + default="http://127.0.0.1:4061", validation_alias=AliasChoices("IOTA_UL_URL") + ) + + QL_URL: AnyHttpUrl = Field( + default="http://127.0.0.1:8668", + validation_alias=AliasChoices("QUANTUMLEAP_URL", "QL_URL"), + ) + + MQTT_BROKER_URL: AnyUrl = Field( + default="mqtt://127.0.0.1:1883", + validation_alias=AliasChoices("MQTT_BROKER_URL", "MQTT_URL", "MQTT_BROKER"), + ) + + MQTT_BROKER_URL_INTERNAL: AnyUrl = Field( + default="mqtt://mosquitto:1883", + validation_alias=AliasChoices("MQTT_BROKER_URL_INTERNAL", "MQTT_URL_INTERNAL"), + ) + + LD_MQTT_BROKER_URL: AnyUrl = Field( + default="mqtt://127.0.0.1:1884", + validation_alias=AliasChoices( + "LD_MQTT_BROKER_URL", "LD_MQTT_URL", "LD_MQTT_BROKER" + ), + ) + + LD_MQTT_BROKER_URL_INTERNAL: AnyUrl = Field( + default="mqtt://mqtt-broker-ld:1883", + validation_alias=AliasChoices( + "LD_MQTT_BROKER_URL_INTERNAL", "LD_MQTT_URL_INTERNAL" + ), + ) # IF CI_JOB_ID is present it will always overwrite the service path - CI_JOB_ID: Optional[str] = Field(default=None, - validation_alias=AliasChoices('CI_JOB_ID')) + CI_JOB_ID: Optional[str] = Field( + default=None, validation_alias=AliasChoices("CI_JOB_ID") + ) # create service paths for multi tenancy scenario and concurrent testing - FIWARE_SERVICE: str = Field(default='filip', - validation_alias=AliasChoices('FIWARE_SERVICE')) - - FIWARE_SERVICEPATH: str = Field(default_factory=generate_servicepath, - validation_alias=AliasChoices('FIWARE_PATH', - 'FIWARE_SERVICEPATH', - 'FIWARE_SERVICE_PATH')) - - - @model_validator(mode='after') + FIWARE_SERVICE: str = Field( + default="filip", validation_alias=AliasChoices("FIWARE_SERVICE") + ) + + FIWARE_SERVICEPATH: str = Field( + default_factory=generate_servicepath, + validation_alias=AliasChoices( + "FIWARE_PATH", "FIWARE_SERVICEPATH", "FIWARE_SERVICE_PATH" + ), + ) + + @model_validator(mode="after") def generate_multi_tenancy_setup(cls, values): """ Tests if the fields for multi tenancy in fiware are consistent. @@ -96,26 +107,32 @@ def generate_multi_tenancy_setup(cls, values): Returns: """ - if values.model_dump().get('CI_JOB_ID', None): + if values.model_dump().get("CI_JOB_ID", None): values.FIWARE_SERVICEPATH = f"/{values.CI_JOB_ID}" # validate header - FiwareHeader(service=values.FIWARE_SERVICE, - service_path=values.FIWARE_SERVICEPATH) + FiwareHeader( + service=values.FIWARE_SERVICE, service_path=values.FIWARE_SERVICEPATH + ) return values - model_config = SettingsConfigDict(env_file=find_dotenv('.env'), - env_file_encoding='utf-8', - case_sensitive=False, - use_enum_values=True) + + model_config = SettingsConfigDict( + env_file=find_dotenv(".env"), + env_file_encoding="utf-8", + case_sensitive=False, + use_enum_values=True, + ) # create settings object settings = TestSettings() -print(f"Running tests with the following settings: \n " - f"{settings.model_dump_json(indent=2)}") +print( + f"Running tests with the following settings: \n " + f"{settings.model_dump_json(indent=2)}" +) # configure logging for all tests logging.basicConfig( - level=settings.LOG_LEVEL, - format='%(asctime)s %(name)s %(levelname)s: %(message)s') + level=settings.LOG_LEVEL, format="%(asctime)s %(name)s %(levelname)s: %(message)s" +) diff --git a/tests/models/test_base.py b/tests/models/test_base.py index 2933664a..6619d246 100644 --- a/tests/models/test_base.py +++ b/tests/models/test_base.py @@ -1,6 +1,7 @@ """ Tests for filip.core.models """ + import copy import json import unittest @@ -22,65 +23,67 @@ class TestModels(unittest.TestCase): def setUp(self) -> None: # create variables for test self.service_paths = [generate_servicepath(), generate_servicepath()] - self.fiware_header = {'fiware-service': settings.FIWARE_SERVICE, - 'fiware-servicepath': settings.FIWARE_SERVICEPATH} - - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + self.fiware_header = { + "fiware-service": settings.FIWARE_SERVICE, + "fiware-servicepath": settings.FIWARE_SERVICEPATH, + } + + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_fiware_header(self): """ Test for fiware header """ header = FiwareHeader.model_validate(self.fiware_header) - self.assertEqual(header.model_dump(by_alias=True), - self.fiware_header) - self.assertEqual(json.loads(header.model_dump_json(by_alias=True)), - self.fiware_header) + self.assertEqual(header.model_dump(by_alias=True), self.fiware_header) + self.assertEqual( + json.loads(header.model_dump_json(by_alias=True)), self.fiware_header + ) with self.assertRaises(ValidationError): - FiwareHeader(service='jkgsadh ', service_path='/testing') + FiwareHeader(service="jkgsadh ", service_path="/testing") with self.assertRaises(ValidationError): - FiwareHeader(service='%', service_path='/testing') + FiwareHeader(service="%", service_path="/testing") with self.assertRaises(ValidationError): - FiwareHeader(service='filip', service_path='testing/') + FiwareHeader(service="filip", service_path="testing/") with self.assertRaises(ValidationError): - FiwareHeader(service='filip', service_path='/$testing') + FiwareHeader(service="filip", service_path="/$testing") with self.assertRaises(ValidationError): - FiwareHeader(service='filip', service_path='/testing ') + FiwareHeader(service="filip", service_path="/testing ") with self.assertRaises(ValidationError): - FiwareHeader(service='filip', service_path='#') + FiwareHeader(service="filip", service_path="#") headers = FiwareHeader.model_validate(self.fiware_header) - with ContextBrokerClient(url=settings.CB_URL, - fiware_header=headers) as client: - entity = ContextEntity(id='myId', type='MyType') + with ContextBrokerClient(url=settings.CB_URL, fiware_header=headers) as client: + entity = ContextEntity(id="myId", type="MyType") for path in self.service_paths: client.fiware_service_path = path client.post_entity(entity=entity) client.get_entity(entity_id=entity.id) - client.fiware_service_path = '/#' - self.assertGreaterEqual(len(client.get_entity_list()), - len(self.service_paths)) + client.fiware_service_path = "/#" + self.assertGreaterEqual( + len(client.get_entity_list()), len(self.service_paths) + ) for path in self.service_paths: client.fiware_service_path = path - client.delete_entity(entity_id=entity.id, - entity_type=entity.type) + client.delete_entity(entity_id=entity.id, entity_type=entity.type) def test_unit_as_metadata(self) -> None: entity_dict = { - "id": 'MyId', - "type": 'MyType', - 'at1': {'value': "20.0", 'type': 'Text'}, - 'at2': {'value': 20.0, - 'type': 'Number', - 'metadata': { - 'name': 'unit', - 'type': 'Unit', - "value": { - "name": "degree Celsius" - } - } - } - } + "id": "MyId", + "type": "MyType", + "at1": {"value": "20.0", "type": "Text"}, + "at2": { + "value": 20.0, + "type": "Number", + "metadata": { + "name": "unit", + "type": "Unit", + "value": {"name": "degree Celsius"}, + }, + }, + } entity = ContextEntity(**entity_dict) def test_strings_in_models(self) -> None: @@ -94,21 +97,23 @@ def test_strings_in_models(self) -> None: """ entity_dict = { - "id": 'MyId', - "type": 'MyType', - 'at1': {'value': "20.0", 'type': 'Text'}, - 'at2': {'value': {'field_value': "20.0"}, - 'type': 'StructuredValue', - 'metadata':{ - 'name': 'test-name', - 'type': 'StructuredValue', - 'value': {'field_value': "20.0"}, - } - } - } - - def field_value_tests(dictionary: dict, keychain: List[str], - test_keys: bool = False): + "id": "MyId", + "type": "MyType", + "at1": {"value": "20.0", "type": "Text"}, + "at2": { + "value": {"field_value": "20.0"}, + "type": "StructuredValue", + "metadata": { + "name": "test-name", + "type": "StructuredValue", + "value": {"field_value": "20.0"}, + }, + }, + } + + def field_value_tests( + dictionary: dict, keychain: List[str], test_keys: bool = False + ): """Recursively test the keys, values of a dictionary that will be transformed into an entity. @@ -129,9 +134,11 @@ def field_value_tests(dictionary: dict, keychain: List[str], field_value_tests(value, keychain_, field == "value") else: # we have a key value pair both strings - for test_char, needs_to_succeed in [("'", False), - ('"', False), - ("", True)]: + for test_char, needs_to_succeed in [ + ("'", False), + ('"', False), + ("", True), + ]: # Append ', " or nothing. The last is not allowed to # fail @@ -150,10 +157,12 @@ def test(dictionary: Dict): client.post_entity(entity=entity) # if post successful get will not throw an # error - client.get_entity(entity_id=entity.id, - entity_type=entity.type) - client.delete_entity(entity_id=entity.id, - entity_type=entity.type) + client.get_entity( + entity_id=entity.id, entity_type=entity.type + ) + client.delete_entity( + entity_id=entity.id, entity_type=entity.type + ) # work on a copy new_dict = copy.deepcopy(entity_dict) @@ -165,19 +174,18 @@ def test(dictionary: Dict): # apply the modification and test value_ = dict_field[field] - dict_field[field] = f'{value}{test_char}' + dict_field[field] = f"{value}{test_char}" test(new_dict) # if keys should be tested, apply the modification to # key and test if test_keys: del dict_field[field] - dict_field[f'{field}{test_char}'] = value_ + dict_field[f"{field}{test_char}"] = value_ test(new_dict) header = FiwareHeader.model_validate(self.fiware_header) - with ContextBrokerClient(url=settings.CB_URL, - fiware_header=header) as client: + with ContextBrokerClient(url=settings.CB_URL, fiware_header=header) as client: field_value_tests(entity_dict, []) @@ -188,7 +196,7 @@ def tearDown(self) -> None: for path in self.service_paths: header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=path) + service=settings.FIWARE_SERVICE, service_path=path + ) clear_all(fiware_header=header, cb_url=settings.CB_URL) client.close() diff --git a/tests/models/test_ngsi_ld_context.py b/tests/models/test_ngsi_ld_context.py index be95f6a7..7848fae6 100644 --- a/tests/models/test_ngsi_ld_context.py +++ b/tests/models/test_ngsi_ld_context.py @@ -1,21 +1,28 @@ """ Test module for context broker models """ + import unittest from geojson_pydantic import Point, MultiPoint, LineString, Polygon, GeometryCollection from pydantic import ValidationError -from filip.models.ngsi_ld.context import \ - ContextLDEntity, ContextProperty, NamedContextProperty, \ - ContextGeoPropertyValue, ContextGeoProperty, NamedContextGeoProperty, \ - NamedContextRelationship +from filip.models.ngsi_ld.context import ( + ContextLDEntity, + ContextProperty, + NamedContextProperty, + ContextGeoPropertyValue, + ContextGeoProperty, + NamedContextGeoProperty, + NamedContextRelationship, +) class TestLDContextModels(unittest.TestCase): """ Test class for context broker models """ + def setUp(self) -> None: """ Setup test data @@ -25,73 +32,52 @@ def setUp(self) -> None: self.entity1_dict = { "id": "urn:ngsi-ld:OffStreetParking:Downtown1", "type": "OffStreetParking", - "name": { - "type": "Property", - "value": "Downtown One" - }, + "name": {"type": "Property", "value": "Downtown One"}, "availableSpotNumber": { "type": "Property", "value": 121, "observedAt": "2017-07-29T12:05:02Z", - "reliability": { - "type": "Property", - "value": 0.7 - }, + "reliability": {"type": "Property", "value": 0.7}, "providedBy": { "type": "Relationship", - "object": "urn:ngsi-ld:Camera:C1" - } - }, - "totalSpotNumber": { - "type": "Property", - "value": 200 + "object": "urn:ngsi-ld:Camera:C1", + }, }, + "totalSpotNumber": {"type": "Property", "value": 200}, "location": { "type": "GeoProperty", "value": { "type": "Point", - "coordinates": (-8.5, 41.2) # coordinates are normally a tuple - } + "coordinates": (-8.5, 41.2), # coordinates are normally a tuple + }, }, "@context": [ "http://example.org/ngsi-ld/latest/parking.jsonld", - "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld" - ] + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld", + ], } self.entity1_props_dict = { "location": { "type": "GeoProperty", - "value": { - "type": "Point", - "coordinates": (-8.5, 41.2) - } - }, - "totalSpotNumber": { - "type": "Property", - "value": 200 + "value": {"type": "Point", "coordinates": (-8.5, 41.2)}, }, + "totalSpotNumber": {"type": "Property", "value": 200}, "availableSpotNumber": { "type": "Property", "value": 121, "observedAt": "2017-07-29T12:05:02Z", - "reliability": { - "type": "Property", - "value": 0.7 - }, + "reliability": {"type": "Property", "value": 0.7}, "providedBy": { "type": "Relationship", - "object": "urn:ngsi-ld:Camera:C1" - } - }, - "name": { - "type": "Property", - "value": "Downtown One" + "object": "urn:ngsi-ld:Camera:C1", + }, }, + "name": {"type": "Property", "value": "Downtown One"}, } self.entity1_context = [ - "http://example.org/ngsi-ld/latest/parking.jsonld", - "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld" - ] + "http://example.org/ngsi-ld/latest/parking.jsonld", + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld", + ] self.entity2_dict = { "id": "urn:ngsi-ld:Vehicle:A4567", "type": "Vehicle", @@ -99,14 +85,11 @@ def setUp(self) -> None: "http://example.org/ngsi-ld/latest/commonTerms.jsonld", "http://example.org/ngsi-ld/latest/vehicle.jsonld", "http://example.org/ngsi-ld/latest/parking.jsonld", - "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld" - ] + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld", + ], } self.entity2_props_dict = { - "brandName": { - "type": "Property", - "value": "Mercedes" - } + "brandName": {"type": "Property", "value": "Mercedes"} } self.entity2_rel_dict = { "isParked": { @@ -115,8 +98,8 @@ def setUp(self) -> None: "observedAt": "2017-07-29T12:00:04Z", "providedBy": { "type": "Relationship", - "object": "urn:ngsi-ld:Person:Bob" - } + "object": "urn:ngsi-ld:Person:Bob", + }, } } self.entity2_dict.update(self.entity2_props_dict) @@ -130,9 +113,9 @@ def setUp(self) -> None: "observedAt": "2017-07-29T12:00:04Z", "providedBy": { "type": "Relationship", - "object": "urn:ngsi-ld:Person:Bob" - } - } + "object": "urn:ngsi-ld:Person:Bob", + }, + }, } # # The entity for testing the nested structure of properties # self.entity_sub_props_dict_wrong = { @@ -158,23 +141,20 @@ def setUp(self) -> None: # "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld" # ] # } - self.testpoint_value = { - "type": "Point", - "coordinates": (-8.5, 41.2) - } + self.testpoint_value = {"type": "Point", "coordinates": (-8.5, 41.2)} self.testmultipoint_value = { "type": "MultiPoint", "coordinates": ( (-3.80356167695194, 43.46296641666926), - (-3.804056, 43.464638) - ) + (-3.804056, 43.464638), + ), } self.testlinestring_value = { "type": "LineString", "coordinates": ( (-3.80356167695194, 43.46296641666926), - (-3.804056, 43.464638) - ) + (-3.804056, 43.464638), + ), } self.testpolygon_value = { "type": "Polygon", @@ -183,48 +163,42 @@ def setUp(self) -> None: (-3.80356167695194, 43.46296641666926), (-3.804056, 43.464638), (-3.805056, 43.463638), - (-3.80356167695194, 43.46296641666926) + (-3.80356167695194, 43.46296641666926), ] - ] + ], } self.testgeometrycollection_value = { "type": "GeometryCollection", "geometries": [ { "type": "Point", - "coordinates": (-3.80356167695194, 43.46296641666926) + "coordinates": (-3.80356167695194, 43.46296641666926), }, { "type": "LineString", - "coordinates": ( - (-3.804056, 43.464638), - (-3.805056, 43.463638) - ) - } - ] + "coordinates": ((-3.804056, 43.464638), (-3.805056, 43.463638)), + }, + ], } self.entity_geo_dict = { "id": "urn:ngsi-ld:Geometry:001", "type": "MyGeometry", - "testpoint": { - "type": "GeoProperty", - "value": self.testpoint_value - }, + "testpoint": {"type": "GeoProperty", "value": self.testpoint_value}, "testmultipoint": { "type": "GeoProperty", "value": self.testmultipoint_value, - "observedAt": "2023-09-12T12:35:00Z" + "observedAt": "2023-09-12T12:35:00Z", }, "testlinestring": { "type": "GeoProperty", "value": self.testlinestring_value, - "observedAt": "2023-09-12T12:35:30Z" + "observedAt": "2023-09-12T12:35:30Z", }, "testpolygon": { "type": "GeoProperty", "value": self.testpolygon_value, - "observedAt": "2023-09-12T12:36:00Z" - } + "observedAt": "2023-09-12T12:36:00Z", + }, } def test_cb_property(self) -> None: @@ -233,11 +207,11 @@ def test_cb_property(self) -> None: Returns: None """ - prop = ContextProperty(**{'value': "20"}) + prop = ContextProperty(**{"value": "20"}) self.assertIsInstance(prop.value, str) - prop = ContextProperty(**{'value': 20.53}) + prop = ContextProperty(**{"value": 20.53}) self.assertIsInstance(prop.value, float) - prop = ContextProperty(**{'value': 20}) + prop = ContextProperty(**{"value": 20}) self.assertIsInstance(prop.value, int) def test_geo_property(self) -> None: @@ -249,33 +223,32 @@ def test_geo_property(self) -> None: geo_entity = ContextLDEntity(**self.entity_geo_dict) new_entity = ContextLDEntity(id="urn:ngsi-ld:Geometry:002", type="MyGeometry") test_point = NamedContextGeoProperty( - name="testpoint", - type="GeoProperty", - value=Point(**self.testpoint_value) + name="testpoint", type="GeoProperty", value=Point(**self.testpoint_value) ) test_MultiPoint = NamedContextGeoProperty( name="testmultipoint", type="GeoProperty", - value=MultiPoint(**self.testmultipoint_value) + value=MultiPoint(**self.testmultipoint_value), ) test_LineString = NamedContextGeoProperty( name="testlinestring", type="GeoProperty", - value=LineString(**self.testlinestring_value) + value=LineString(**self.testlinestring_value), ) test_Polygon = NamedContextGeoProperty( name="testpolygon", type="GeoProperty", - value=Polygon(**self.testpolygon_value) + value=Polygon(**self.testpolygon_value), ) with self.assertRaises(ValidationError): test_GeometryCollection = NamedContextGeoProperty( name="testgeometrycollection", type="GeoProperty", - value=GeometryCollection(**self.testgeometrycollection_value) + value=GeometryCollection(**self.testgeometrycollection_value), ) - new_entity.add_geo_properties([test_point, test_MultiPoint, test_LineString, - test_Polygon]) + new_entity.add_geo_properties( + [test_point, test_MultiPoint, test_LineString, test_Polygon] + ) def test_cb_entity(self) -> None: """ @@ -287,41 +260,39 @@ def test_cb_entity(self) -> None: entity1 = ContextLDEntity(**self.entity1_dict) entity2 = ContextLDEntity(**self.entity2_dict) - self.assertEqual(self.entity1_dict, - entity1.model_dump(exclude_unset=True)) + self.assertEqual(self.entity1_dict, entity1.model_dump(exclude_unset=True)) entity1 = ContextLDEntity.model_validate(self.entity1_dict) - self.assertEqual(self.entity2_dict, - entity2.model_dump(exclude_unset=True)) + self.assertEqual(self.entity2_dict, entity2.model_dump(exclude_unset=True)) entity2 = ContextLDEntity.model_validate(self.entity2_dict) # check all properties can be returned by get_properties - properties_1 = entity1.get_properties(response_format='list') + properties_1 = entity1.get_properties(response_format="list") for prop in properties_1: - self.assertEqual(self.entity1_props_dict[prop.name], - prop.model_dump( - exclude={'name'}, - exclude_unset=True)) + self.assertEqual( + self.entity1_props_dict[prop.name], + prop.model_dump(exclude={"name"}, exclude_unset=True), + ) - properties_2 = entity2.get_properties(response_format='list') + properties_2 = entity2.get_properties(response_format="list") for prop in properties_2: - self.assertEqual(self.entity2_props_dict[prop.name], - prop.model_dump( - exclude={'name'}, - exclude_unset=True)) + self.assertEqual( + self.entity2_props_dict[prop.name], + prop.model_dump(exclude={"name"}, exclude_unset=True), + ) # check all relationships can be returned by get_relationships - relationships = entity2.get_relationships(response_format='list') + relationships = entity2.get_relationships(response_format="list") for relationship in relationships: - self.assertEqual(self.entity2_rel_dict[relationship.name], - relationship.model_dump( - exclude={'name'}, - exclude_unset=True)) + self.assertEqual( + self.entity2_rel_dict[relationship.name], + relationship.model_dump(exclude={"name"}, exclude_unset=True), + ) # test add properties - new_prop = {'new_prop': ContextProperty(value=25)} + new_prop = {"new_prop": ContextProperty(value=25)} entity2.add_properties(new_prop) - properties = entity2.get_properties(response_format='list') + properties = entity2.get_properties(response_format="list") self.assertIn("new_prop", [prop.name for prop in properties]) def test_validate_subproperties_dict(self) -> None: @@ -339,13 +310,15 @@ def test_validate_subproperties_dict_wrong(self) -> None: None """ entity_sub_props_dict_wrong_1 = self.entity1_dict.copy() - entity_sub_props_dict_wrong_1[ - "availableSpotNumber"]["reliability"]["type"] = "NotProperty" + entity_sub_props_dict_wrong_1["availableSpotNumber"]["reliability"][ + "type" + ] = "NotProperty" with self.assertRaises(ValueError): entity5 = ContextLDEntity(**entity_sub_props_dict_wrong_1) entity_sub_props_dict_wrong_2 = self.entity1_dict.copy() - entity_sub_props_dict_wrong_2[ - "availableSpotNumber"]["providedBy"]["type"] = "NotRelationship" + entity_sub_props_dict_wrong_2["availableSpotNumber"]["providedBy"][ + "type" + ] = "NotRelationship" with self.assertRaises(ValueError): entity5 = ContextLDEntity(**entity_sub_props_dict_wrong_2) @@ -353,12 +326,11 @@ def test_get_properties(self): """ Test the get_properties method """ - entity = ContextLDEntity(id="urn:ngsi-ld:test", - type="Tester", - hasLocation={ - "type": "Relationship", - "object": "urn:ngsi-ld:test2" - }) + entity = ContextLDEntity( + id="urn:ngsi-ld:test", + type="Tester", + hasLocation={"type": "Relationship", "object": "urn:ngsi-ld:test2"}, + ) properties = [ NamedContextProperty(name="prop1"), @@ -366,18 +338,17 @@ def test_get_properties(self): ] entity.add_properties(properties) entity.get_properties(response_format="list") - self.assertEqual(entity.get_properties(response_format="list"), - properties) + self.assertEqual(entity.get_properties(response_format="list"), properties) def test_entity_delete_properties(self): """ Test the delete_properties method """ - prop = ContextProperty(**{'value': 20, 'type': 'Property'}) - named_prop = NamedContextProperty(**{'name': 'test2', - 'value': 20, - 'type': 'Property'}) - prop3 = ContextProperty(**{'value': 20, 'type': 'Property'}) + prop = ContextProperty(**{"value": 20, "type": "Property"}) + named_prop = NamedContextProperty( + **{"name": "test2", "value": 20, "type": "Property"} + ) + prop3 = ContextProperty(**{"value": 20, "type": "Property"}) entity = ContextLDEntity(id="urn:ngsi-ld:12", type="Test") @@ -385,54 +356,54 @@ def test_entity_delete_properties(self): entity.add_properties([named_prop]) entity.delete_properties({"test1": prop}) - self.assertEqual(set([_prop.name for _prop in entity.get_properties()]), - {"test2", "test3"}) + self.assertEqual( + set([_prop.name for _prop in entity.get_properties()]), {"test2", "test3"} + ) entity.delete_properties([named_prop]) - self.assertEqual(set([_prop.name for _prop in entity.get_properties()]), - {"test3"}) + self.assertEqual( + set([_prop.name for _prop in entity.get_properties()]), {"test3"} + ) entity.delete_properties(["test3"]) - self.assertEqual(set([_prop.name for _prop in entity.get_properties()]), - set()) + self.assertEqual(set([_prop.name for _prop in entity.get_properties()]), set()) def test_entity_relationships(self): entity = ContextLDEntity(**self.entity3_dict) # test get relationships - relationships_list = entity.get_relationships(response_format='list') + relationships_list = entity.get_relationships(response_format="list") self.assertEqual(len(relationships_list), 1) - relationships_dict = entity.get_relationships(response_format='dict') + relationships_dict = entity.get_relationships(response_format="dict") self.assertIn("isParked", relationships_dict) # test add relationships new_rel_dict = { - "name": "new_rel", - "type": "Relationship", - "obejct": 'urn:ngsi-ld:test'} + "name": "new_rel", + "type": "Relationship", + "obejct": "urn:ngsi-ld:test", + } new_rel = NamedContextRelationship(**new_rel_dict) entity.add_relationships([new_rel]) - relationships_list = entity.get_relationships(response_format='list') + relationships_list = entity.get_relationships(response_format="list") self.assertEqual(len(relationships_list), 2) # test delete relationships entity.delete_relationships(["isParked"]) - relationships_list = entity.get_relationships(response_format='list') + relationships_list = entity.get_relationships(response_format="list") self.assertEqual(len(relationships_list), 1) - relationships_dict = entity.get_relationships(response_format='dict') + relationships_dict = entity.get_relationships(response_format="dict") self.assertNotIn("isParked", relationships_dict) def test_get_context(self): entity1 = ContextLDEntity(**self.entity1_dict) context_entity1 = entity1.get_context() - self.assertEqual(self.entity1_context, - context_entity1) + self.assertEqual(self.entity1_context, context_entity1) # test here if entity without context can be validated and get_context # works accordingly: entity3 = ContextLDEntity(**self.entity3_dict) context_entity3 = entity3.get_context() - self.assertEqual(None, - context_entity3) + self.assertEqual(None, context_entity3) diff --git a/tests/models/test_ngsi_ld_subscriptions.py b/tests/models/test_ngsi_ld_subscriptions.py index 61b6d434..ef54debe 100644 --- a/tests/models/test_ngsi_ld_subscriptions.py +++ b/tests/models/test_ngsi_ld_subscriptions.py @@ -1,10 +1,15 @@ """ Test module for context subscriptions and notifications """ + import unittest from pydantic import ValidationError -from filip.models.ngsi_ld.subscriptions import \ - Endpoint, NotificationParams, EntityInfo, TemporalQuery +from filip.models.ngsi_ld.subscriptions import ( + Endpoint, + NotificationParams, + EntityInfo, + TemporalQuery, +) from filip.models.base import FiwareHeader from tests.config import settings @@ -21,47 +26,43 @@ def setUp(self) -> None: None """ self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) self.http_url = "https://test.de:80" self.mqtt_url = "mqtt://test.de:1883" - self.mqtt_topic = '/filip/testing' + self.mqtt_topic = "/filip/testing" self.notification = { - "attributes": ["speed"], - "format": "keyValues", - "endpoint": { - "uri": "http://my.endpoint.org/notify", - "accept": "application/json" - } - } + "attributes": ["speed"], + "format": "keyValues", + "endpoint": { + "uri": "http://my.endpoint.org/notify", + "accept": "application/json", + }, + } self.sub_dict = { "id": "urn:ngsi-ld:Subscription:mySubscription", "type": "Subscription", - "entities": [ - { - "type": "Vehicle" - } - ], + "entities": [{"type": "Vehicle"}], "watchedAttributes": ["speed"], "q": "speed>50", "geoQ": { "georel": "near;maxDistance==2000", "geometry": "Point", - "coordinates": [-1, 100] - }, + "coordinates": [-1, 100], + }, "notification": { "attributes": ["speed"], "format": "keyValues", "endpoint": { "uri": "http://my.endpoint.org/notify", - "accept": "application/json" - } + "accept": "application/json", + }, }, "@context": [ "http://example.org/ngsi-ld/latest/vehicle.jsonld", - "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld" - ] - } + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.3.jsonld", + ], + } def test_endpoint_models(self): """ @@ -69,30 +70,27 @@ def test_endpoint_models(self): Returns: """ - endpoint_http = Endpoint(**{ - "uri": "http://my.endpoint.org/notify", - "accept": "application/json" - }) - endpoint_mqtt = Endpoint(**{ - "uri": "mqtt://my.host.org:1883/my/test/topic", - "accept": "application/json", - "notifierInfo": [ - { - "key": "MQTT-Version", - "value": "mqtt5.0" - } - ] - }) + endpoint_http = Endpoint( + **{"uri": "http://my.endpoint.org/notify", "accept": "application/json"} + ) + endpoint_mqtt = Endpoint( + **{ + "uri": "mqtt://my.host.org:1883/my/test/topic", + "accept": "application/json", + "notifierInfo": [{"key": "MQTT-Version", "value": "mqtt5.0"}], + } + ) with self.assertRaises(ValidationError): - endpoint_https = Endpoint(**{ - "uri": "https://my.endpoint.org/notify", - "accept": "application/json" - }) + endpoint_https = Endpoint( + **{ + "uri": "https://my.endpoint.org/notify", + "accept": "application/json", + } + ) with self.assertRaises(ValidationError): - endpoint_amqx = Endpoint(**{ - "uri": "amqx://my.endpoint.org/notify", - "accept": "application/json" - }) + endpoint_amqx = Endpoint( + **{"uri": "amqx://my.endpoint.org/notify", "accept": "application/json"} + ) def test_notification_models(self): """ @@ -108,17 +106,11 @@ def test_entity_selector_models(self): Returns: """ - entity_info = EntityInfo.model_validate({ - "type": "Vehicle" - }) + entity_info = EntityInfo.model_validate({"type": "Vehicle"}) with self.assertRaises(ValueError): - entity_info = EntityInfo.model_validate({ - "id": "test:001" - }) + entity_info = EntityInfo.model_validate({"id": "test:001"}) with self.assertRaises(ValueError): - entity_info = EntityInfo.model_validate({ - "idPattern": ".*" - }) + entity_info = EntityInfo.model_validate({"idPattern": ".*"}) def test_temporal_query_models(self): """ @@ -126,46 +118,40 @@ def test_temporal_query_models(self): Returns: """ - example0_temporalQ = { - "timerel": "before", - "timeAt": "2017-12-13T14:20:00Z" - } - self.assertEqual(example0_temporalQ, - TemporalQuery.model_validate(example0_temporalQ).model_dump( - exclude_unset=True) - ) - - example1_temporalQ = { - "timerel": "after", - "timeAt": "2017-12-13T14:20:00Z" - } - self.assertEqual(example1_temporalQ, - TemporalQuery.model_validate(example1_temporalQ).model_dump( - exclude_unset=True) - ) + example0_temporalQ = {"timerel": "before", "timeAt": "2017-12-13T14:20:00Z"} + self.assertEqual( + example0_temporalQ, + TemporalQuery.model_validate(example0_temporalQ).model_dump( + exclude_unset=True + ), + ) + + example1_temporalQ = {"timerel": "after", "timeAt": "2017-12-13T14:20:00Z"} + self.assertEqual( + example1_temporalQ, + TemporalQuery.model_validate(example1_temporalQ).model_dump( + exclude_unset=True + ), + ) example2_temporalQ = { "timerel": "between", "timeAt": "2017-12-13T14:20:00Z", "endTimeAt": "2017-12-13T14:40:00Z", - "timeproperty": "modifiedAt" + "timeproperty": "modifiedAt", } - self.assertEqual(example2_temporalQ, - TemporalQuery.model_validate(example2_temporalQ).model_dump( - exclude_unset=True) - ) + self.assertEqual( + example2_temporalQ, + TemporalQuery.model_validate(example2_temporalQ).model_dump( + exclude_unset=True + ), + ) - example3_temporalQ = { - "timerel": "between", - "timeAt": "2017-12-13T14:20:00Z" - } + example3_temporalQ = {"timerel": "between", "timeAt": "2017-12-13T14:20:00Z"} with self.assertRaises(ValueError): TemporalQuery.model_validate(example3_temporalQ) - example4_temporalQ = { - "timerel": "before", - "timeAt": "14:20:00Z" - } + example4_temporalQ = {"timerel": "before", "timeAt": "14:20:00Z"} with self.assertRaises(ValueError): TemporalQuery.model_validate(example4_temporalQ) @@ -173,7 +159,7 @@ def test_temporal_query_models(self): "timerel": "between", "timeAt": "2017-12-13T14:20:00Z", "endTimeAt": "14:40:00Z", - "timeproperty": "modifiedAt" + "timeproperty": "modifiedAt", } with self.assertRaises(ValueError): TemporalQuery.model_validate(example5_temporalQ) diff --git a/tests/models/test_ngsi_v2_base.py b/tests/models/test_ngsi_v2_base.py index d0d2d128..300505e0 100644 --- a/tests/models/test_ngsi_v2_base.py +++ b/tests/models/test_ngsi_v2_base.py @@ -1,6 +1,7 @@ """ Test module for context subscriptions and notifications """ + import unittest from pydantic import ValidationError @@ -15,6 +16,7 @@ class TestSubscriptions(unittest.TestCase): """ Test class for context broker models """ + def setUp(self) -> None: """ Setup test data @@ -22,15 +24,14 @@ def setUp(self) -> None: None """ self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) self.http_url = "https://test.de:80" self.mqtt_url = "mqtt://test.de:1883" - self.mqtt_topic = '/filip/testing' + self.mqtt_topic = "/filip/testing" def test_entity_pattern(self) -> None: - """ - """ + """ """ _id = "urn:ngsi-ld:Test:001" _idPattern = ".*" _type = "Test" @@ -68,5 +69,4 @@ def tearDown(self) -> None: """ Cleanup test server """ - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL) + clear_all(fiware_header=self.fiware_header, cb_url=settings.CB_URL) diff --git a/tests/models/test_ngsi_v2_context.py b/tests/models/test_ngsi_v2_context.py index db2c1ea4..776fc3a5 100644 --- a/tests/models/test_ngsi_v2_context.py +++ b/tests/models/test_ngsi_v2_context.py @@ -1,6 +1,7 @@ """ Test module for context broker models """ + import unittest from typing import List from pydantic import ValidationError diff --git a/tests/models/test_ngsi_v2_iot.py b/tests/models/test_ngsi_v2_iot.py index fe99111b..240b5c4d 100644 --- a/tests/models/test_ngsi_v2_iot.py +++ b/tests/models/test_ngsi_v2_iot.py @@ -1,6 +1,7 @@ """ Test module for context broker models """ + import time import unittest from typing import List @@ -10,8 +11,16 @@ import pyjexl from filip.models.base import FiwareHeader -from filip.models.ngsi_v2.iot import DeviceCommand, ServiceGroup, \ - Device, TransportProtocol, IoTABaseAttribute, ExpressionLanguage, PayloadProtocol, DeviceAttribute +from filip.models.ngsi_v2.iot import ( + DeviceCommand, + ServiceGroup, + Device, + TransportProtocol, + IoTABaseAttribute, + ExpressionLanguage, + PayloadProtocol, + DeviceAttribute, +) from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient from filip.utils.cleanup import clear_all, clean_test @@ -31,17 +40,19 @@ def setUp(self) -> None: """ self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) self.iota_client = IoTAClient( - url=settings.IOTA_JSON_URL, - fiware_header=self.fiware_header) + url=settings.IOTA_JSON_URL, fiware_header=self.fiware_header + ) self.cb_client = ContextBrokerClient( - url=settings.CB_URL, - fiware_header=self.fiware_header) + url=settings.CB_URL, fiware_header=self.fiware_header + ) def test_fiware_safe_fields(self): """ @@ -60,86 +71,131 @@ def test_fiware_safe_fields(self): # Test if all needed fields, detect all invalid strings for string in invalid_strings: - self.assertRaises(ValidationError, IoTABaseAttribute, - name=string, type="name", - entity_name="name", entity_type="name") - self.assertRaises(ValidationError, IoTABaseAttribute, - name="name", type=string, - entity_name="name", entity_type="name") - self.assertRaises(ValidationError, IoTABaseAttribute, - name="name", type="name", - entity_name=string, entity_type="name") - self.assertRaises(ValidationError, IoTABaseAttribute, - name="name", type="name", - entity_name="name", entity_type=string) - - self.assertRaises(ValidationError, - DeviceCommand, name=string, type="name") - self.assertRaises(ValidationError, - ServiceGroup, entity_type=string, - resource="", apikey="") - - self.assertRaises(ValidationError, - Device, device_id="", - entity_name=string, - entity_type="name", - transport=TransportProtocol.HTTP) - self.assertRaises(ValidationError, - Device, device_id="", - entity_name="name", - entity_type=string, - transport=TransportProtocol.HTTP) + self.assertRaises( + ValidationError, + IoTABaseAttribute, + name=string, + type="name", + entity_name="name", + entity_type="name", + ) + self.assertRaises( + ValidationError, + IoTABaseAttribute, + name="name", + type=string, + entity_name="name", + entity_type="name", + ) + self.assertRaises( + ValidationError, + IoTABaseAttribute, + name="name", + type="name", + entity_name=string, + entity_type="name", + ) + self.assertRaises( + ValidationError, + IoTABaseAttribute, + name="name", + type="name", + entity_name="name", + entity_type=string, + ) + + self.assertRaises(ValidationError, DeviceCommand, name=string, type="name") + self.assertRaises( + ValidationError, + ServiceGroup, + entity_type=string, + resource="", + apikey="", + ) + + self.assertRaises( + ValidationError, + Device, + device_id="", + entity_name=string, + entity_type="name", + transport=TransportProtocol.HTTP, + ) + self.assertRaises( + ValidationError, + Device, + device_id="", + entity_name="name", + entity_type=string, + transport=TransportProtocol.HTTP, + ) # Test if all needed fields, do not trow wrong errors for string in valid_strings: - IoTABaseAttribute(name=string, type=string, - entity_name=string, entity_type=string) + IoTABaseAttribute( + name=string, type=string, entity_name=string, entity_type=string + ) DeviceCommand(name=string, type="name") ServiceGroup(entity_type=string, resource="", apikey="") - Device(device_id="", entity_name=string, entity_type=string, - transport=TransportProtocol.HTTP) + Device( + device_id="", + entity_name=string, + entity_type=string, + transport=TransportProtocol.HTTP, + ) # Test for the special-string protected field if all strings are blocked for string in special_strings: with self.assertRaises(ValidationError): - IoTABaseAttribute(name=string, type="name", entity_name="name", - entity_type="name") + IoTABaseAttribute( + name=string, type="name", entity_name="name", entity_type="name" + ) with self.assertRaises(ValidationError): - IoTABaseAttribute(name="name", type=string, entity_name="name", - entity_type="name") + IoTABaseAttribute( + name="name", type=string, entity_name="name", entity_type="name" + ) with self.assertRaises(ValidationError): DeviceCommand(name=string, type="name") # Test for the normal protected field if all strings are allowed for string in special_strings: - IoTABaseAttribute(name="name", type="name", - entity_name=string, entity_type=string) + IoTABaseAttribute( + name="name", type="name", entity_name=string, entity_type=string + ) ServiceGroup(entity_type=string, resource="", apikey="") - Device(device_id="", entity_name=string, entity_type=string, - transport=TransportProtocol.HTTP) + Device( + device_id="", + entity_name=string, + entity_type=string, + transport=TransportProtocol.HTTP, + ) - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) def test_expression_language(self): - api_key = settings.FIWARE_SERVICEPATH.strip('/') + api_key = settings.FIWARE_SERVICEPATH.strip("/") # Test expression language on service group level service_group_jexl = ServiceGroup( - entity_type='Thing', + entity_type="Thing", apikey=api_key, - resource='/iot/json', - expressionLanguage=ExpressionLanguage.JEXL) + resource="/iot/json", + expressionLanguage=ExpressionLanguage.JEXL, + ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") service_group_legacy = ServiceGroup( - entity_type='Thing', + entity_type="Thing", apikey=api_key, - resource='/iot/json', - expressionLanguage=ExpressionLanguage.LEGACY) + resource="/iot/json", + expressionLanguage=ExpressionLanguage.LEGACY, + ) assert len(w) == 1 assert issubclass(w[0].category, UserWarning) @@ -148,44 +204,50 @@ def test_expression_language(self): self.iota_client.post_group(service_group=service_group_jexl) # Test jexl expression language on device level - device1 = Device(device_id="test_device", - entity_name="test_entity", - entity_type="test_entity_type", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - expressionLanguage=ExpressionLanguage.JEXL, - attributes=[DeviceAttribute(name="value", type="Number"), - DeviceAttribute(name="fraction", type="Number", - expression="(value + 3) / 10"), - DeviceAttribute(name="spaces", type="Text"), - DeviceAttribute(name="trimmed", type="Text", - expression="spaces|trim"), - ] - ) + device1 = Device( + device_id="test_device", + entity_name="test_entity", + entity_type="test_entity_type", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + expressionLanguage=ExpressionLanguage.JEXL, + attributes=[ + DeviceAttribute(name="value", type="Number"), + DeviceAttribute( + name="fraction", type="Number", expression="(value + 3) / 10" + ), + DeviceAttribute(name="spaces", type="Text"), + DeviceAttribute(name="trimmed", type="Text", expression="spaces|trim"), + ], + ) self.iota_client.post_device(device=device1) mqtt_cl = mqtt_client.Client(callback_api_version=CallbackAPIVersion.VERSION2) mqtt_cl.connect(settings.MQTT_BROKER_URL.host, settings.MQTT_BROKER_URL.port) mqtt_cl.loop_start() - mqtt_cl.publish(topic=f'/json/{api_key}/{device1.device_id}/attrs', - payload='{"value": 12, "spaces": " foobar "}') + mqtt_cl.publish( + topic=f"/json/{api_key}/{device1.device_id}/attrs", + payload='{"value": 12, "spaces": " foobar "}', + ) time.sleep(2) entity1 = self.cb_client.get_entity(entity_id=device1.entity_name) - self.assertEqual(entity1.get_attribute('fraction').value, 1.5) - self.assertEqual(entity1.get_attribute('trimmed').value, 'foobar') + self.assertEqual(entity1.get_attribute("fraction").value, 1.5) + self.assertEqual(entity1.get_attribute("trimmed").value, "foobar") mqtt_cl.loop_stop() mqtt_cl.disconnect() # Test for wrong jexl expressions - device2 = Device(device_id="wrong_device", - entity_name="test_entity", - entity_type="test_entity_type", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON) + device2 = Device( + device_id="wrong_device", + entity_name="test_entity", + entity_type="test_entity_type", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + ) attr1 = DeviceAttribute(name="value", type="Number", expression="value ++ 3") with self.assertRaises(pyjexl.jexl.ParseError): @@ -197,7 +259,9 @@ def test_expression_language(self): device2.add_attribute(attr2) device2.delete_attribute(attr2) - attr3 = DeviceAttribute(name="brackets", type="Number", expression="((2 + 3) / 10") + attr3 = DeviceAttribute( + name="brackets", type="Number", expression="((2 + 3) / 10" + ) with self.assertRaises(pyjexl.jexl.ParseError): device2.add_attribute(attr3) device2.delete_attribute(attr3) @@ -206,29 +270,36 @@ def test_expression_language(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - device3 = Device(device_id="legacy_device", - entity_name="test_entity", - entity_type="test_entity_type", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - expressionLanguage=ExpressionLanguage.LEGACY) + device3 = Device( + device_id="legacy_device", + entity_name="test_entity", + entity_type="test_entity_type", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + expressionLanguage=ExpressionLanguage.LEGACY, + ) assert len(w) == 2 # Test for expression language set to None service_group_null_expression = ServiceGroup( - entity_type='Thing', + entity_type="Thing", apikey=api_key, - resource='/iot/json', - expressionLanguage=None) - self.assertEqual(service_group_null_expression.expressionLanguage, ExpressionLanguage.JEXL) - - device4 = Device(device_id="null_expression_device", - entity_name="null_expression_entity", - entity_type="test_entity_type", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - expressionLanguage=None) + resource="/iot/json", + expressionLanguage=None, + ) + self.assertEqual( + service_group_null_expression.expressionLanguage, ExpressionLanguage.JEXL + ) + + device4 = Device( + device_id="null_expression_device", + entity_name="null_expression_entity", + entity_type="test_entity_type", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + expressionLanguage=None, + ) self.assertEqual(device4.expressionLanguage, ExpressionLanguage.JEXL) def test_add_device_attributes(self): @@ -240,58 +311,39 @@ def test_add_device_attributes(self): - object_id is not required, if given must be unique, i.e. not equal to any existing object_id and name """ + def initial_device(): - attr = DeviceAttribute( - name="temperature", - type="Number" - ) + attr = DeviceAttribute(name="temperature", type="Number") return Device( device_id="dummy:01", entity_name="entity:01", entity_type="MyEntity", - attributes=[attr] + attributes=[attr], ) # fail, because attr1 and attr ara identical device_a = initial_device() - attr1 = DeviceAttribute( - name="temperature", - type="Number" - ) + attr1 = DeviceAttribute(name="temperature", type="Number") with self.assertRaises(ValueError): device_a.add_attribute(attribute=attr1) # fail, because the object_id is duplicated with the name of attr1 device_b = initial_device() attr2 = DeviceAttribute( - name="temperature", - type="Number", - object_id="temperature" + name="temperature", type="Number", object_id="temperature" ) with self.assertRaises(ValueError): device_b.add_attribute(attribute=attr2) # success device_c = initial_device() - attr3 = DeviceAttribute( - name="temperature", - type="Number", - object_id="t1" - ) + attr3 = DeviceAttribute(name="temperature", type="Number", object_id="t1") device_c.add_attribute(attribute=attr3) # success - attr4 = DeviceAttribute( - name="temperature", - type="Number", - object_id="t2" - ) + attr4 = DeviceAttribute(name="temperature", type="Number", object_id="t2") device_c.add_attribute(attribute=attr4) # fail, because object id is duplicated - attr5 = DeviceAttribute( - name="temperature2", - type="Number", - object_id="t2" - ) + attr5 = DeviceAttribute(name="temperature2", type="Number", object_id="t2") with self.assertRaises(ValueError): device_c.add_attribute(attribute=attr5) @@ -306,23 +358,20 @@ def test_device_creation(self): existing object_id and name """ - def create_device(attr1_name, attr2_name, - attr1_object_id=None, attr2_object_id=None): + def create_device( + attr1_name, attr2_name, attr1_object_id=None, attr2_object_id=None + ): _attr1 = DeviceAttribute( - name=attr1_name, - object_id=attr1_object_id, - type="Number" + name=attr1_name, object_id=attr1_object_id, type="Number" ) _attr2 = DeviceAttribute( - name=attr2_name, - object_id=attr2_object_id, - type="Number" + name=attr2_name, object_id=attr2_object_id, type="Number" ) return Device( device_id="dummy:01", entity_name="entity:01", entity_type="MyEntity", - attributes=[_attr1, _attr2] + attributes=[_attr1, _attr2], ) # fail, because attr1 and attr ara identical @@ -331,7 +380,7 @@ def create_device(attr1_name, attr2_name, attr1_name="temperature", attr2_name="temperature", attr1_object_id=None, - attr2_object_id=None + attr2_object_id=None, ) # fail, because the object_id is duplicated with the name of attr1 @@ -340,7 +389,7 @@ def create_device(attr1_name, attr2_name, attr1_name="temperature", attr2_name="temperature", attr1_object_id=None, - attr2_object_id="temperature" + attr2_object_id="temperature", ) # success @@ -348,14 +397,10 @@ def create_device(attr1_name, attr2_name, attr1_name="temperature", attr2_name="temperature", attr1_object_id=None, - attr2_object_id="t1" + attr2_object_id="t1", ) # success - attr4 = DeviceAttribute( - name="temperature", - type="Number", - object_id="t2" - ) + attr4 = DeviceAttribute(name="temperature", type="Number", object_id="t2") device.add_attribute(attribute=attr4) # fail, because object id is duplicated @@ -364,15 +409,17 @@ def create_device(attr1_name, attr2_name, attr1_name="temperature2", attr2_name="temperature", attr1_object_id="t", - attr2_object_id="t" + attr2_object_id="t", ) def tearDown(self) -> None: """ Cleanup test server """ - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL, - iota_url=settings.IOTA_JSON_URL) + clear_all( + fiware_header=self.fiware_header, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL, + ) self.iota_client.close() self.cb_client.close() diff --git a/tests/models/test_ngsi_v2_subscriptions.py b/tests/models/test_ngsi_v2_subscriptions.py index b2b9b8a2..73e12c22 100644 --- a/tests/models/test_ngsi_v2_subscriptions.py +++ b/tests/models/test_ngsi_v2_subscriptions.py @@ -1,20 +1,23 @@ """ Test module for context subscriptions and notifications """ + import json import unittest from pydantic import ValidationError from filip.clients.ngsi_v2 import ContextBrokerClient -from filip.models.ngsi_v2.subscriptions import \ - Http, \ - HttpCustom, \ - Mqtt, \ - MqttCustom, \ - Notification, \ - Subscription, \ - NgsiPayload, \ - NgsiPayloadAttr, Condition +from filip.models.ngsi_v2.subscriptions import ( + Http, + HttpCustom, + Mqtt, + MqttCustom, + Notification, + Subscription, + NgsiPayload, + NgsiPayloadAttr, + Condition, +) from filip.models.base import FiwareHeader from filip.utils.cleanup import clear_all, clean_test from tests.config import settings @@ -32,47 +35,27 @@ def setUp(self) -> None: None """ self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) self.http_url = "https://test.de:80" self.mqtt_url = "mqtt://test.de:1883" - self.mqtt_topic = '/filip/testing' + self.mqtt_topic = "/filip/testing" self.notification = { - "http": - { - "url": "http://localhost:1234" - }, - "attrs": [ - "temperature", - "humidity" - ] + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], } self.sub_dict = { "description": "One subscription to rule them all", "subject": { - "entities": [ - { - "idPattern": ".*", - "type": "Room" - } - ], + "entities": [{"idPattern": ".*", "type": "Room"}], "condition": { - "attrs": [ - "temperature" - ], - "expression": { - "q": "temperature>40" - } - } + "attrs": ["temperature"], + "expression": {"q": "temperature>40"}, + }, }, "notification": { - "http": { - "url": "http://localhost:1234" - }, - "attrs": [ - "temperature", - "humidity" - ] + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], }, "expires": "2030-04-05T14:00:00Z", } @@ -87,20 +70,16 @@ def test_notification_models(self): with self.assertRaises(ValidationError): HttpCustom(url="brokenScheme://test.de:80") with self.assertRaises(ValidationError): - Mqtt(url="brokenScheme://test.de:1883", - topic='/testing') + Mqtt(url="brokenScheme://test.de:1883", topic="/testing") with self.assertRaises(ValidationError): - Mqtt(url="mqtt://test.de:1883", - topic='/,t') + Mqtt(url="mqtt://test.de:1883", topic="/,t") with self.assertRaises(ValidationError): HttpCustom(url="https://working-url.de:80", json={}, ngsi={}) with self.assertRaises(ValidationError): HttpCustom(url="https://working-url.de:80", payload="", json={}) httpCustom = HttpCustom(url=self.http_url) - mqtt = Mqtt(url=self.mqtt_url, - topic=self.mqtt_topic) - mqttCustom = MqttCustom(url=self.mqtt_url, - topic=self.mqtt_topic) + mqtt = Mqtt(url=self.mqtt_url, topic=self.mqtt_topic) + mqttCustom = MqttCustom(url=self.mqtt_url, topic=self.mqtt_topic) # Test validator for conflicting fields notification = Notification.model_validate(self.notification) @@ -115,26 +94,19 @@ def test_notification_models(self): with self.assertRaises(ValidationError): HttpCustom(url=self.http_url, json={}, payload="") with self.assertRaises(ValidationError): - MqttCustom(url=self.mqtt_url, - topic=self.mqtt_topic, ngsi=NgsiPayload(), payload="") + MqttCustom( + url=self.mqtt_url, topic=self.mqtt_topic, ngsi=NgsiPayload(), payload="" + ) with self.assertRaises(ValidationError): HttpCustom(url=self.http_url, ngsi=NgsiPayload(), json="") - #Test validator for ngsi payload type + # Test validator for ngsi payload type with self.assertRaises(ValidationError): - attr_dict = { - "metadata": {} - } + attr_dict = {"metadata": {}} NgsiPayloadAttr(**attr_dict) with self.assertRaises(ValidationError): - attr_dict = { - "id": "entityId", - "type": "entityType", - "k": "v" - } - NgsiPayload(NgsiPayloadAttr(**attr_dict), - id="someId", - type="someType") + attr_dict = {"id": "entityId", "type": "entityType", "k": "v"} + NgsiPayload(NgsiPayloadAttr(**attr_dict), id="someId", type="someType") # test onlyChangedAttrs-field notification = Notification.model_validate(self.notification) @@ -156,34 +128,27 @@ def test_substitution_models(self): """ # Substitution in payloads payload = "t=${temperature}" - _json = { - "t1": "${temperature}" - } + _json = {"t1": "${temperature}"} ngsi_payload = { # NGSI payload (templatized) "id": "some_prefix:${id}", "type": "NewType", - "t2": "${temperature}" + "t2": "${temperature}", } # In case of httpCustom: notification_httpCustom_data = { - "httpCustom": - { - "url": "http://localhost:1234" - }, - "attrs": [ - "temperature", - "humidity" - ] + "httpCustom": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], } notification_httpCustom = Notification.model_validate( - notification_httpCustom_data) + notification_httpCustom_data + ) notification_httpCustom.httpCustom.url = "http://${hostName}.com" # Headers (both header name and value can be templatized) notification_httpCustom.httpCustom.headers = { "Fiware-Service": "${Service}", "Fiware-ServicePath": "${ServicePath}", - "x-auth-token": "${authToken}" + "x-auth-token": "${authToken}", } notification_httpCustom.httpCustom.qs = { "type": "${type}", @@ -197,40 +162,40 @@ def test_substitution_models(self): # In case of mqttCustom: notification_mqttCustom_data = { - "mqttCustom": - { + "mqttCustom": { "url": "mqtt://localhost:1883", - "topic": "/some/topic/${id}" + "topic": "/some/topic/${id}", }, - "attrs": [ - "temperature", - "humidity" - ] + "attrs": ["temperature", "humidity"], } notification_mqttCustom = Notification.model_validate( - notification_mqttCustom_data) + notification_mqttCustom_data + ) notification_mqttCustom.mqttCustom.payload = payload notification_mqttCustom.mqttCustom.payload = None notification_mqttCustom.mqttCustom.json = _json notification_mqttCustom.mqttCustom.json = None notification_mqttCustom.mqttCustom.ngsi = ngsi_payload - @clean_test(fiware_service=settings.FIWARE_SERVICE, - fiware_servicepath=settings.FIWARE_SERVICEPATH, - cb_url=settings.CB_URL) + @clean_test( + fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + ) def test_subscription_models(self) -> None: """ Test subscription models Returns: None """ - tmp_dict=self.sub_dict.copy() + tmp_dict = self.sub_dict.copy() sub = Subscription.model_validate(tmp_dict) - fiware_header = FiwareHeader(service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) + fiware_header = FiwareHeader( + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) with ContextBrokerClient( - url=settings.CB_URL, - fiware_header=fiware_header) as client: + url=settings.CB_URL, fiware_header=fiware_header + ) as client: sub_id = client.post_subscription(subscription=sub) sub_res = client.get_subscription(subscription_id=sub_id) @@ -241,67 +206,71 @@ def compare_dicts(dict1: dict, dict2: dict): else: self.assertEqual(str(value), str(dict2[key])) - compare_dicts(sub.model_dump(exclude={'id'}), - sub_res.model_dump(exclude={'id'})) + compare_dicts( + sub.model_dump(exclude={"id"}), sub_res.model_dump(exclude={"id"}) + ) - tmp_dict.update({"notification":{ - "httpCustom": { - "url": "http://localhost:1234", - "ngsi":{ - "patchattr":{ - "value":"${temperature/2}", - "type":"Calculated" - } - }, - "method":"POST" - }, - "attrs": [ - "temperature", - "humidity" - ] - }}) + tmp_dict.update( + { + "notification": { + "httpCustom": { + "url": "http://localhost:1234", + "ngsi": { + "patchattr": { + "value": "${temperature/2}", + "type": "Calculated", + } + }, + "method": "POST", + }, + "attrs": ["temperature", "humidity"], + } + } + ) sub = Subscription.model_validate(tmp_dict) sub_id = client.post_subscription(subscription=sub) sub_res = client.get_subscription(subscription_id=sub_id) - compare_dicts(sub.model_dump(exclude={'id'}), - sub_res.model_dump(exclude={'id'})) + compare_dicts( + sub.model_dump(exclude={"id"}), sub_res.model_dump(exclude={"id"}) + ) - tmp_dict.update({"notification":{ - "httpCustom": { - "url": "http://localhost:1234", - "json":{ - "t":"${temperate}", - "h":"${humidity}" - }, - "method":"POST" - }, - "attrs": [ - "temperature", - "humidity" - ] - }}) + tmp_dict.update( + { + "notification": { + "httpCustom": { + "url": "http://localhost:1234", + "json": {"t": "${temperate}", "h": "${humidity}"}, + "method": "POST", + }, + "attrs": ["temperature", "humidity"], + } + } + ) sub = Subscription.model_validate(tmp_dict) sub_id = client.post_subscription(subscription=sub) sub_res = client.get_subscription(subscription_id=sub_id) - compare_dicts(sub.model_dump(exclude={'id'}), - sub_res.model_dump(exclude={'id'})) + compare_dicts( + sub.model_dump(exclude={"id"}), sub_res.model_dump(exclude={"id"}) + ) - tmp_dict.update({"notification":{ - "httpCustom": { - "url": "http://localhost:1234", - "payload":"Temperature is ${temperature} and humidity ${humidity}", - "method":"POST" - }, - "attrs": [ - "temperature", - "humidity" - ] - }}) + tmp_dict.update( + { + "notification": { + "httpCustom": { + "url": "http://localhost:1234", + "payload": "Temperature is ${temperature} and humidity ${humidity}", + "method": "POST", + }, + "attrs": ["temperature", "humidity"], + } + } + ) sub = Subscription.model_validate(tmp_dict) sub_id = client.post_subscription(subscription=sub) sub_res = client.get_subscription(subscription_id=sub_id) - compare_dicts(sub.model_dump(exclude={'id'}), - sub_res.model_dump(exclude={'id'})) + compare_dicts( + sub.model_dump(exclude={"id"}), sub_res.model_dump(exclude={"id"}) + ) # test validation of throttling with self.assertRaises(ValidationError): @@ -311,14 +280,22 @@ def compare_dicts(dict1: dict, dict2: dict): def test_query_string_serialization(self): sub = Subscription.model_validate(self.sub_dict) - self.assertIsInstance(json.loads(sub.subject.condition.expression.model_dump_json())["q"], - str) - self.assertIsInstance(json.loads(sub.subject.condition.model_dump_json())["expression"]["q"], - str) - self.assertIsInstance(json.loads(sub.subject.model_dump_json())["condition"]["expression"]["q"], - str) - self.assertIsInstance(json.loads(sub.model_dump_json())["subject"]["condition"]["expression"]["q"], - str) + self.assertIsInstance( + json.loads(sub.subject.condition.expression.model_dump_json())["q"], str + ) + self.assertIsInstance( + json.loads(sub.subject.condition.model_dump_json())["expression"]["q"], str + ) + self.assertIsInstance( + json.loads(sub.subject.model_dump_json())["condition"]["expression"]["q"], + str, + ) + self.assertIsInstance( + json.loads(sub.model_dump_json())["subject"]["condition"]["expression"][ + "q" + ], + str, + ) def test_model_dump_json(self): sub = Subscription.model_validate(self.sub_dict) @@ -363,5 +340,4 @@ def tearDown(self) -> None: """ Cleanup test server """ - clear_all(fiware_header=self.fiware_header, - cb_url=settings.CB_URL) \ No newline at end of file + clear_all(fiware_header=self.fiware_header, cb_url=settings.CB_URL) diff --git a/tests/models/test_ngsiv2_timeseries.py b/tests/models/test_ngsiv2_timeseries.py index f4d02109..c4bb6579 100644 --- a/tests/models/test_ngsiv2_timeseries.py +++ b/tests/models/test_ngsiv2_timeseries.py @@ -1,6 +1,7 @@ """ Tests for time series model """ + import logging import unittest from filip.models.ngsi_v2.timeseries import TimeSeries, TimeSeriesHeader @@ -13,6 +14,7 @@ class TestTimeSeriesModel(unittest.TestCase): """ Test class for time series model """ + def setUp(self) -> None: """ Setup test data @@ -21,62 +23,32 @@ def setUp(self) -> None: """ self.data1 = { "attributes": [ - { - "attrName": "temperature", - "values": [ - 24.1, - 25.3, - 26.7 - ] - }, - { - "attrName": "pressure", - "values": [ - 1.01, - 0.9, - 1.02 - ] - } + {"attrName": "temperature", "values": [24.1, 25.3, 26.7]}, + {"attrName": "pressure", "values": [1.01, 0.9, 1.02]}, ], "entityId": "Kitchen", "index": [ "2018-01-05T15:44:34", "2018-01-06T15:44:59", - "2018-01-07T15:44:59" - ] + "2018-01-07T15:44:59", + ], } self.data2 = { "attributes": [ - { - "attrName": "temperature", - "values": [ - 34.1, - 35.3, - 36.7 - ] - }, - { - "attrName": "pressure", - "values": [ - 2.01, - 1.9, - 2.02 - ] - } + {"attrName": "temperature", "values": [34.1, 35.3, 36.7]}, + {"attrName": "pressure", "values": [2.01, 1.9, 2.02]}, ], "entityId": "Kitchen", "index": [ "2018-01-08T15:44:34", "2018-01-09T15:44:59", - "2018-01-10T15:44:59" - ] + "2018-01-10T15:44:59", + ], } - self.timeseries_header = {"entityId": "test_id", - "entityType": "test_type"} + self.timeseries_header = {"entityId": "test_id", "entityType": "test_type"} - self.timeseries_header_alias = {"id": "test_id", - "type": "test_type"} + self.timeseries_header_alias = {"id": "test_id", "type": "test_type"} def test_model_creation(self): """ @@ -102,9 +74,10 @@ def test_timeseries_header(self): header = TimeSeriesHeader(**self.timeseries_header) header_by_alias = TimeSeriesHeader(**self.timeseries_header_alias) self.assertEqual(header.model_dump(), header_by_alias.model_dump()) - self.assertEqual(header.model_dump(by_alias=True), - header_by_alias.model_dump(by_alias=True)) + self.assertEqual( + header.model_dump(by_alias=True), header_by_alias.model_dump(by_alias=True) + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/models/test_units.py b/tests/models/test_units.py index 4a90fe60..fe1ec98c 100644 --- a/tests/models/test_units.py +++ b/tests/models/test_units.py @@ -1,13 +1,9 @@ """ Test for filip.models.units """ + from unittest import TestCase -from filip.models.ngsi_v2.units import \ - Unit, \ - Units, \ - UnitCode, \ - UnitText, \ - load_units +from filip.models.ngsi_v2.units import Unit, Units, UnitCode, UnitText, load_units class TestUnitCodes(TestCase): @@ -15,8 +11,7 @@ class TestUnitCodes(TestCase): def setUp(self): self.units_data = load_units() self.units = Units() - self.unit = {"code": "C58", - "name": "newton second per metre"} + self.unit = {"code": "C58", "name": "newton second per metre"} def test_unit_code(self): """ @@ -57,8 +52,7 @@ def test_units(self): units = Units() self.assertEqual(self.units_data.Name.to_list(), units.keys()) self.assertEqual(self.units_data.Name.to_list(), units.names) - self.assertEqual(self.units_data.CommonCode.to_list(), - units.keys(by_code=True)) + self.assertEqual(self.units_data.CommonCode.to_list(), units.keys(by_code=True)) self.assertEqual(self.units_data.CommonCode.to_list(), units.codes) # check get or __getitem__, respectively @@ -72,7 +66,6 @@ def test_units(self): for v in units.values(): v.model_dump_json(indent=2) - def test_unit_validator(self): """ Test if unit hints are given for typos @@ -80,7 +73,6 @@ def test_unit_validator(self): None """ unit_data = self.unit.copy() - unit_data['name'] = "celcius" + unit_data["name"] = "celcius" with self.assertRaises(ValueError): Unit(**unit_data) - diff --git a/tests/semantics/test_parser.py b/tests/semantics/test_parser.py index 6e4d97db..f60f8022 100644 --- a/tests/semantics/test_parser.py +++ b/tests/semantics/test_parser.py @@ -1,6 +1,7 @@ """ Tests for filip.semantics.ontology_parser.parser """ + import collections import unittest @@ -24,8 +25,9 @@ def test_parser(self): """ vocabulary = Vocabulary() - with open(get_file_path('ontology_files/ParsingTesterOntology.ttl'), - 'r') as file: + with open( + get_file_path("ontology_files/ParsingTesterOntology.ttl"), "r" + ) as file: data = file.read() # source = Source(id="my_unique_id", source_name="TestSource", @@ -37,108 +39,143 @@ def test_parser(self): # # post_process_vocabulary(vocabulary=vocabulary) - vocabulary = VocabularyConfigurator.\ - add_ontology_to_vocabulary_as_string(vocabulary, "test", data) - + vocabulary = VocabularyConfigurator.add_ontology_to_vocabulary_as_string( + vocabulary, "test", data + ) # class annotations - self.assertEqual(vocabulary.get_class_by_iri(iri("Class1")).label, - "Class1") - self.assertEqual(vocabulary.get_class_by_iri(iri("Class2")).get_label(), - "Class2") - self.assertEqual(vocabulary.get_class_by_iri( - iri("Class1")).comment.lower(), "comment on class 1") + self.assertEqual(vocabulary.get_class_by_iri(iri("Class1")).label, "Class1") + self.assertEqual( + vocabulary.get_class_by_iri(iri("Class2")).get_label(), "Class2" + ) + self.assertEqual( + vocabulary.get_class_by_iri(iri("Class1")).comment.lower(), + "comment on class 1", + ) # subclassing - assertList(vocabulary.get_class_by_iri(iri("Class1")).parent_class_iris, - ["http://www.w3.org/2002/07/owl#Thing"]) + assertList( + vocabulary.get_class_by_iri(iri("Class1")).parent_class_iris, + ["http://www.w3.org/2002/07/owl#Thing"], + ) assertList( vocabulary.get_class_by_iri(iri("Class1a")).parent_class_iris, - [iri("Class1")]) + [iri("Class1")], + ) assertList( vocabulary.get_class_by_iri(iri("Class12")).parent_class_iris, - [iri("Class1"), iri("Class2")]) + [iri("Class1"), iri("Class2")], + ) # assertList( # vocabulary.get_class_by_iri(iri("Class12*")).parent_class_iris, # [iri("Class1"), iri("Class2")]) assertList( vocabulary.get_class_by_iri(iri("Class1b")).parent_class_iris, - [iri("Class1")]) + [iri("Class1")], + ) assertList( vocabulary.get_class_by_iri(iri("Class123")).parent_class_iris, - [iri("Class1"), iri("Class2"), iri("Class3")]) + [iri("Class1"), iri("Class2"), iri("Class3")], + ) assertList( vocabulary.get_class_by_iri(iri("Class1aa")).ancestor_class_iris, - ["http://www.w3.org/2002/07/owl#Thing", iri("Class1"), - iri("Class1a")]) + ["http://www.w3.org/2002/07/owl#Thing", iri("Class1"), iri("Class1a")], + ) - assertList(vocabulary.get_class_by_iri(iri("Class3")).child_class_iris, - [iri("Class3a"), iri("Class123"), iri("Class3aa"), - iri("Class13")]) + assertList( + vocabulary.get_class_by_iri(iri("Class3")).child_class_iris, + [iri("Class3a"), iri("Class123"), iri("Class3aa"), iri("Class13")], + ) # Relation Target statments cor1 = get_cor_with_prop(vocabulary, iri("Class1"), iri("objProp3")) - assertList(get_targets_for_combine_object_relation(vocabulary, cor1), - [iri("Class3"), iri("Class123"), iri("Class3a"), - iri("Class3aa"), iri("Class13")]) + assertList( + get_targets_for_combine_object_relation(vocabulary, cor1), + [ + iri("Class3"), + iri("Class123"), + iri("Class3a"), + iri("Class3aa"), + iri("Class13"), + ], + ) cor1 = get_cor_with_prop(vocabulary, iri("Class1"), iri("objProp2")) # assertList(get_targets_for_combine_object_relation(vocabulary, cor1), # [iri("Class12"), iri("Class12*"), iri("Class123")]) cor1 = get_cor_with_prop(vocabulary, iri("Class1"), iri("objProp4")) - assertList(get_targets_for_combine_object_relation(vocabulary, cor1), - [iri("Class123")]) + assertList( + get_targets_for_combine_object_relation(vocabulary, cor1), [iri("Class123")] + ) cor1 = get_cor_with_prop(vocabulary, iri("Class1"), iri("oProp1")) - assertList(get_targets_for_combine_object_relation(vocabulary, cor1), - [iri("Class4"), iri("Class2"), iri("Class12"), - iri("Class123")]) + assertList( + get_targets_for_combine_object_relation(vocabulary, cor1), + [iri("Class4"), iri("Class2"), iri("Class12"), iri("Class123")], + ) cor1 = get_cor_with_prop(vocabulary, iri("Class1"), iri("objProp5")) - assertList(get_targets_for_combine_object_relation(vocabulary, cor1), - [iri("Class12"), iri("Class123"), iri("Class13")]) - - target_str = get_cor_with_prop(vocabulary, iri("Class1"), - iri("objProp5")).get_relations( - vocabulary)[0].target_statement.to_string(vocabulary) + assertList( + get_targets_for_combine_object_relation(vocabulary, cor1), + [iri("Class12"), iri("Class123"), iri("Class13")], + ) + + target_str = ( + get_cor_with_prop(vocabulary, iri("Class1"), iri("objProp5")) + .get_relations(vocabulary)[0] + .target_statement.to_string(vocabulary) + ) self.assertEqual(target_str, "(Class1 and (Class2 or Class3))") # Individuals assertList( vocabulary.get_individual(iri("Individual1")).parent_class_iris, - [iri("Class1"), iri("Class2")]) + [iri("Class1"), iri("Class2")], + ) assertList( vocabulary.get_individual(iri("Individual2")).parent_class_iris, - [iri("Class1")]) + [iri("Class1")], + ) assertList( vocabulary.get_individual(iri("Individual3")).parent_class_iris, - [iri("Class1"), iri("Class2"), iri("Class3")]) + [iri("Class1"), iri("Class2"), iri("Class3")], + ) assertList( vocabulary.get_individual(iri("Individual4")).parent_class_iris, - [iri("Class1"), iri("Class2")]) + [iri("Class1"), iri("Class2")], + ) # obj properties self.assertIn(iri("oProp1"), vocabulary.object_properties) assertList( vocabulary.get_object_property(iri("oProp1")).inverse_property_iris, - [iri("objProp3")]) - assertList(vocabulary.get_object_property( - iri("objProp3")).inverse_property_iris, [iri("oProp1")]) + [iri("objProp3")], + ) + assertList( + vocabulary.get_object_property(iri("objProp3")).inverse_property_iris, + [iri("oProp1")], + ) # datatype_catalogue - assertList(vocabulary.get_datatype(iri("customDataType1")).enum_values, - ["0", "15", "30"]) - assertList(vocabulary.get_datatype(iri("customDataType4")).enum_values, - ["1", "2", "3", "4"]) - self.assertEqual(vocabulary.get_datatype( - iri("customDataType4")).type, DatatypeType.enum) + assertList( + vocabulary.get_datatype(iri("customDataType1")).enum_values, + ["0", "15", "30"], + ) + assertList( + vocabulary.get_datatype(iri("customDataType4")).enum_values, + ["1", "2", "3", "4"], + ) + self.assertEqual( + vocabulary.get_datatype(iri("customDataType4")).type, DatatypeType.enum + ) # data properties self.assertIn(iri("dataProp1"), vocabulary.data_properties) # value target statments cdr1 = get_cdr_with_prop(vocabulary, iri("Class3"), iri("dataProp1")) - assertList(cdr1.get_possible_enum_target_values(vocabulary), - ["1", "2", "3", "4"]) + assertList( + cdr1.get_possible_enum_target_values(vocabulary), ["1", "2", "3", "4"] + ) def tearDown(self) -> None: pass @@ -154,11 +191,11 @@ def get_cor_with_prop(vocabulary: Vocabulary, class_iri: str, prop_iri: str): return c assert False, "{} has no combined obj rel with property {}".format( - class_iri, prop_iri) + class_iri, prop_iri + ) -def get_cdr_with_prop(vocabulary: Vocabulary, class_iri: str, - prop_iri: str): +def get_cdr_with_prop(vocabulary: Vocabulary, class_iri: str, prop_iri: str): # cors = vocabulary.get_combined_object_relations_for_class(class_iri) class_ = vocabulary.get_class_by_iri(class_iri=class_iri) cdrs = class_.get_combined_data_relations(vocabulary) @@ -167,24 +204,24 @@ def get_cdr_with_prop(vocabulary: Vocabulary, class_iri: str, return c assert False, "{} has no combined data rel with property {}".format( - class_iri, prop_iri) + class_iri, prop_iri + ) def assertList(actual, expected): assert len(actual) == len(expected), actual - assert collections.Counter(actual) == collections.Counter( - expected), actual + assert collections.Counter(actual) == collections.Counter(expected), actual def iri(iriEnd: str): - base_iri = "http://www.semanticweb.org/redin/ontologies/2020/11/" \ - "untitled-ontology-25#" + base_iri = ( + "http://www.semanticweb.org/redin/ontologies/2020/11/" "untitled-ontology-25#" + ) return base_iri + iriEnd -def get_targets_for_combine_object_relation(vocabulary: Vocabulary, - com_obj_rel): +def get_targets_for_combine_object_relation(vocabulary: Vocabulary, com_obj_rel): possible_class_iris = set() for rel_id in com_obj_rel.relation_ids: @@ -195,6 +232,7 @@ def get_targets_for_combine_object_relation(vocabulary: Vocabulary, return list(possible_class_iris) + def get_file_path(path_end: str) -> str: """ Get the correct path to the file needed for this test @@ -204,4 +242,4 @@ def get_file_path(path_end: str) -> str: # Match the needed path to the config file in both cases path = Path(__file__).parent.resolve() - return str(path.joinpath(path_end)) \ No newline at end of file + return str(path.joinpath(path_end)) diff --git a/tests/semantics/test_vocabulary_configurator.py b/tests/semantics/test_vocabulary_configurator.py index 72a1f929..ae795c00 100644 --- a/tests/semantics/test_vocabulary_configurator.py +++ b/tests/semantics/test_vocabulary_configurator.py @@ -1,6 +1,7 @@ """ Tests for filip.semantics.vocabulary_configurator """ + import unittest from pathlib import Path @@ -22,41 +23,35 @@ def setUp(self) -> None: pascal_case_individual_labels=False, camel_case_property_labels=False, camel_case_datatype_labels=False, - pascal_case_datatype_enum_labels=False + pascal_case_datatype_enum_labels=False, ) ) - vocabulary_1 = \ - VocabularyConfigurator.add_ontology_to_vocabulary_as_file( - vocabulary=vocabulary, - path_to_file=self.get_file_path( - 'ontology_files/RoomFloorOntology.ttl')) + vocabulary_1 = VocabularyConfigurator.add_ontology_to_vocabulary_as_file( + vocabulary=vocabulary, + path_to_file=self.get_file_path("ontology_files/RoomFloorOntology.ttl"), + ) - with open(self.get_file_path( - 'ontology_files/RoomFloor_Duplicate_Labels.ttl'), 'r') \ - as file: + with open( + self.get_file_path("ontology_files/RoomFloor_Duplicate_Labels.ttl"), "r" + ) as file: data = file.read() - vocabulary_2 = \ - VocabularyConfigurator.add_ontology_to_vocabulary_as_string( - vocabulary=vocabulary_1, - source_name='RoomFloorOntology_Duplicate_Labels', - source_content=data - ) + vocabulary_2 = VocabularyConfigurator.add_ontology_to_vocabulary_as_string( + vocabulary=vocabulary_1, + source_name="RoomFloorOntology_Duplicate_Labels", + source_content=data, + ) - vocabulary_3 = \ - VocabularyConfigurator.add_ontology_to_vocabulary_as_file( - vocabulary=vocabulary, - path_to_file=self.get_file_path( - 'ontology_files/RoomFloorOntology.ttl'), - source_name='test_name' - ) - vocabulary_3 = \ - VocabularyConfigurator.add_ontology_to_vocabulary_as_file( - vocabulary=vocabulary_3, - path_to_file=self.get_file_path( - 'ontology_files/ParsingTesterOntology.ttl') - ) + vocabulary_3 = VocabularyConfigurator.add_ontology_to_vocabulary_as_file( + vocabulary=vocabulary, + path_to_file=self.get_file_path("ontology_files/RoomFloorOntology.ttl"), + source_name="test_name", + ) + vocabulary_3 = VocabularyConfigurator.add_ontology_to_vocabulary_as_file( + vocabulary=vocabulary_3, + path_to_file=self.get_file_path("ontology_files/ParsingTesterOntology.ttl"), + ) self.vocabulary = vocabulary self.vocabulary_1 = vocabulary_1 @@ -79,60 +74,63 @@ def test_vocabulary_composition(self): self.assertEqual(len(vocabulary_3.sources), 3) # test source names - self.assertIn('RoomFloorOntology', - [n.source_name for n in vocabulary_1.sources.values()]) + self.assertIn( + "RoomFloorOntology", [n.source_name for n in vocabulary_1.sources.values()] + ) - self.assertIn('RoomFloorOntology', - [n.source_name for n in vocabulary_2.sources.values()]) - self.assertIn('RoomFloorOntology_Duplicate_Labels', - [n.source_name for n in vocabulary_2.sources.values()]) + self.assertIn( + "RoomFloorOntology", [n.source_name for n in vocabulary_2.sources.values()] + ) + self.assertIn( + "RoomFloorOntology_Duplicate_Labels", + [n.source_name for n in vocabulary_2.sources.values()], + ) - self.assertIn('test_name', - [n.source_name for n in vocabulary_3.sources.values()]) + self.assertIn( + "test_name", [n.source_name for n in vocabulary_3.sources.values()] + ) # test content of vocabulary self.assertEqual(len(vocabulary_2.classes), 10) self.assertEqual(len(vocabulary_3.classes), 18) # test deletion of source - source_id = [s.id for s in vocabulary_3.sources.values() - if s.source_name == "test_name"][0] + source_id = [ + s.id for s in vocabulary_3.sources.values() if s.source_name == "test_name" + ][0] vocabulary_4 = VocabularyConfigurator.delete_source_from_vocabulary( - vocabulary=vocabulary_3, - source_id=source_id + vocabulary=vocabulary_3, source_id=source_id + ) + self.assertIn( + "test_name", [n.source_name for n in vocabulary_3.sources.values()] + ) + self.assertNotIn( + "test_name", [n.source_name for n in vocabulary_4.sources.values()] ) - self.assertIn('test_name', - [n.source_name for n in vocabulary_3.sources.values()]) - self.assertNotIn('test_name', - [n.source_name for n in vocabulary_4.sources.values()]) def test_duplicate_label_detection(self): conflict_voc = self.vocabulary_2 - conflict_summary = VocabularyConfigurator.\ - get_label_conflicts_in_vocabulary(conflict_voc) + conflict_summary = VocabularyConfigurator.get_label_conflicts_in_vocabulary( + conflict_voc + ) - self.assertIn('Sensor', conflict_summary.class_label_duplicates) - self.assertIn('isOnFloor', conflict_summary.field_label_duplicates) - self.assertIn('MeasurmentType', - conflict_summary.datatype_label_duplicates) + self.assertIn("Sensor", conflict_summary.class_label_duplicates) + self.assertIn("isOnFloor", conflict_summary.field_label_duplicates) + self.assertIn("MeasurmentType", conflict_summary.datatype_label_duplicates) - self.assertEqual(len(conflict_summary.class_label_duplicates[ - 'Sensor']), 2) + self.assertEqual(len(conflict_summary.class_label_duplicates["Sensor"]), 2) def test_valid_test(self): self.assertEqual( - VocabularyConfigurator.is_vocabulary_valid(self.vocabulary_2), - False + VocabularyConfigurator.is_vocabulary_valid(self.vocabulary_2), False ) self.assertEqual( - VocabularyConfigurator.is_vocabulary_valid(self.vocabulary_1), - True + VocabularyConfigurator.is_vocabulary_valid(self.vocabulary_1), True ) self.assertEqual( - VocabularyConfigurator.is_vocabulary_valid(self.vocabulary_3), - True + VocabularyConfigurator.is_vocabulary_valid(self.vocabulary_3), True ) def test_build_models(self): @@ -143,28 +141,33 @@ def test_build_models(self): def test_device_class(self): vocabulary = self.vocabulary_3 - class_thing = vocabulary.get_class_by_iri( - "http://www.w3.org/2002/07/owl#Thing") + class_thing = vocabulary.get_class_by_iri("http://www.w3.org/2002/07/owl#Thing") class_1 = vocabulary.get_class_by_iri( "http://www.semanticweb.org/redin/ontologies/2020/11/untitled" - "-ontology-25#Class1") + "-ontology-25#Class1" + ) class_2 = vocabulary.get_class_by_iri( "http://www.semanticweb.org/redin/ontologies/2020/11/untitled" - "-ontology-25#Class2") + "-ontology-25#Class2" + ) class_3 = vocabulary.get_class_by_iri( "http://www.semanticweb.org/redin/ontologies/2020/11/untitled" - "-ontology-25#Class3") + "-ontology-25#Class3" + ) class_123 = vocabulary.get_class_by_iri( "http://www.semanticweb.org/redin/ontologies/2020/11/untitled" - "-ontology-25#Class123") + "-ontology-25#Class123" + ) data_prop_1 = vocabulary.get_data_property( "http://www.semanticweb.org/redin/ontologies/2020/11/untitled" - "-ontology-25#dataProp1") + "-ontology-25#dataProp1" + ) data_prop_2 = vocabulary.get_data_property( "http://www.semanticweb.org/redin/ontologies/2020/11/untitled" - "-ontology-25#dataProp2") + "-ontology-25#dataProp2" + ) self.assertFalse(class_thing.is_iot_class(vocabulary)) self.assertFalse(class_1.is_iot_class(vocabulary)) diff --git a/tests/test_config.py b/tests/test_config.py index e526799b..d0770a7e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,7 @@ """ Test module for configuration functions """ + import os import unittest from filip.config import Settings @@ -12,15 +13,15 @@ class TestSettings(unittest.TestCase): """ Test case for loading settings """ + def setUp(self) -> None: # Test if the testcase was run directly or over in a global test-run. # Match the needed path to the config file in both cases if os.getcwd().split("\\")[-1] == "tests": - self.settings_parsing = Settings(_env_file='test_config.env') + self.settings_parsing = Settings(_env_file="test_config.env") else: - self.settings_parsing = \ - Settings(_env_file='./tests/test_config.env') + self.settings_parsing = Settings(_env_file="./tests/test_config.env") for key, value in json.loads(self.settings_parsing.model_dump_json()).items(): os.environ[key] = value @@ -33,9 +34,15 @@ def test_load_dotenv(self): Returns: None """ - self.assertEqual(str(self.settings_parsing.IOTA_URL), str(AnyHttpUrl("http://myHost:4041/"))) - self.assertEqual(str(self.settings_parsing.CB_URL), str(AnyHttpUrl("http://myHost:1026/"))) - self.assertEqual(str(self.settings_parsing.QL_URL), str(AnyHttpUrl("http://myHost:8668/"))) + self.assertEqual( + str(self.settings_parsing.IOTA_URL), str(AnyHttpUrl("http://myHost:4041/")) + ) + self.assertEqual( + str(self.settings_parsing.CB_URL), str(AnyHttpUrl("http://myHost:1026/")) + ) + self.assertEqual( + str(self.settings_parsing.QL_URL), str(AnyHttpUrl("http://myHost:8668/")) + ) def test_example_dotenv(self): """ diff --git a/tests/test_logging.py b/tests/test_logging.py index a0f75f78..254df559 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,6 +1,7 @@ """ Test module for logging functionality """ + import logging from unittest import TestCase @@ -9,9 +10,9 @@ class TestLoggingConfig(TestCase): """ Test case for logging functionality """ + def setUp(self) -> None: - self.logger = logging.getLogger( - name=f"{__package__}.{self.__class__.__name__}") + self.logger = logging.getLogger(name=f"{__package__}.{self.__class__.__name__}") def test_overwrite_config(self): """ @@ -23,12 +24,13 @@ def test_overwrite_config(self): # Try to set logging level before calling logging.basisConfig() # Since no handler is configured this will fail - self.logger = logging.getLogger( - name=f"{__package__}.{self.__class__.__name__}") - self.logger.warning("Trying to set log_level to '%s' via settings " - "before calling basicConfig. This will fail " - "because no handler is added to the logger", - logging.DEBUG) + self.logger = logging.getLogger(name=f"{__package__}.{self.__class__.__name__}") + self.logger.warning( + "Trying to set log_level to '%s' via settings " + "before calling basicConfig. This will fail " + "because no handler is added to the logger", + logging.DEBUG, + ) self.logger.setLevel(level=logging.DEBUG) # The next line will not show up! self.logger.info("Current LOG_LEVEL is '%s'", self.logger.level) @@ -37,17 +39,19 @@ def test_overwrite_config(self): # but adding a handler before. self.logger.handlers.clear() handler = logging.StreamHandler() - formatter = logging.Formatter(fmt='Custom Logging Stream %(asctime)s - ' - '%(name)s - ' - '%(levelname)s : %(message)s') + formatter = logging.Formatter( + fmt="Custom Logging Stream %(asctime)s - " + "%(name)s - " + "%(levelname)s : %(message)s" + ) handler.setFormatter(formatter) - self.logger.info("Set LOG_LEVEL to '%s' via settings and adding a " - "handler before. ", - logging.DEBUG) + self.logger.info( + "Set LOG_LEVEL to '%s' via settings and adding a " "handler before. ", + logging.DEBUG, + ) self.logger.addHandler(handler) self.logger.setLevel(level=logging.DEBUG) - self.logger.info("Current LOG_LEVEL has changed to '%s'", - self.logger.level) + self.logger.info("Current LOG_LEVEL has changed to '%s'", self.logger.level) # The next line will not show up! self.logger.debug("Current LOG_LEVEL is '%s'", self.logger.level) @@ -59,19 +63,20 @@ def test_overwrite_config(self): # '%(message)s') logger = logging.getLogger() handler = logging.StreamHandler() - formatter = logging.Formatter(fmt='Root Stream %(asctime)s - ' - '%(name)s - ' - '%(levelname)s : %(message)s') + formatter = logging.Formatter( + fmt="Root Stream %(asctime)s - " "%(name)s - " "%(levelname)s : %(message)s" + ) handler.setFormatter(formatter) logger.addHandler(handler) - logger.setLevel('DEBUG') - logger.debug('Initialize root logger') + logger.setLevel("DEBUG") + logger.debug("Initialize root logger") # This message will show up twice because now a handler is configured # in the root logger and all messages will be forwarded # but it does because the # loggers are detached and we need to delete the handler first - self.logger.info("Current LOG_LEVEL '%s' (this will appear twice)", - self.logger.level) + self.logger.info( + "Current LOG_LEVEL '%s' (this will appear twice)", self.logger.level + ) def tearDown(self) -> None: pass diff --git a/tests/utils/test_clear.py b/tests/utils/test_clear.py index 992ec20d..61422700 100644 --- a/tests/utils/test_clear.py +++ b/tests/utils/test_clear.py @@ -1,6 +1,7 @@ """ Tests clear functions in filip.utils.cleanup """ + import random import time import unittest @@ -13,13 +14,20 @@ from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient, QuantumLeapClient from filip.models.base import FiwareHeader, FiwareLDHeader from filip.models.ngsi_ld.context import ContextLDEntity, ActionTypeLD -from filip.models.ngsi_ld.subscriptions import SubscriptionLD, NotificationParams, \ - Endpoint +from filip.models.ngsi_ld.subscriptions import ( + SubscriptionLD, + NotificationParams, + Endpoint, +) from filip.models.ngsi_v2.context import ContextEntity from filip.models.ngsi_v2.iot import Device, ServiceGroup from filip.models.ngsi_v2.subscriptions import Subscription, Message -from filip.utils.cleanup import clear_context_broker, clear_iot_agent, clear_quantumleap, \ - clear_context_broker_ld +from filip.utils.cleanup import ( + clear_context_broker, + clear_iot_agent, + clear_quantumleap, + clear_context_broker_ld, +) from tests.config import settings @@ -34,85 +42,87 @@ def setUp(self) -> None: """ # use specific service for testing clear functions self.fiware_header = FiwareHeader( - service="filip_clear_test", - service_path=settings.FIWARE_SERVICEPATH) + service="filip_clear_test", service_path=settings.FIWARE_SERVICEPATH + ) self.cb_url = settings.CB_URL - self.cb_client = ContextBrokerClient(url=self.cb_url, - fiware_header=self.fiware_header) + self.cb_client = ContextBrokerClient( + url=self.cb_url, fiware_header=self.fiware_header + ) self.cb_client_ld = ContextBrokerLDClient( fiware_header=FiwareLDHeader(ngsild_tenant=settings.FIWARE_SERVICE), - url=settings.LD_CB_URL) + url=settings.LD_CB_URL, + ) self.iota_url = settings.IOTA_URL - self.iota_client = IoTAClient(url=self.iota_url, - fiware_header=self.fiware_header) + self.iota_client = IoTAClient( + url=self.iota_url, fiware_header=self.fiware_header + ) self.ql_url = settings.QL_URL - self.ql_client = QuantumLeapClient(url=self.ql_url, - fiware_header=self.fiware_header) + self.ql_client = QuantumLeapClient( + url=self.ql_url, fiware_header=self.fiware_header + ) self.sub_dict = { "description": "One subscription to rule them all", "subject": { - "entities": [ - { - "idPattern": ".*", - "type": "Room" - } - ], + "entities": [{"idPattern": ".*", "type": "Room"}], "condition": { - "attrs": [ - "temperature" - ], - "expression": { - "q": "temperature>40" - } - } + "attrs": ["temperature"], + "expression": {"q": "temperature>40"}, + }, }, "notification": { - "http": { - "url": "http://localhost:1234" - }, - "attrs": [ - "temperature", - "humidity" - ] + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], }, "expires": datetime.now(), - "throttling": 0 + "throttling": 0, } def test_clear_context_broker(self): """ Test for clearing context broker using context broker client """ - entity = ContextEntity(id=str(random.randint(1, 50)), - type=f'filip:object:Type') + entity = ContextEntity(id=str(random.randint(1, 50)), type=f"filip:object:Type") self.cb_client.post_entity(entity=entity) subscription = Subscription.model_validate(self.sub_dict) self.cb_client.post_subscription(subscription=subscription) clear_context_broker(cb_client=self.cb_client) - self.assertEqual(0, len(self.cb_client.get_entity_list()) or len(self.cb_client.get_subscription_list())) + self.assertEqual( + 0, + len(self.cb_client.get_entity_list()) + or len(self.cb_client.get_subscription_list()), + ) def test_clear_context_broker_ld(self): """ Test for clearing context broker LD using context broker client """ random_list = [random.randint(0, 100) for _ in range(10)] - entities = [ContextLDEntity(id=f"urn:ngsi-ld:clear_test:{str(i)}", - type='clear_test') for i in random_list] - self.cb_client_ld.entity_batch_operation(action_type=ActionTypeLD.CREATE, - entities=entities) - notification_param = NotificationParams(attributes=["attr"], - endpoint=Endpoint(**{ - "uri": urllib.parse.urljoin( - str(settings.LD_CB_URL), - "/ngsi-ld/v1/subscriptions"), - "accept": "application/json" - })) - sub = SubscriptionLD(id=f"urn:ngsi-ld:Subscription:clear_test:{random.randint(0, 100)}", - notification=notification_param, - entities=[{"type": "clear_test"}]) + entities = [ + ContextLDEntity(id=f"urn:ngsi-ld:clear_test:{str(i)}", type="clear_test") + for i in random_list + ] + self.cb_client_ld.entity_batch_operation( + action_type=ActionTypeLD.CREATE, entities=entities + ) + notification_param = NotificationParams( + attributes=["attr"], + endpoint=Endpoint( + **{ + "uri": urllib.parse.urljoin( + str(settings.LD_CB_URL), "/ngsi-ld/v1/subscriptions" + ), + "accept": "application/json", + } + ), + ) + sub = SubscriptionLD( + id=f"urn:ngsi-ld:Subscription:clear_test:{random.randint(0, 100)}", + notification=notification_param, + entities=[{"type": "clear_test"}], + ) self.cb_client_ld.post_subscription(subscription=sub) clear_context_broker_ld(cb_ld_client=self.cb_client_ld) self.assertEqual(0, len(self.cb_client_ld.get_entity_list())) @@ -122,22 +132,25 @@ def test_clear_context_broker_with_url(self): """ Test for clearing context broker using context broker url and fiware header as parameters """ - entity = ContextEntity(id=str(random.randint(1, 50)), - type=f'filip:object:Type') + entity = ContextEntity(id=str(random.randint(1, 50)), type=f"filip:object:Type") self.cb_client.post_entity(entity=entity) subscription = Subscription.model_validate(self.sub_dict) self.cb_client.post_subscription(subscription=subscription) clear_context_broker(url=self.cb_url, fiware_header=self.fiware_header) - self.assertEqual(0, len(self.cb_client.get_entity_list()) or len(self.cb_client.get_entity_list())) + self.assertEqual( + 0, + len(self.cb_client.get_entity_list()) + or len(self.cb_client.get_entity_list()), + ) def test_clear_iot_agent(self): """ Test for clearing iota using iota client """ - service_group = ServiceGroup(entity_type='Thing', - resource='/iot/json', - apikey=str(uuid4())) + service_group = ServiceGroup( + entity_type="Thing", resource="/iot/json", apikey=str(uuid4()) + ) device = { "device_id": "test_device", "service": self.fiware_header.service, @@ -151,15 +164,19 @@ def test_clear_iot_agent(self): clear_iot_agent(iota_client=self.iota_client) - self.assertEqual(0, len(self.iota_client.get_device_list()) or len(self.iota_client.get_group_list())) + self.assertEqual( + 0, + len(self.iota_client.get_device_list()) + or len(self.iota_client.get_group_list()), + ) def test_clear_iot_agent_url(self): """ Test for clearing iota using iota url and fiware header as parameters """ - service_group = ServiceGroup(entity_type='Thing', - resource='/iot/json', - apikey=str(uuid4())) + service_group = ServiceGroup( + entity_type="Thing", resource="/iot/json", apikey=str(uuid4()) + ) device = { "device_id": "test_device", "service": self.fiware_header.service, @@ -173,39 +190,44 @@ def test_clear_iot_agent_url(self): clear_iot_agent(url=self.iota_url, fiware_header=self.fiware_header) - self.assertEqual(0, len(self.iota_client.get_device_list()) or len(self.iota_client.get_group_list())) + self.assertEqual( + 0, + len(self.iota_client.get_device_list()) + or len(self.iota_client.get_group_list()), + ) def test_clear_quantumleap(self): from random import random clear_quantumleap(ql_client=self.ql_client) rec_numbs = 3 + def create_data_points(): def create_entities(_id) -> List[ContextEntity]: def create_attr(): - return {'temperature': {'value': random(), - 'type': 'Number'}, - 'humidity': {'value': random(), - 'type': 'Number'}, - 'co2': {'value': random(), - 'type': 'Number'}} + return { + "temperature": {"value": random(), "type": "Number"}, + "humidity": {"value": random(), "type": "Number"}, + "co2": {"value": random(), "type": "Number"}, + } - return [ContextEntity(id=f'Room:{_id}', type='Room', **create_attr())] + return [ContextEntity(id=f"Room:{_id}", type="Room", **create_attr())] fiware_header = self.fiware_header - with QuantumLeapClient(url=settings.QL_URL, fiware_header=fiware_header) \ - as client: + with QuantumLeapClient( + url=settings.QL_URL, fiware_header=fiware_header + ) as client: for i in range(rec_numbs): - notification_message = Message(data=create_entities(i), - subscriptionId="test") + notification_message = Message( + data=create_entities(i), subscriptionId="test" + ) client.post_notification(notification_message) create_data_points() time.sleep(2) self.assertEqual(len(self.ql_client.get_entities()), rec_numbs) - clear_quantumleap(url=self.ql_url, - fiware_header=self.fiware_header) + clear_quantumleap(url=self.ql_url, fiware_header=self.fiware_header) with self.assertRaises(RequestException): self.ql_client.get_entities() diff --git a/tests/utils/test_filter.py b/tests/utils/test_filter.py index c19b4067..246d74d9 100644 --- a/tests/utils/test_filter.py +++ b/tests/utils/test_filter.py @@ -1,6 +1,7 @@ """ Tests filter functions in filip.utils.filter """ + import unittest from datetime import datetime @@ -24,59 +25,48 @@ def setUp(self) -> None: None """ self.fiware_header = FiwareHeader( - service=settings.FIWARE_SERVICE, - service_path=settings.FIWARE_SERVICEPATH) + service=settings.FIWARE_SERVICE, service_path=settings.FIWARE_SERVICEPATH + ) self.url = settings.CB_URL - self.client = ContextBrokerClient(url=self.url, - fiware_header=self.fiware_header) - clear_all(fiware_header=self.fiware_header, - cb_url=self.url) - self.subscription = Subscription.model_validate({ - "description": "One subscription to rule them all", - "subject": { - "entities": [ - { - "idPattern": ".*", - "type": "Room" - } - ], - "condition": { - "attrs": [ - "temperature" - ], - "expression": { - "q": "temperature>40" - } - } - }, - "notification": { - "http": { - "url": "http://localhost:1234" + self.client = ContextBrokerClient( + url=self.url, fiware_header=self.fiware_header + ) + clear_all(fiware_header=self.fiware_header, cb_url=self.url) + self.subscription = Subscription.model_validate( + { + "description": "One subscription to rule them all", + "subject": { + "entities": [{"idPattern": ".*", "type": "Room"}], + "condition": { + "attrs": ["temperature"], + "expression": {"q": "temperature>40"}, + }, + }, + "notification": { + "http": {"url": "http://localhost:1234"}, + "attrs": ["temperature", "humidity"], }, - "attrs": [ - "temperature", - "humidity" - ] - }, - "expires": datetime.now(), - "throttling": 0 - }) - self.subscription.subject.entities[0] = EntityPattern(idPattern=".*", - type="Room") + "expires": datetime.now(), + "throttling": 0, + } + ) + self.subscription.subject.entities[0] = EntityPattern( + idPattern=".*", type="Room" + ) def test_filter_subscriptions_by_entity(self): subscription_1 = self.subscription.model_copy() self.client.post_subscription(subscription=subscription_1) subscription_2 = self.subscription.model_copy() - subscription_2.subject.entities[0] = EntityPattern(idPattern=".*", - type="Building") + subscription_2.subject.entities[0] = EntityPattern( + idPattern=".*", type="Building" + ) self.client.post_subscription(subscription=subscription_2) - filtered_sub = filter.filter_subscriptions_by_entity("test", - "Building", - self.url, - self.fiware_header) + filtered_sub = filter.filter_subscriptions_by_entity( + "test", "Building", self.url, self.fiware_header + ) self.assertGreater(len(filtered_sub), 0) def test_filter_device_list(self) -> None: @@ -105,7 +95,7 @@ def test_filter_device_list(self) -> None: service_path=settings.FIWARE_SERVICEPATH, device_id=device_id, entity_type=entity_type, - entity_name=entity_id + entity_name=entity_id, ) devices.append(device) @@ -116,65 +106,82 @@ def test_filter_device_list(self) -> None: self.assertEqual(len(filter.filter_device_list(devices)), len(devices)) # test with entity type - self.assertEqual(len(filter.filter_device_list( - devices, - entity_types=[entity_type_1])), - 10) - self.assertEqual(len(filter.filter_device_list( - devices, - entity_types=[entity_type_1, entity_type_2])), - 20) + self.assertEqual( + len(filter.filter_device_list(devices, entity_types=[entity_type_1])), 10 + ) + self.assertEqual( + len( + filter.filter_device_list( + devices, entity_types=[entity_type_1, entity_type_2] + ) + ), + 20, + ) # test with entity id - self.assertEqual(len(filter.filter_device_list( - devices, - entity_names=entity_id_list[5:])), - len(entity_id_list[5:])) + self.assertEqual( + len(filter.filter_device_list(devices, entity_names=entity_id_list[5:])), + len(entity_id_list[5:]), + ) # test with entity type and entity id - self.assertEqual(len(filter.filter_device_list( - devices, - entity_names=entity_id_list, - entity_types=[entity_type_1])), - 10) + self.assertEqual( + len( + filter.filter_device_list( + devices, entity_names=entity_id_list, entity_types=[entity_type_1] + ) + ), + 10, + ) # test with device id - self.assertEqual(len(filter.filter_device_list( - devices, device_ids=device_id_list[5:])), - len(device_id_list[5:])) + self.assertEqual( + len(filter.filter_device_list(devices, device_ids=device_id_list[5:])), + len(device_id_list[5:]), + ) # test with single args - self.assertEqual(len(filter.filter_device_list( - devices, - device_ids=devices[0].device_id)), 1) - - self.assertEqual(len(filter.filter_device_list( - devices, - entity_names=devices[0].entity_name)), 1) - - self.assertNotEqual(len(filter.filter_device_list( - devices, - entity_types=devices[0].entity_type)), 1) - - self.assertEqual(len(filter.filter_device_list( - devices, - device_ids=devices[0].device_id, - entity_names=devices[0].entity_name, - entity_types=devices[0].entity_type)), 1) + self.assertEqual( + len(filter.filter_device_list(devices, device_ids=devices[0].device_id)), 1 + ) + + self.assertEqual( + len( + filter.filter_device_list(devices, entity_names=devices[0].entity_name) + ), + 1, + ) + + self.assertNotEqual( + len( + filter.filter_device_list(devices, entity_types=devices[0].entity_type) + ), + 1, + ) + + self.assertEqual( + len( + filter.filter_device_list( + devices, + device_ids=devices[0].device_id, + entity_names=devices[0].entity_name, + entity_types=devices[0].entity_type, + ) + ), + 1, + ) # test for errors with self.assertRaises(TypeError): - filter.filter_device_list(devices, device_ids={'1234'}) + filter.filter_device_list(devices, device_ids={"1234"}) with self.assertRaises(TypeError): - filter.filter_device_list(devices, entity_names={'1234'}) + filter.filter_device_list(devices, entity_names={"1234"}) with self.assertRaises(TypeError): - filter.filter_device_list(devices, entity_types={'1234'}) - + filter.filter_device_list(devices, entity_types={"1234"}) def tearDown(self) -> None: """ Cleanup test server """ self.client.close() - clear_all(fiware_header=self.fiware_header, - cb_url=self.url) + clear_all(fiware_header=self.fiware_header, cb_url=self.url) diff --git a/tests/utils/test_simple_ql.py b/tests/utils/test_simple_ql.py index de5fa591..659e14ff 100644 --- a/tests/utils/test_simple_ql.py +++ b/tests/utils/test_simple_ql.py @@ -1,63 +1,58 @@ import unittest -from filip.utils.simple_ql import \ - QueryStatement, \ - Operator, \ - QueryString +from filip.utils.simple_ql import QueryStatement, Operator, QueryString class TestContextBroker(unittest.TestCase): def setUp(self) -> None: - self.left_hand_side = 'attr' + self.left_hand_side = "attr" self.numeric_right_hand_side = 20 self.string_right_hand_side = "'20'" def test_statements(self): for op in list(Operator): - QueryStatement(self.left_hand_side, op, - self.numeric_right_hand_side) - if op not in [Operator.EQUAL, - Operator.UNEQUAL, - Operator.MATCH_PATTERN]: - self.assertRaises(ValueError, QueryStatement, - self.left_hand_side, - op, self.string_right_hand_side) + QueryStatement(self.left_hand_side, op, self.numeric_right_hand_side) + if op not in [Operator.EQUAL, Operator.UNEQUAL, Operator.MATCH_PATTERN]: + self.assertRaises( + ValueError, + QueryStatement, + self.left_hand_side, + op, + self.string_right_hand_side, + ) def test_simple_query(self): # create queries for testing - test_query_string = '' + test_query_string = "" test_statements = [] test_tuples = [] for op in Operator.list(): - statement_string = ''.join( - [self.left_hand_side, op, str(self.numeric_right_hand_side)]) - test_query_string = ';'.join([test_query_string, statement_string]) + statement_string = "".join( + [self.left_hand_side, op, str(self.numeric_right_hand_side)] + ) + test_query_string = ";".join([test_query_string, statement_string]) test_statements.append( - QueryStatement(self.left_hand_side, op, - self.numeric_right_hand_side) - ) - test_tuples.append( - (self.left_hand_side, op, self.numeric_right_hand_side) + QueryStatement(self.left_hand_side, op, self.numeric_right_hand_side) ) - test_query_string = test_query_string.strip(';') + test_tuples.append((self.left_hand_side, op, self.numeric_right_hand_side)) + test_query_string = test_query_string.strip(";") query_from_statements = QueryString(qs=test_statements) query_from_tuples = QueryString(qs=test_tuples) query_from_string = QueryString.parse_str(test_query_string) # Test string conversion - self.assertEqual(str(query_from_statements), - query_from_statements.to_str()) + self.assertEqual(str(query_from_statements), query_from_statements.to_str()) self.assertEqual(str(query_from_tuples), query_from_tuples.to_str()) self.assertEqual(str(query_from_string), query_from_string.to_str()) # The implementation does not maintain order of statements. # Hence we compare sets of the different Methods. - set_from_test_string = set(test_query_string.split(';')) - set_from_statements = set(str(query_from_statements).split(';')) - set_from_tuples = set(str(query_from_tuples).split(';')) - set_from_string = set(str(query_from_string).split(';')) + set_from_test_string = set(test_query_string.split(";")) + set_from_statements = set(str(query_from_statements).split(";")) + set_from_tuples = set(str(query_from_tuples).split(";")) + set_from_string = set(str(query_from_string).split(";")) self.assertEqual(set_from_test_string, set_from_statements) self.assertEqual(set_from_test_string, set_from_tuples) self.assertEqual(set_from_test_string, set_from_string) diff --git a/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation.py b/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation.py index 3d4afef1..e3d797be 100644 --- a/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation.py +++ b/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation.py @@ -52,12 +52,14 @@ COM_STEP = 60 * 60 # 60 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + ) # define a list for storing historical data history_weather_station = [] @@ -76,7 +78,7 @@ def on_message(client, userdata, msg): Callback function for incoming messages """ # decode the payload - payload = msg.payload.decode('utf-8') + payload = msg.payload.decode("utf-8") # ToDo: Parse the payload using the `json` package and write it to # the history. ... @@ -91,12 +93,6 @@ def on_message(client, userdata, msg): mqtt_url = urlparse(MQTT_BROKER_URL) ... - - - - - - # ToDo: Print and subscribe to the weather station topic. print(f"WeatherStation topic:\n {TOPIC_WEATHER_STATION}") mqttc.subscribe(topic=TOPIC_WEATHER_STATION) @@ -107,14 +103,12 @@ def on_message(client, userdata, msg): # ToDo: Create a loop that publishes every 0.2 seconds a message to the broker # that holds the simulation time "t_sim" and the corresponding temperature # "t_amb". - for t_sim in range(sim_model.t_start, - int(sim_model.t_end + COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, int(sim_model.t_end + COM_STEP), int(COM_STEP) + ): # ToDo: Publish the simulated ambient temperature. ... - - # simulation step for next loop sim_model.do_step(int(t_sim + COM_STEP)) time.sleep(0.2) @@ -126,9 +120,9 @@ def on_message(client, userdata, msg): # plot results fig, ax = plt.subplots() - t_simulation = [item["t_sim"]/3600 for item in history_weather_station] + t_simulation = [item["t_sim"] / 3600 for item in history_weather_station] temperature = [item["t_amb"] for item in history_weather_station] ax.plot(t_simulation, temperature) - ax.set_xlabel('time in h') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in h") + ax.set_ylabel("ambient temperature in °C") plt.show() diff --git a/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation_solution.py b/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation_solution.py index 9e675ef0..a8bb9b88 100644 --- a/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation_solution.py +++ b/tutorials/ngsi_v2/e1_virtual_weatherstation/e1_virtual_weatherstation_solution.py @@ -52,19 +52,22 @@ COM_STEP = 60 * 60 # 60 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + ) # define a list for storing historical data history_weather_station = [] # ToDo: Create an MQTTv5 client with paho-mqtt. - mqttc = mqtt.Client(protocol=mqtt.MQTTv5, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2) + mqttc = mqtt.Client( + protocol=mqtt.MQTTv5, callback_api_version=mqtt.CallbackAPIVersion.VERSION2 + ) # set user data if required mqttc.username_pw_set(username=MQTT_USER, password=MQTT_PW) @@ -77,25 +80,26 @@ def on_message(client, userdata, msg): Callback function for incoming messages """ # decode the payload - payload = msg.payload.decode('utf-8') + payload = msg.payload.decode("utf-8") # ToDo: Parse the payload using the `json` package and write it to # the history. history_weather_station.append(json.loads(payload)) - # add your callback function to the client. You can either use a global # or a topic specific callback with `mqttc.message_callback_add()` mqttc.on_message = on_message # ToDo: Connect to the mqtt broker and subscribe to your topic. mqtt_url = urlparse(MQTT_BROKER_URL) - mqttc.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # ToDo: Print and subscribe to the weather station topic. print(f"WeatherStation topic:\n {TOPIC_WEATHER_STATION}") @@ -107,13 +111,14 @@ def on_message(client, userdata, msg): # ToDo: Create a loop that publishes every 0.2 seconds a message to the broker # that holds the simulation time "t_sim" and the corresponding temperature # "t_amb". - for t_sim in range(sim_model.t_start, - int(sim_model.t_end + COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, int(sim_model.t_end + COM_STEP), int(COM_STEP) + ): # ToDo: Publish the simulated ambient temperature. - mqttc.publish(topic=TOPIC_WEATHER_STATION, - payload=json.dumps({"t_amb": sim_model.t_amb, - "t_sim": sim_model.t_sim})) + mqttc.publish( + topic=TOPIC_WEATHER_STATION, + payload=json.dumps({"t_amb": sim_model.t_amb, "t_sim": sim_model.t_sim}), + ) # simulation step for next loop sim_model.do_step(int(t_sim + COM_STEP)) @@ -126,9 +131,9 @@ def on_message(client, userdata, msg): # plot results fig, ax = plt.subplots() - t_simulation = [item["t_sim"]/3600 for item in history_weather_station] + t_simulation = [item["t_sim"] / 3600 for item in history_weather_station] temperature = [item["t_amb"] for item in history_weather_station] ax.plot(t_simulation, temperature) - ax.set_xlabel('time in h') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in h") + ax.set_ylabel("ambient temperature in °C") plt.show() diff --git a/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck.py b/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck.py index 1ce7af44..436b86b0 100644 --- a/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck.py +++ b/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck.py @@ -15,12 +15,13 @@ """ # ## Import packages -from filip.clients.ngsi_v2 import \ - HttpClient, \ - HttpClientConfig, \ - ContextBrokerClient, \ - IoTAClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ( + HttpClient, + HttpClientConfig, + ContextBrokerClient, + IoTAClient, + QuantumLeapClient, +) # ## Parameters # ToDo: Enter your context broker url and port, e.g. http://localhost:1026. @@ -49,6 +50,8 @@ # ToDo: Create a multi client check again all services for their version. multic = HttpClient(config=config) - print(f"Multi Client (Context Broker): {multic.cb.get_version()}\n" - f"Multi Client (IoTA): {multic.iota.get_version()}\n" - f"Multi Client (Quantum Leap): {multic.timeseries.get_version()}") + print( + f"Multi Client (Context Broker): {multic.cb.get_version()}\n" + f"Multi Client (IoTA): {multic.iota.get_version()}\n" + f"Multi Client (Quantum Leap): {multic.timeseries.get_version()}" + ) diff --git a/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck_solution.py b/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck_solution.py index 9cda3daf..5846866a 100644 --- a/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck_solution.py +++ b/tutorials/ngsi_v2/e2_healthcheck/e2_healthcheck_solution.py @@ -15,12 +15,13 @@ """ # ## Import packages -from filip.clients.ngsi_v2 import \ - HttpClient, \ - HttpClientConfig, \ - ContextBrokerClient, \ - IoTAClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ( + HttpClient, + HttpClientConfig, + ContextBrokerClient, + IoTAClient, + QuantumLeapClient, +) # ## Parameters # ToDo: Enter your context broker url and port, e.g. http://localhost:1026. @@ -49,6 +50,8 @@ # ToDo: Create a multi client check again all services for their version. multic = HttpClient(config=config) - print(f"Multi Client (Context Broker): {multic.cb.get_version()}\n" - f"Multi Client (IoTA): {multic.iota.get_version()}\n" - f"Multi Client (Quantum Leap): {multic.timeseries.get_version()}") + print( + f"Multi Client (Context Broker): {multic.cb.get_version()}\n" + f"Multi Client (IoTA): {multic.iota.get_version()}\n" + f"Multi Client (Quantum Leap): {multic.timeseries.get_version()}" + ) diff --git a/tutorials/ngsi_v2/e3_context_entities/e3_context_entities.py b/tutorials/ngsi_v2/e3_context_entities/e3_context_entities.py index 509f9316..db268e9b 100644 --- a/tutorials/ngsi_v2/e3_context_entities/e3_context_entities.py +++ b/tutorials/ngsi_v2/e3_context_entities/e3_context_entities.py @@ -36,6 +36,7 @@ # ## Import packages import json from pathlib import Path + # filip imports from filip.clients.ngsi_v2 import ContextBrokerClient from filip.models import FiwareHeader @@ -52,9 +53,9 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Path to json-files to store entity data for follow up exercises, # e.g. ../e3_my_entities.json. Files that are used in exercises and files @@ -64,47 +65,30 @@ WRITE_ENTITIES_FILEPATH = Path("../e3_context_entities.json") # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_context_broker(url=CB_URL, fiware_header=fiware_header) # Create a context entity for a `building` following the smart data models # specifications - building = ContextEntity(id="urn:ngsi-ld:building:001", - type="Building") + building = ContextEntity(id="urn:ngsi-ld:building:001", type="Building") # create the property `category` to your building - category = NamedContextAttribute(name="category", - type="Array", - value=["office"]) + category = NamedContextAttribute(name="category", type="Array", value=["office"]) # ToDo: Create a property `address` for your building. Follow the full yaml # description in the specifications. It reuses the specification from # here: https://schema.org/PostalAddress - address = NamedContextAttribute(name="address", - type="PostalAddress", - value={...}) - - - - + address = NamedContextAttribute(name="address", type="PostalAddress", value={...}) # ToDo: Create a `description` property for your building. building_description = NamedContextAttribute(...) - - - - - # add all properties to your building using the # `add_attribute` function of your building object - building.add_attributes(attrs=[building_description, - category, - address]) + building.add_attributes(attrs=[building_description, category, address]) # ToDo: Create a context broker client and add the fiware_header. cbc = ... @@ -113,8 +97,7 @@ ... # Update your local building model with the one from the server - building = cbc.get_entity(entity_id=building.id, - entity_type=building.type) + building = cbc.get_entity(entity_id=building.id, entity_type=building.type) # print your `building model` as json print(f"This is your building model: \n {building.model_dump_json(indent=2)} \n") @@ -122,80 +105,67 @@ # ToDo: Create an `opening hours` property and add it to the building object # in the context broker. Do not update the whole entity! In real # scenarios it might have been modified by other users. - opening_hours = NamedContextAttribute(name="openingHours", - type="array", - value=[...]) - - - - + opening_hours = NamedContextAttribute( + name="openingHours", type="array", value=[...] + ) cbc.update_or_append_entity_attributes( - entity_id=building.id, - entity_type=building.type, - attrs=[opening_hours]) + entity_id=building.id, entity_type=building.type, attrs=[opening_hours] + ) # ToDo: Retrieve and print the property `opening hours`. hours = cbc.get_attribute_value(...) - print(f"Your opening hours: {hours} \n") # ToDo: Modify the property `opening hours` of the building. cbc.update_attribute_value(...) - - - - # ToDo: At this point you might have already noticed that your local # building model and the building model in the context broker are out of # sync. Hence, synchronize them again! - building = cbc.get_entity(entity_id=building.id, - entity_type=building.type) + building = cbc.get_entity(entity_id=building.id, entity_type=building.type) # print your building print(f"Your updated building model: \n {building.model_dump_json(indent=2)} \n") # ToDo: Create an entity of the thermal zone and add a description property # to it. - thermal_zone = ContextEntity(id="ThermalZone:001", - type="ThermalZone") + thermal_zone = ContextEntity(id="ThermalZone:001", type="ThermalZone") - thermal_zone_description = NamedContextAttribute(name="description", - type="Text", - value="This zones covers " - "the entire building") + thermal_zone_description = NamedContextAttribute( + name="description", + type="Text", + value="This zones covers " "the entire building", + ) thermal_zone.add_attributes(attrs=[thermal_zone_description]) # ToDo: Create and add a property that references your building model. Use the # `Relationship` for type and `refBuilding` for its name. - ref_building = NamedContextAttribute(name="refBuilding", - type="Relationship", - value=building.id) + ref_building = NamedContextAttribute( + name="refBuilding", type="Relationship", value=building.id + ) thermal_zone.add_attributes(attrs=[ref_building]) # print all relationships of your thermal zone for relationship in thermal_zone.get_relationships(): - print(f"Relationship properties of your thermal zone model: \n " - f"{relationship.model_dump_json(indent=2)} \n") + print( + f"Relationship properties of your thermal zone model: \n " + f"{relationship.model_dump_json(indent=2)} \n" + ) # ToDo: Post your thermal zone model to the context broker. ... ... - # ToDo: Create and add a property that references your thermal zone. Use the # `Relationship` for type and `hasZone` for its name. Make sure that # your local model and the server model are in sync afterwards. ref_zone = NamedContextAttribute(...) - cbc.update_or_append_entity_attributes(...) - - building = cbc.get_entity(entity_id=building.id, - entity_type=building.type) + building = cbc.get_entity(entity_id=building.id, entity_type=building.type) # ToDo: Create a filter request that retrieves all entities from the # server that have `refBuilding` attribute that reference your building @@ -205,8 +175,10 @@ # 2. Use the string in a context broker request and retrieve the entities. query = QueryString(qs=("refBuilding", "==", building.id)) for entity in cbc.get_entity_list(q=query): - print(f"All entities referencing the building: " - f"\n {entity.model_dump_json(indent=2)}\n") + print( + f"All entities referencing the building: " + f"\n {entity.model_dump_json(indent=2)}\n" + ) # ToDo: Create a filter request that retrieves all entities from the # server that have `hasZone` attribute that reference your thermal zone @@ -216,14 +188,17 @@ # 2. Use the string in a context broker request and retrieve the entities. query = ... for entity in cbc.get_entity_list(q=query): - print(f"All entities referencing the thermal zone: " - f"\n {entity.model_dump_json(indent=2)} \n") + print( + f"All entities referencing the thermal zone: " + f"\n {entity.model_dump_json(indent=2)} \n" + ) # write entities to file and clear server state - assert WRITE_ENTITIES_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_ENTITIES_FILEPATH.suffix}" + assert ( + WRITE_ENTITIES_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_ENTITIES_FILEPATH.suffix}" WRITE_ENTITIES_FILEPATH.touch(exist_ok=True) - with WRITE_ENTITIES_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_ENTITIES_FILEPATH.open("w", encoding="utf-8") as f: entities = [item.model_dump() for item in cbc.get_entity_list()] json.dump(entities, f, ensure_ascii=False, indent=2) diff --git a/tutorials/ngsi_v2/e3_context_entities/e3_context_entities_solution.py b/tutorials/ngsi_v2/e3_context_entities/e3_context_entities_solution.py index 69cb7d1f..0e97d87b 100644 --- a/tutorials/ngsi_v2/e3_context_entities/e3_context_entities_solution.py +++ b/tutorials/ngsi_v2/e3_context_entities/e3_context_entities_solution.py @@ -36,6 +36,7 @@ # ## Import packages import json from pathlib import Path + # filip imports from filip.clients.ngsi_v2 import ContextBrokerClient from filip.models import FiwareHeader @@ -52,9 +53,9 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Path to json-files to store entity data for follow up exercises, # e.g. ../e3_my_entities.json. Files that are used in exercises and files @@ -64,47 +65,43 @@ WRITE_ENTITIES_FILEPATH = Path("../e3_context_entities_solution_entities.json") # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_context_broker(url=CB_URL, fiware_header=fiware_header) # Create a context entity for a `building` following the smart data models # specifications - building = ContextEntity(id="urn:ngsi-ld:building:001", - type="Building") + building = ContextEntity(id="urn:ngsi-ld:building:001", type="Building") # create the property `category` to your building - category = NamedContextAttribute(name="category", - type="Array", - value=["office"]) + category = NamedContextAttribute(name="category", type="Array", value=["office"]) # ToDo: Create a property `address` for your building. Follow the full yaml # description in the specifications. It reuses the specification from # here: https://schema.org/PostalAddress - address = NamedContextAttribute(name="address", - type="PostalAddress", - value={ - "addressCountry": "DE", - "addressLocality": "Any City", - "postalCode": "12345", - "streetAddress": "Any Street 5" - }) + address = NamedContextAttribute( + name="address", + type="PostalAddress", + value={ + "addressCountry": "DE", + "addressLocality": "Any City", + "postalCode": "12345", + "streetAddress": "Any Street 5", + }, + ) # ToDo: Create a `description` property for your building. - building_description = NamedContextAttribute(name="description", - type="Text", - value="Small office building " - "with good insulation " - "standard") + building_description = NamedContextAttribute( + name="description", + type="Text", + value="Small office building " "with good insulation " "standard", + ) # add all properties to your building using the # `add_attribute` function of your building object - building.add_attributes(attrs=[building_description, - category, - address]) + building.add_attributes(attrs=[building_description, category, address]) # ToDo: Create a context broker client and add the fiware_header. cbc = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) @@ -113,8 +110,7 @@ cbc.post_entity(entity=building) # Update your local building model with the one from the server - building = cbc.get_entity(entity_id=building.id, - entity_type=building.type) + building = cbc.get_entity(entity_id=building.id, entity_type=building.type) # print your `building model` as json print(f"This is your building model: \n {building.model_dump_json(indent=2)} \n") @@ -122,80 +118,79 @@ # ToDo: Create an `opening hours` property and add it to the building object # in the context broker. Do not update the whole entity! In real # scenarios it might have been modified by other users. - opening_hours = NamedContextAttribute(name="openingHours", - type="array", - value=[ - "Mo-Fr 10:00-19:00", - "Sa closed", - "Su closed" - ]) + opening_hours = NamedContextAttribute( + name="openingHours", + type="array", + value=["Mo-Fr 10:00-19:00", "Sa closed", "Su closed"], + ) cbc.update_or_append_entity_attributes( - entity_id=building.id, - entity_type=building.type, - attrs=[opening_hours]) + entity_id=building.id, entity_type=building.type, attrs=[opening_hours] + ) # ToDo: Retrieve and print the property `opening hours`. - hours = cbc.get_attribute_value(entity_id=building.id, - entity_type=building.type, - attr_name=opening_hours.name) + hours = cbc.get_attribute_value( + entity_id=building.id, entity_type=building.type, attr_name=opening_hours.name + ) print(f"Your opening hours: {hours} \n") # ToDo: Modify the property `opening hours` of the building. - cbc.update_attribute_value(entity_id=building.id, - entity_type=building.type, - attr_name=opening_hours.name, - value=["Mo-Sa 10:00-19:00", - "Su closed"]) + cbc.update_attribute_value( + entity_id=building.id, + entity_type=building.type, + attr_name=opening_hours.name, + value=["Mo-Sa 10:00-19:00", "Su closed"], + ) # ToDo: At this point you might have already noticed that your local # building model and the building model in the context broker are out of # sync. Hence, synchronize them again! - building = cbc.get_entity(entity_id=building.id, - entity_type=building.type) + building = cbc.get_entity(entity_id=building.id, entity_type=building.type) # print your building print(f"Your updated building model: \n {building.model_dump_json(indent=2)} \n") # ToDo: Create an entity of the thermal zone and add a description property # to it. - thermal_zone = ContextEntity(id="ThermalZone:001", - type="ThermalZone") + thermal_zone = ContextEntity(id="ThermalZone:001", type="ThermalZone") - thermal_zone_description = NamedContextAttribute(name="description", - type="Text", - value="This zones covers " - "the entire building") + thermal_zone_description = NamedContextAttribute( + name="description", + type="Text", + value="This zones covers " "the entire building", + ) thermal_zone.add_attributes(attrs=[thermal_zone_description]) # ToDo: Create and add a property that references your building model. Use the # `Relationship` for type and `refBuilding` for its name. - ref_building = NamedContextAttribute(name="refBuilding", - type="Relationship", - value=building.id) + ref_building = NamedContextAttribute( + name="refBuilding", type="Relationship", value=building.id + ) thermal_zone.add_attributes(attrs=[ref_building]) # print all relationships of your thermal zone for relationship in thermal_zone.get_relationships(): - print(f"Relationship properties of your thermal zone model: \n " - f"{relationship.model_dump_json(indent=2)} \n") + print( + f"Relationship properties of your thermal zone model: \n " + f"{relationship.model_dump_json(indent=2)} \n" + ) # ToDo: Post your thermal zone model to the context broker. cbc.post_entity(entity=thermal_zone) - thermal_zone = cbc.get_entity(entity_id=thermal_zone.id, - entity_type=thermal_zone.type) + thermal_zone = cbc.get_entity( + entity_id=thermal_zone.id, entity_type=thermal_zone.type + ) # ToDo: Create and add a property that references your thermal zone. Use the # `Relationship` for type and `hasZone` for its name. Make sure that # your local model and the server model are in sync afterwards. - ref_zone = NamedContextAttribute(name="hasZone", - type="Relationship", - value=thermal_zone.id) - cbc.update_or_append_entity_attributes(entity_id=building.id, - entity_type=building.type, - attrs=[ref_zone]) - building = cbc.get_entity(entity_id=building.id, - entity_type=building.type) + ref_zone = NamedContextAttribute( + name="hasZone", type="Relationship", value=thermal_zone.id + ) + cbc.update_or_append_entity_attributes( + entity_id=building.id, entity_type=building.type, attrs=[ref_zone] + ) + building = cbc.get_entity(entity_id=building.id, entity_type=building.type) # ToDo: Create a filter request that retrieves all entities from the # server that have `refBuilding` attribute that reference your building @@ -205,8 +200,10 @@ # 2. Use the string in a context broker request and retrieve the entities. query = QueryString(qs=("refBuilding", "==", building.id)) for entity in cbc.get_entity_list(q=query): - print(f"All entities referencing the building: " - f"\n {entity.model_dump_json(indent=2)}\n") + print( + f"All entities referencing the building: " + f"\n {entity.model_dump_json(indent=2)}\n" + ) # ToDo: Create a filter request that retrieves all entities from the # server that have `hasZone` attribute that reference your thermal zone @@ -216,14 +213,17 @@ # 2. Use the string in a context broker request and retrieve the entities. query = QueryString(qs=("hasZone", "==", thermal_zone.id)) for entity in cbc.get_entity_list(q=query): - print(f"All entities referencing the thermal zone: " - f"\n {entity.model_dump_json(indent=2)} \n") + print( + f"All entities referencing the thermal zone: " + f"\n {entity.model_dump_json(indent=2)} \n" + ) # write entities to file and clear server state - assert WRITE_ENTITIES_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_ENTITIES_FILEPATH.suffix}" + assert ( + WRITE_ENTITIES_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_ENTITIES_FILEPATH.suffix}" WRITE_ENTITIES_FILEPATH.touch(exist_ok=True) - with WRITE_ENTITIES_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_ENTITIES_FILEPATH.open("w", encoding="utf-8") as f: entities = [item.model_dump() for item in cbc.get_entity_list()] json.dump(entities, f, ensure_ascii=False, indent=2) diff --git a/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors.py b/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors.py index a1f80c7a..8d0a6ef2 100644 --- a/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors.py +++ b/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors.py @@ -36,6 +36,7 @@ from filip.models.base import FiwareHeader from filip.models.ngsi_v2.iot import Device, DeviceAttribute, ServiceGroup from filip.utils.cleanup import clear_context_broker, clear_iot_agent + # import simulation model from tutorials.ngsi_v2.simulation_model import SimulationModel @@ -55,14 +56,14 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" # path to json-files to device configuration data for follow-up exercises WRITE_GROUPS_FILEPATH = Path("../e4_iot_thermal_zone_sensors_groups.json") @@ -78,53 +79,51 @@ COM_STEP = 60 * 60 * 0.25 # 15 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN, - temp_start=TEMPERATURE_ZONE_START) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + temp_start=TEMPERATURE_ZONE_START, + ) # define lists to store historical data history_weather_station = [] history_zone_temperature_sensor = [] # create a service group with your api key - service_group = ServiceGroup(apikey=APIKEY, - resource="/iot/json") + service_group = ServiceGroup(apikey=APIKEY, resource="/iot/json") # ToDo: Create two IoTA-MQTT devices for the weather station and the zone # temperature sensor. Also add the simulation time as `active attribute` # to each device! # create the weather station device # create the `sim_time` attribute and add it to the weather station's attributes - t_sim = DeviceAttribute(name='sim_time', - object_id='t_sim', - type="Number") - - weather_station = Device(device_id='device:001', - entity_name='urn:ngsi-ld:WeatherStation:001', - entity_type='WeatherStation', - protocol='IoTA-JSON', - transport='MQTT', - apikey=APIKEY, - attributes=[t_sim], - commands=[]) + t_sim = DeviceAttribute(name="sim_time", object_id="t_sim", type="Number") + + weather_station = Device( + device_id="device:001", + entity_name="urn:ngsi-ld:WeatherStation:001", + entity_type="WeatherStation", + protocol="IoTA-JSON", + transport="MQTT", + apikey=APIKEY, + attributes=[t_sim], + commands=[], + ) # create a temperature attribute and add it via the api of the # `device`-model. Use the `t_amb` as `object_id`. `object_id` specifies # what key will be used in the MQTT Message payload - t_amb = DeviceAttribute(name='temperature', - object_id='t_amb', - type="Number") + t_amb = DeviceAttribute(name="temperature", object_id="t_amb", type="Number") weather_station.add_attribute(t_amb) @@ -132,12 +131,6 @@ # creation. zone_temperature_sensor = Device(...) - - - - - - # ToDo: Create the temperature attribute. Use the `t_zone` as `object_id`. # `object_id` specifies what key will be used in the MQTT Message payload. t_zone = DeviceAttribute(...) @@ -159,7 +152,9 @@ # devices were correctly created. cbc = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # get weather station entity - print(f"Weather station:\n{cbc.get_entity(weather_station.entity_name).model_dump_json(indent=2)}") + print( + f"Weather station:\n{cbc.get_entity(weather_station.entity_name).model_dump_json(indent=2)}" + ) # ToDo: Get zone temperature sensor entity. print(...) @@ -184,12 +179,6 @@ # ToDO: Connect to the MQTT broker and subscribe to your topic. ... - - - - - - # subscribe to topics # subscribe to all incoming command topics for the registered devices mqttc.subscribe() @@ -200,19 +189,18 @@ # to the broker that holds the simulation time `sim_time` and the # corresponding temperature `temperature`. You may use the `object_id` # or the attribute name as a key in your payload. - for t_sim in range(sim_model.t_start, - sim_model.t_end + int(COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, sim_model.t_end + int(COM_STEP), int(COM_STEP) + ): # publish the simulated ambient temperature - mqttc.publish(device_id=weather_station.device_id, - payload={"temperature": sim_model.t_amb, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=weather_station.device_id, + payload={"temperature": sim_model.t_amb, "sim_time": sim_model.t_sim}, + ) # ToDo: Publish the simulated zone temperature. ... - - # simulation step for the next loop sim_model.do_step(int(t_sim + COM_STEP)) # wait for one second before publishing the next values @@ -221,23 +209,21 @@ # get corresponding entities and store the data weather_station_entity = cbc.get_entity( entity_id=weather_station.entity_name, - entity_type=weather_station.entity_type + entity_type=weather_station.entity_type, ) # append the data to the local history history_weather_station.append( - {"sim_time": weather_station_entity.sim_time.value, - "temperature": weather_station_entity.temperature.value}) + { + "sim_time": weather_station_entity.sim_time.value, + "temperature": weather_station_entity.temperature.value, + } + ) # ToDo: Get zone temperature sensor and store the data. zone_temperature_sensor_entity = ... - - - history_zone_temperature_sensor.append(...) - - # close the mqtt listening thread mqttc.loop_stop() # disconnect the mqtt device @@ -245,20 +231,19 @@ # plot the results fig, ax = plt.subplots() - t_simulation = [item["sim_time"]/3600 for item in history_weather_station] + t_simulation = [item["sim_time"] / 3600 for item in history_weather_station] temperature = [item["temperature"] for item in history_weather_station] ax.plot(t_simulation, temperature) ax.title.set_text("Weather Station") - ax.set_xlabel('time in h') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in h") + ax.set_ylabel("ambient temperature in °C") fig2, ax2 = plt.subplots() - t_simulation = [item["sim_time"]/3600 for item in history_zone_temperature_sensor] - temperature = [item["temperature"] for item in - history_zone_temperature_sensor] + t_simulation = [item["sim_time"] / 3600 for item in history_zone_temperature_sensor] + temperature = [item["temperature"] for item in history_zone_temperature_sensor] ax2.plot(t_simulation, temperature) ax2.title.set_text("Zone Temperature Sensor") - ax2.set_xlabel('time in h') - ax2.set_ylabel('zone temperature in °C') + ax2.set_xlabel("time in h") + ax2.set_ylabel("zone temperature in °C") plt.show() diff --git a/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors_solution.py b/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors_solution.py index 74e7de09..432dce13 100644 --- a/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors_solution.py +++ b/tutorials/ngsi_v2/e4_iot_thermal_zone_sensors/e4_iot_thermal_zone_sensors_solution.py @@ -36,6 +36,7 @@ from filip.models.base import FiwareHeader from filip.models.ngsi_v2.iot import Device, DeviceAttribute, ServiceGroup from filip.utils.cleanup import clear_context_broker, clear_iot_agent + # import simulation model from tutorials.ngsi_v2.simulation_model import SimulationModel @@ -55,14 +56,14 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" # path to json-files to device configuration data for follow-up exercises WRITE_GROUPS_FILEPATH = Path("../e4_iot_thermal_zone_sensors_solution_groups.json") @@ -78,72 +79,70 @@ COM_STEP = 60 * 60 * 0.25 # 15 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN, - temp_start=TEMPERATURE_ZONE_START) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + temp_start=TEMPERATURE_ZONE_START, + ) # define lists to store historical data history_weather_station = [] history_zone_temperature_sensor = [] # create a service group with your api key - service_group = ServiceGroup(apikey=APIKEY, - resource="/iot/json") + service_group = ServiceGroup(apikey=APIKEY, resource="/iot/json") # ToDo: Create two IoTA-MQTT devices for the weather station and the zone # temperature sensor. Also add the simulation time as `active attribute` # to each device! # create the weather station device # create the `sim_time` attribute and add it to the weather station's attributes - t_sim = DeviceAttribute(name='sim_time', - object_id='t_sim', - type="Number") - - weather_station = Device(device_id='device:001', - entity_name='urn:ngsi-ld:WeatherStation:001', - entity_type='WeatherStation', - protocol='IoTA-JSON', - transport='MQTT', - apikey=APIKEY, - attributes=[t_sim], - commands=[]) + t_sim = DeviceAttribute(name="sim_time", object_id="t_sim", type="Number") + + weather_station = Device( + device_id="device:001", + entity_name="urn:ngsi-ld:WeatherStation:001", + entity_type="WeatherStation", + protocol="IoTA-JSON", + transport="MQTT", + apikey=APIKEY, + attributes=[t_sim], + commands=[], + ) # create a temperature attribute and add it via the api of the # `device`-model. Use the `t_amb` as `object_id`. `object_id` specifies # what key will be used in the MQTT Message payload - t_amb = DeviceAttribute(name='temperature', - object_id='t_amb', - type="Number") + t_amb = DeviceAttribute(name="temperature", object_id="t_amb", type="Number") weather_station.add_attribute(t_amb) # ToDo: Create the zone temperature device and add the `t_sim` attribute upon # creation. - zone_temperature_sensor = Device(device_id='device:002', - entity_name='urn:ngsi-ld:TemperatureSensor:001', - entity_type='TemperatureSensor', - protocol='IoTA-JSON', - transport='MQTT', - apikey=APIKEY, - attributes=[t_sim], - commands=[]) + zone_temperature_sensor = Device( + device_id="device:002", + entity_name="urn:ngsi-ld:TemperatureSensor:001", + entity_type="TemperatureSensor", + protocol="IoTA-JSON", + transport="MQTT", + apikey=APIKEY, + attributes=[t_sim], + commands=[], + ) # ToDo: Create the temperature attribute. Use the `t_zone` as `object_id`. # `object_id` specifies what key will be used in the MQTT Message payload. - t_zone = DeviceAttribute(name='temperature', - object_id='t_zone', - type="Number") + t_zone = DeviceAttribute(name="temperature", object_id="t_zone", type="Number") zone_temperature_sensor.add_attribute(t_zone) @@ -162,9 +161,13 @@ # devices were correctly created. cbc = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # get weather station entity - print(f"Weather station:\n{cbc.get_entity(weather_station.entity_name).model_dump_json(indent=2)}") + print( + f"Weather station:\n{cbc.get_entity(weather_station.entity_name).model_dump_json(indent=2)}" + ) # ToDo: Get zone temperature sensor entity. - print(f"Zone temperature sensor:\n{cbc.get_entity(zone_temperature_sensor.entity_name).model_dump_json(indent=2)}") + print( + f"Zone temperature sensor:\n{cbc.get_entity(zone_temperature_sensor.entity_name).model_dump_json(indent=2)}" + ) # ToDo: Create an MQTTv5 client using filip.clients.mqtt.IoTAMQTTClient. mqttc = IoTAMQTTClient(protocol=mqtt.MQTTv5) @@ -188,13 +191,15 @@ # ToDO: Connect to the MQTT broker and subscribe to your topic. mqtt_url = urlparse(MQTT_BROKER_URL) - mqttc.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # subscribe to topics # subscribe to all incoming command topics for the registered devices mqttc.subscribe() @@ -205,18 +210,20 @@ # to the broker that holds the simulation time `sim_time` and the # corresponding temperature `temperature`. You may use the `object_id` # or the attribute name as a key in your payload. - for t_sim in range(sim_model.t_start, - sim_model.t_end + int(COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, sim_model.t_end + int(COM_STEP), int(COM_STEP) + ): # publish the simulated ambient temperature - mqttc.publish(device_id=weather_station.device_id, - payload={"temperature": sim_model.t_amb, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=weather_station.device_id, + payload={"temperature": sim_model.t_amb, "sim_time": sim_model.t_sim}, + ) # ToDo: Publish the simulated zone temperature. - mqttc.publish(device_id=zone_temperature_sensor.device_id, - payload={"temperature": sim_model.t_zone, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=zone_temperature_sensor.device_id, + payload={"temperature": sim_model.t_zone, "sim_time": sim_model.t_sim}, + ) # simulation step for the next loop sim_model.do_step(int(t_sim + COM_STEP)) @@ -226,21 +233,27 @@ # get corresponding entities and store the data weather_station_entity = cbc.get_entity( entity_id=weather_station.entity_name, - entity_type=weather_station.entity_type + entity_type=weather_station.entity_type, ) # append the data to the local history history_weather_station.append( - {"sim_time": weather_station_entity.sim_time.value, - "temperature": weather_station_entity.temperature.value}) + { + "sim_time": weather_station_entity.sim_time.value, + "temperature": weather_station_entity.temperature.value, + } + ) # ToDo: Get zone temperature sensor and store the data. zone_temperature_sensor_entity = cbc.get_entity( entity_id=zone_temperature_sensor.entity_name, - entity_type=zone_temperature_sensor.entity_type + entity_type=zone_temperature_sensor.entity_type, ) history_zone_temperature_sensor.append( - {"sim_time": zone_temperature_sensor_entity.sim_time.value, - "temperature": zone_temperature_sensor_entity.temperature.value}) + { + "sim_time": zone_temperature_sensor_entity.sim_time.value, + "temperature": zone_temperature_sensor_entity.temperature.value, + } + ) # close the mqtt listening thread mqttc.loop_stop() @@ -249,36 +262,37 @@ # plot the results fig, ax = plt.subplots() - t_simulation = [item["sim_time"]/3600 for item in history_weather_station] + t_simulation = [item["sim_time"] / 3600 for item in history_weather_station] temperature = [item["temperature"] for item in history_weather_station] ax.plot(t_simulation, temperature) ax.title.set_text("Weather Station") - ax.set_xlabel('time in h') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in h") + ax.set_ylabel("ambient temperature in °C") fig2, ax2 = plt.subplots() - t_simulation = [item["sim_time"]/3600 for item in history_zone_temperature_sensor] - temperature = [item["temperature"] for item in - history_zone_temperature_sensor] + t_simulation = [item["sim_time"] / 3600 for item in history_zone_temperature_sensor] + temperature = [item["temperature"] for item in history_zone_temperature_sensor] ax2.plot(t_simulation, temperature) ax2.title.set_text("Zone Temperature Sensor") - ax2.set_xlabel('time in h') - ax2.set_ylabel('zone temperature in °C') + ax2.set_xlabel("time in h") + ax2.set_ylabel("zone temperature in °C") plt.show() # write devices and groups to file and clear server state - assert WRITE_DEVICES_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_DEVICES_FILEPATH.suffix}" + assert ( + WRITE_DEVICES_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_DEVICES_FILEPATH.suffix}" WRITE_DEVICES_FILEPATH.touch(exist_ok=True) - with WRITE_DEVICES_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_DEVICES_FILEPATH.open("w", encoding="utf-8") as f: devices = [item.model_dump() for item in iotac.get_device_list()] json.dump(devices, f, ensure_ascii=False, indent=2) - assert WRITE_GROUPS_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_GROUPS_FILEPATH.suffix}" + assert ( + WRITE_GROUPS_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_GROUPS_FILEPATH.suffix}" WRITE_GROUPS_FILEPATH.touch(exist_ok=True) - with WRITE_GROUPS_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_GROUPS_FILEPATH.open("w", encoding="utf-8") as f: groups = [item.model_dump() for item in iotac.get_group_list()] json.dump(groups, f, ensure_ascii=False, indent=2) diff --git a/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control.py b/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control.py index ff250032..673b3821 100644 --- a/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control.py +++ b/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control.py @@ -47,13 +47,15 @@ from filip.models.base import DataType, FiwareHeader from filip.models.ngsi_v2.context import NamedCommand from filip.models.ngsi_v2.subscriptions import Subscription, Message -from filip.models.ngsi_v2.iot import \ - Device, \ - DeviceAttribute, \ - DeviceCommand, \ - PayloadProtocol, \ - ServiceGroup +from filip.models.ngsi_v2.iot import ( + Device, + DeviceAttribute, + DeviceCommand, + PayloadProtocol, + ServiceGroup, +) from filip.utils.cleanup import clear_context_broker, clear_iot_agent + # import simulation model from tutorials.ngsi_v2.simulation_model import SimulationModel @@ -75,33 +77,29 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" UNIQUE_ID = str(uuid4()) TOPIC_CONTROLLER = f"fiware_workshop/{UNIQUE_ID}/controller" print(TOPIC_CONTROLLER) # path to json-files to store entity data for follow-up exercises -WRITE_GROUPS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_groups.json") -WRITE_DEVICES_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_devices.json") -WRITE_SUBSCRIPTIONS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_subscriptions.json") +WRITE_GROUPS_FILEPATH = Path("../e5_iot_thermal_zone_control_groups.json") +WRITE_DEVICES_FILEPATH = Path("../e5_iot_thermal_zone_control_devices.json") +WRITE_SUBSCRIPTIONS_FILEPATH = Path("../e5_iot_thermal_zone_control_subscriptions.json") # path to read json-files from previous exercises -READ_GROUPS_FILEPATH = \ - Path("../e4_iot_thermal_zone_sensors_groups.json") -READ_DEVICES_FILEPATH = \ - Path("../e4_iot_thermal_zone_sensors_devices.json") +READ_GROUPS_FILEPATH = Path("../e4_iot_thermal_zone_sensors_groups.json") +READ_DEVICES_FILEPATH = Path("../e4_iot_thermal_zone_sensors_devices.json") # opening the files -with open(READ_GROUPS_FILEPATH, 'r') as groups_file, \ - open(READ_DEVICES_FILEPATH, 'r') as devices_file: +with open(READ_GROUPS_FILEPATH, "r") as groups_file, open( + READ_DEVICES_FILEPATH, "r" +) as devices_file: json_groups = json.load(groups_file) json_devices = json.load(devices_file) @@ -115,20 +113,21 @@ COM_STEP = 60 * 60 * 0.25 # 15 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN, - temp_start=TEMPERATURE_ZONE_START) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + temp_start=TEMPERATURE_ZONE_START, + ) # define lists to store historical data history_weather_station = [] @@ -153,39 +152,32 @@ # ToDo: Create an additional device holding a command attribute and # post it to the IoT-Agent. It should be mapped to the `type` heater. # create the sim_time attribute and add it during device creation - t_sim = DeviceAttribute(name='sim_time', - object_id='t_sim', - type="Number") + t_sim = DeviceAttribute(name="sim_time", object_id="t_sim", type="Number") # ToDo: Create the command attribute of name `heater_on` (currently it is # not possible to add metadata here). - cmd = DeviceCommand(name=..., - type=...) + cmd = DeviceCommand(name=..., type=...) # ToDo: Create the device configuration and send it to the server. heater = Device(...) - - - - - - - iotac.post_device(device=heater) # ToDo: Check the entity that corresponds to your device. - heater_entity = cbc.get_entity(entity_id=heater.entity_name, - entity_type=heater.entity_type) - print(f"Your device entity before running the simulation: \n " - f"{heater_entity.model_dump_json(indent=2)}") + heater_entity = cbc.get_entity( + entity_id=heater.entity_name, entity_type=heater.entity_type + ) + print( + f"Your device entity before running the simulation: \n " + f"{heater_entity.model_dump_json(indent=2)}" + ) # create a MQTTv5 client with paho-mqtt and the known groups and devices. - mqttc = IoTAMQTTClient(protocol=mqtt.MQTTv5, - devices=[weather_station, - zone_temperature_sensor, - heater], - service_groups=[group]) + mqttc = IoTAMQTTClient( + protocol=mqtt.MQTTv5, + devices=[weather_station, zone_temperature_sensor, heater], + service_groups=[group], + ) # set user data if required mqttc.username_pw_set(username=MQTT_USER, password=MQTT_PW) @@ -197,45 +189,37 @@ def on_command(client, obj, msg): Callback for incoming commands """ # decode the message payload using the libraries builtin encoders - apikey, device_id, payload = \ - client.get_encoder(PayloadProtocol.IOTA_JSON).decode_message( - msg=msg) + apikey, device_id, payload = client.get_encoder( + PayloadProtocol.IOTA_JSON + ).decode_message(msg=msg) # map the command value to the simulation sim_model.heater_on = payload[cmd.name] # ToDo: Acknowledge the command. In this case commands are usually single # messages. The first key is equal to the commands name. - client.publish(device_id=device_id, - command_name=..., - payload=...) + client.publish(device_id=device_id, command_name=..., payload=...) # ToDo: Add the command callback to your MQTTClient. This will get # triggered for the specified device_id. - mqttc.add_command_callback(device_id=..., - callback=...) + mqttc.add_command_callback(device_id=..., callback=...) # ToDO: Create an MQTT subscription for asynchronous communication that # gets triggered when the temperature attribute changes. subscription = { "description": "Subscription to receive MQTT-Notifications about " - "urn:ngsi-ld:ThermalZone:001", + "urn:ngsi-ld:ThermalZone:001", "subject": { - "entities": [ - { - "id": ..., - "type": ... - } - ], + "entities": [{"id": ..., "type": ...}], }, "notification": { "mqtt": { "url": MQTT_BROKER_URL_INTERNAL, "topic": TOPIC_CONTROLLER, "user": MQTT_USER, - "passwd": MQTT_PW + "passwd": MQTT_PW, } }, - "throttling": 0 + "throttling": 0, } # generate Subscription object for validation and post it subscription = Subscription(**subscription) @@ -267,20 +251,19 @@ def on_measurement(client, obj, msg): command = NamedCommand(name=cmd.name, value=state) cbc.post_command(...) - - - mqttc.message_callback_add(sub=TOPIC_CONTROLLER, - callback=on_measurement) + mqttc.message_callback_add(sub=TOPIC_CONTROLLER, callback=on_measurement) # connect to the mqtt broker and subscribe to your topic mqtt_url = urlparse(MQTT_BROKER_URL_EXPOSED) - mqttc.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # subscribe to topics # subscribe to all incoming command topics for the registered devices mqttc.subscribe() @@ -293,22 +276,23 @@ def on_measurement(client, obj, msg): # to the broker that holds the simulation time "sim_time" and the # corresponding temperature "temperature". You may use the `object_id` # or the attribute name as key in your payload. - for t_sim in range(sim_model.t_start, - sim_model.t_end + int(COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, sim_model.t_end + int(COM_STEP), int(COM_STEP) + ): # publish the simulated ambient temperature - mqttc.publish(device_id=weather_station.device_id, - payload={"temperature": sim_model.t_amb, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=weather_station.device_id, + payload={"temperature": sim_model.t_amb, "sim_time": sim_model.t_sim}, + ) # publish the simulated zone temperature - mqttc.publish(device_id=zone_temperature_sensor.device_id, - payload={"temperature": sim_model.t_zone, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=zone_temperature_sensor.device_id, + payload={"temperature": sim_model.t_zone, "sim_time": sim_model.t_sim}, + ) # publish the 'sim_time' for the heater device - mqttc.publish(device_id=heater.device_id, - payload={"sim_time": sim_model.t_sim}) + mqttc.publish(device_id=heater.device_id, payload={"sim_time": sim_model.t_sim}) time.sleep(0.1) # simulation step for next loop @@ -319,86 +303,100 @@ def on_measurement(client, obj, msg): # get corresponding entities and write values to history weather_station_entity = cbc.get_entity( entity_id=weather_station.entity_name, - entity_type=weather_station.entity_type + entity_type=weather_station.entity_type, ) # append the data to the local history history_weather_station.append( - {"sim_time": weather_station_entity.sim_time.value, - "temperature": weather_station_entity.temperature.value}) + { + "sim_time": weather_station_entity.sim_time.value, + "temperature": weather_station_entity.temperature.value, + } + ) # get zone temperature sensor and write values to history zone_temperature_sensor_entity = cbc.get_entity( entity_id=zone_temperature_sensor.entity_name, - entity_type=zone_temperature_sensor.entity_type + entity_type=zone_temperature_sensor.entity_type, ) history_zone_temperature_sensor.append( - {"sim_time": zone_temperature_sensor_entity.sim_time.value, - "temperature": zone_temperature_sensor_entity.temperature.value}) + { + "sim_time": zone_temperature_sensor_entity.sim_time.value, + "temperature": zone_temperature_sensor_entity.temperature.value, + } + ) # get zone temperature sensor and write values to history heater_entity = cbc.get_entity( - entity_id=heater.entity_name, - entity_type=heater.entity_type) + entity_id=heater.entity_name, entity_type=heater.entity_type + ) history_heater.append( - {"sim_time": heater_entity.sim_time.value, - "on_off": heater_entity.heater_on_info.value}) + { + "sim_time": heater_entity.sim_time.value, + "on_off": heater_entity.heater_on_info.value, + } + ) # close the mqtt listening thread mqttc.loop_stop() # disconnect the mqtt device mqttc.disconnect() - print(cbc.get_entity(entity_id=heater.entity_name, - entity_type=heater.entity_type).model_dump_json(indent=2)) + print( + cbc.get_entity( + entity_id=heater.entity_name, entity_type=heater.entity_type + ).model_dump_json(indent=2) + ) # plot results fig, ax = plt.subplots() - t_simulation = [item["sim_time"]/60 for item in history_weather_station] + t_simulation = [item["sim_time"] / 60 for item in history_weather_station] temperature = [item["temperature"] for item in history_weather_station] ax.plot(t_simulation, temperature) ax.title.set_text("Weather Station") - ax.set_xlabel('time in min') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in min") + ax.set_ylabel("ambient temperature in °C") plt.show() fig2, ax2 = plt.subplots() - t_simulation = [item["sim_time"]/60 for item in history_zone_temperature_sensor] - temperature = [item["temperature"] for item in - history_zone_temperature_sensor] + t_simulation = [item["sim_time"] / 60 for item in history_zone_temperature_sensor] + temperature = [item["temperature"] for item in history_zone_temperature_sensor] ax2.plot(t_simulation, temperature) ax2.title.set_text("Zone Temperature Sensor") - ax2.set_xlabel('time in min') - ax2.set_ylabel('zone temperature in °C') + ax2.set_xlabel("time in min") + ax2.set_ylabel("zone temperature in °C") plt.show() fig3, ax3 = plt.subplots() - t_simulation = [item["sim_time"]/60 for item in history_heater] + t_simulation = [item["sim_time"] / 60 for item in history_heater] on_off = [item["on_off"] for item in history_heater] ax3.plot(t_simulation, on_off) ax3.title.set_text("Heater") - ax3.set_xlabel('time in min') - ax3.set_ylabel('on/off') + ax3.set_xlabel("time in min") + ax3.set_ylabel("on/off") plt.show() # write devices and groups to file and clear server state - assert WRITE_DEVICES_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_DEVICES_FILEPATH.suffix}" + assert ( + WRITE_DEVICES_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_DEVICES_FILEPATH.suffix}" WRITE_DEVICES_FILEPATH.touch(exist_ok=True) - with WRITE_DEVICES_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_DEVICES_FILEPATH.open("w", encoding="utf-8") as f: devices = [item.model_dump() for item in iotac.get_device_list()] json.dump(devices, f, ensure_ascii=False, indent=2) - assert WRITE_GROUPS_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_GROUPS_FILEPATH.suffix}" + assert ( + WRITE_GROUPS_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_GROUPS_FILEPATH.suffix}" WRITE_GROUPS_FILEPATH.touch(exist_ok=True) - with WRITE_GROUPS_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_GROUPS_FILEPATH.open("w", encoding="utf-8") as f: groups = [item.model_dump() for item in iotac.get_group_list()] json.dump(groups, f, ensure_ascii=False, indent=2) - assert WRITE_SUBSCRIPTIONS_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_SUBSCRIPTIONS_FILEPATH.suffix}" + assert ( + WRITE_SUBSCRIPTIONS_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_SUBSCRIPTIONS_FILEPATH.suffix}" WRITE_SUBSCRIPTIONS_FILEPATH.touch(exist_ok=True) - with WRITE_SUBSCRIPTIONS_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_SUBSCRIPTIONS_FILEPATH.open("w", encoding="utf-8") as f: subs = [item.model_dump() for item in cbc.get_subscription_list()] json.dump(subs, f, ensure_ascii=False, indent=2) diff --git a/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control_solution.py b/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control_solution.py index d05bccdf..f40c9785 100644 --- a/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control_solution.py +++ b/tutorials/ngsi_v2/e5_iot_thermal_zone_control/e5_iot_thermal_zone_control_solution.py @@ -47,13 +47,15 @@ from filip.models.base import DataType, FiwareHeader from filip.models.ngsi_v2.context import NamedCommand from filip.models.ngsi_v2.subscriptions import Subscription, Message -from filip.models.ngsi_v2.iot import \ - Device, \ - DeviceAttribute, \ - DeviceCommand, \ - PayloadProtocol, \ - ServiceGroup +from filip.models.ngsi_v2.iot import ( + Device, + DeviceAttribute, + DeviceCommand, + PayloadProtocol, + ServiceGroup, +) from filip.utils.cleanup import clear_context_broker, clear_iot_agent + # import simulation model from tutorials.ngsi_v2.simulation_model import SimulationModel @@ -75,33 +77,31 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" UNIQUE_ID = str(uuid4()) TOPIC_CONTROLLER = f"fiware_workshop/{UNIQUE_ID}/controller" print(TOPIC_CONTROLLER) # path to json-files to store entity data for follow-up exercises -WRITE_GROUPS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_groups.json") -WRITE_DEVICES_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_devices.json") -WRITE_SUBSCRIPTIONS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_subscriptions.json") +WRITE_GROUPS_FILEPATH = Path("../e5_iot_thermal_zone_control_solution_groups.json") +WRITE_DEVICES_FILEPATH = Path("../e5_iot_thermal_zone_control_solution_devices.json") +WRITE_SUBSCRIPTIONS_FILEPATH = Path( + "../e5_iot_thermal_zone_control_solution_subscriptions.json" +) # path to read json-files from previous exercises -READ_GROUPS_FILEPATH = \ - Path("../e4_iot_thermal_zone_sensors_solution_groups.json") -READ_DEVICES_FILEPATH = \ - Path("../e4_iot_thermal_zone_sensors_solution_devices.json") +READ_GROUPS_FILEPATH = Path("../e4_iot_thermal_zone_sensors_solution_groups.json") +READ_DEVICES_FILEPATH = Path("../e4_iot_thermal_zone_sensors_solution_devices.json") # opening the files -with open(READ_GROUPS_FILEPATH, 'r') as groups_file, \ - open(READ_DEVICES_FILEPATH, 'r') as devices_file: +with open(READ_GROUPS_FILEPATH, "r") as groups_file, open( + READ_DEVICES_FILEPATH, "r" +) as devices_file: json_groups = json.load(groups_file) json_devices = json.load(devices_file) @@ -115,20 +115,21 @@ COM_STEP = 60 * 60 * 0.25 # 15 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN, - temp_start=TEMPERATURE_ZONE_START) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + temp_start=TEMPERATURE_ZONE_START, + ) # define lists to store historical data history_weather_station = [] @@ -153,39 +154,41 @@ # ToDo: Create an additional device holding a command attribute and # post it to the IoT-Agent. It should be mapped to the `type` heater. # create the sim_time attribute and add it during device creation - t_sim = DeviceAttribute(name='sim_time', - object_id='t_sim', - type="Number") + t_sim = DeviceAttribute(name="sim_time", object_id="t_sim", type="Number") # ToDo: Create the command attribute of name `heater_on` (currently it is # not possible to add metadata here). - cmd = DeviceCommand(name="heater_on", - type=DataType.BOOLEAN) + cmd = DeviceCommand(name="heater_on", type=DataType.BOOLEAN) # ToDo: Create the device configuration and send it to the server. - heater = Device(device_id="device:003", - entity_name="urn:ngsi-ld:Heater:001", - entity_type="Heater", - apikey=APIKEY, - attributes=[t_sim], - commands=[cmd], - transport='MQTT', - protocol='IoTA-JSON') + heater = Device( + device_id="device:003", + entity_name="urn:ngsi-ld:Heater:001", + entity_type="Heater", + apikey=APIKEY, + attributes=[t_sim], + commands=[cmd], + transport="MQTT", + protocol="IoTA-JSON", + ) iotac.post_device(device=heater) # ToDo: Check the entity that corresponds to your device. - heater_entity = cbc.get_entity(entity_id=heater.entity_name, - entity_type=heater.entity_type) - print(f"Your device entity before running the simulation: \n " - f"{heater_entity.model_dump_json(indent=2)}") + heater_entity = cbc.get_entity( + entity_id=heater.entity_name, entity_type=heater.entity_type + ) + print( + f"Your device entity before running the simulation: \n " + f"{heater_entity.model_dump_json(indent=2)}" + ) # create a MQTTv5 client with paho-mqtt and the known groups and devices. - mqttc = IoTAMQTTClient(protocol=mqtt.MQTTv5, - devices=[weather_station, - zone_temperature_sensor, - heater], - service_groups=[group]) + mqttc = IoTAMQTTClient( + protocol=mqtt.MQTTv5, + devices=[weather_station, zone_temperature_sensor, heater], + service_groups=[group], + ) # set user data if required mqttc.username_pw_set(username=MQTT_USER, password=MQTT_PW) @@ -197,33 +200,32 @@ def on_command(client, obj, msg): Callback for incoming commands """ # decode the message payload using the libraries builtin encoders - apikey, device_id, payload = \ - client.get_encoder(PayloadProtocol.IOTA_JSON).decode_message( - msg=msg) + apikey, device_id, payload = client.get_encoder( + PayloadProtocol.IOTA_JSON + ).decode_message(msg=msg) # map the command value to the simulation sim_model.heater_on = payload[cmd.name] # ToDo: Acknowledge the command. In this case commands are usually single # messages. The first key is equal to the commands name. - client.publish(device_id=device_id, - command_name=next(iter(payload)), - payload=payload) + client.publish( + device_id=device_id, command_name=next(iter(payload)), payload=payload + ) # ToDo: Add the command callback to your MQTTClient. This will get # triggered for the specified device_id. - mqttc.add_command_callback(device_id=heater.device_id, - callback=on_command) + mqttc.add_command_callback(device_id=heater.device_id, callback=on_command) # ToDO: Create an MQTT subscription for asynchronous communication that # gets triggered when the temperature attribute changes. subscription = { "description": "Subscription to receive MQTT-Notifications about " - "urn:ngsi-ld:ThermalZone:001", + "urn:ngsi-ld:ThermalZone:001", "subject": { "entities": [ { "id": zone_temperature_sensor.entity_name, - "type": zone_temperature_sensor.entity_type + "type": zone_temperature_sensor.entity_type, } ], }, @@ -232,10 +234,10 @@ def on_command(client, obj, msg): "url": MQTT_BROKER_URL_INTERNAL, "topic": TOPIC_CONTROLLER, "user": MQTT_USER, - "passwd": MQTT_PW + "passwd": MQTT_PW, } }, - "throttling": 0 + "throttling": 0, } # generate Subscription object for validation and post it subscription = Subscription(**subscription) @@ -265,22 +267,25 @@ def on_measurement(client, obj, msg): # ToDo: Send the command to the heater entity. if update: command = NamedCommand(name=cmd.name, value=state) - cbc.post_command(entity_id=heater.entity_name, - entity_type=heater.entity_type, - command=command) + cbc.post_command( + entity_id=heater.entity_name, + entity_type=heater.entity_type, + command=command, + ) - mqttc.message_callback_add(sub=TOPIC_CONTROLLER, - callback=on_measurement) + mqttc.message_callback_add(sub=TOPIC_CONTROLLER, callback=on_measurement) # connect to the mqtt broker and subscribe to your topic mqtt_url = urlparse(MQTT_BROKER_URL_EXPOSED) - mqttc.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # subscribe to topics # subscribe to all incoming command topics for the registered devices mqttc.subscribe() @@ -293,22 +298,23 @@ def on_measurement(client, obj, msg): # to the broker that holds the simulation time "sim_time" and the # corresponding temperature "temperature". You may use the `object_id` # or the attribute name as key in your payload. - for t_sim in range(sim_model.t_start, - sim_model.t_end + int(COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, sim_model.t_end + int(COM_STEP), int(COM_STEP) + ): # publish the simulated ambient temperature - mqttc.publish(device_id=weather_station.device_id, - payload={"temperature": sim_model.t_amb, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=weather_station.device_id, + payload={"temperature": sim_model.t_amb, "sim_time": sim_model.t_sim}, + ) # publish the simulated zone temperature - mqttc.publish(device_id=zone_temperature_sensor.device_id, - payload={"temperature": sim_model.t_zone, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=zone_temperature_sensor.device_id, + payload={"temperature": sim_model.t_zone, "sim_time": sim_model.t_sim}, + ) # publish the 'sim_time' for the heater device - mqttc.publish(device_id=heater.device_id, - payload={"sim_time": sim_model.t_sim}) + mqttc.publish(device_id=heater.device_id, payload={"sim_time": sim_model.t_sim}) time.sleep(0.1) # simulation step for next loop @@ -319,86 +325,100 @@ def on_measurement(client, obj, msg): # get corresponding entities and write values to history weather_station_entity = cbc.get_entity( entity_id=weather_station.entity_name, - entity_type=weather_station.entity_type + entity_type=weather_station.entity_type, ) # append the data to the local history history_weather_station.append( - {"sim_time": weather_station_entity.sim_time.value, - "temperature": weather_station_entity.temperature.value}) + { + "sim_time": weather_station_entity.sim_time.value, + "temperature": weather_station_entity.temperature.value, + } + ) # get zone temperature sensor and write values to history zone_temperature_sensor_entity = cbc.get_entity( entity_id=zone_temperature_sensor.entity_name, - entity_type=zone_temperature_sensor.entity_type + entity_type=zone_temperature_sensor.entity_type, ) history_zone_temperature_sensor.append( - {"sim_time": zone_temperature_sensor_entity.sim_time.value, - "temperature": zone_temperature_sensor_entity.temperature.value}) + { + "sim_time": zone_temperature_sensor_entity.sim_time.value, + "temperature": zone_temperature_sensor_entity.temperature.value, + } + ) # get zone temperature sensor and write values to history heater_entity = cbc.get_entity( - entity_id=heater.entity_name, - entity_type=heater.entity_type) + entity_id=heater.entity_name, entity_type=heater.entity_type + ) history_heater.append( - {"sim_time": heater_entity.sim_time.value, - "on_off": heater_entity.heater_on_info.value}) + { + "sim_time": heater_entity.sim_time.value, + "on_off": heater_entity.heater_on_info.value, + } + ) # close the mqtt listening thread mqttc.loop_stop() # disconnect the mqtt device mqttc.disconnect() - print(cbc.get_entity(entity_id=heater.entity_name, - entity_type=heater.entity_type).model_dump_json(indent=2)) + print( + cbc.get_entity( + entity_id=heater.entity_name, entity_type=heater.entity_type + ).model_dump_json(indent=2) + ) # plot results fig, ax = plt.subplots() - t_simulation = [item["sim_time"]/60 for item in history_weather_station] + t_simulation = [item["sim_time"] / 60 for item in history_weather_station] temperature = [item["temperature"] for item in history_weather_station] ax.plot(t_simulation, temperature) ax.title.set_text("Weather Station") - ax.set_xlabel('time in min') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in min") + ax.set_ylabel("ambient temperature in °C") plt.show() fig2, ax2 = plt.subplots() - t_simulation = [item["sim_time"]/60 for item in history_zone_temperature_sensor] - temperature = [item["temperature"] for item in - history_zone_temperature_sensor] + t_simulation = [item["sim_time"] / 60 for item in history_zone_temperature_sensor] + temperature = [item["temperature"] for item in history_zone_temperature_sensor] ax2.plot(t_simulation, temperature) ax2.title.set_text("Zone Temperature Sensor") - ax2.set_xlabel('time in min') - ax2.set_ylabel('zone temperature in °C') + ax2.set_xlabel("time in min") + ax2.set_ylabel("zone temperature in °C") plt.show() fig3, ax3 = plt.subplots() - t_simulation = [item["sim_time"]/60 for item in history_heater] + t_simulation = [item["sim_time"] / 60 for item in history_heater] on_off = [item["on_off"] for item in history_heater] ax3.plot(t_simulation, on_off) ax3.title.set_text("Heater") - ax3.set_xlabel('time in min') - ax3.set_ylabel('on/off') + ax3.set_xlabel("time in min") + ax3.set_ylabel("on/off") plt.show() # write devices and groups to file and clear server state - assert WRITE_DEVICES_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_DEVICES_FILEPATH.suffix}" + assert ( + WRITE_DEVICES_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_DEVICES_FILEPATH.suffix}" WRITE_DEVICES_FILEPATH.touch(exist_ok=True) - with WRITE_DEVICES_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_DEVICES_FILEPATH.open("w", encoding="utf-8") as f: devices = [item.model_dump() for item in iotac.get_device_list()] json.dump(devices, f, ensure_ascii=False, indent=2) - assert WRITE_GROUPS_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_GROUPS_FILEPATH.suffix}" + assert ( + WRITE_GROUPS_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_GROUPS_FILEPATH.suffix}" WRITE_GROUPS_FILEPATH.touch(exist_ok=True) - with WRITE_GROUPS_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_GROUPS_FILEPATH.open("w", encoding="utf-8") as f: groups = [item.model_dump() for item in iotac.get_group_list()] json.dump(groups, f, ensure_ascii=False, indent=2) - assert WRITE_SUBSCRIPTIONS_FILEPATH.suffix == '.json', \ - f"Wrong file extension! {WRITE_SUBSCRIPTIONS_FILEPATH.suffix}" + assert ( + WRITE_SUBSCRIPTIONS_FILEPATH.suffix == ".json" + ), f"Wrong file extension! {WRITE_SUBSCRIPTIONS_FILEPATH.suffix}" WRITE_SUBSCRIPTIONS_FILEPATH.touch(exist_ok=True) - with WRITE_SUBSCRIPTIONS_FILEPATH.open('w', encoding='utf-8') as f: + with WRITE_SUBSCRIPTIONS_FILEPATH.open("w", encoding="utf-8") as f: subs = [item.model_dump() for item in cbc.get_subscription_list()] json.dump(subs, f, ensure_ascii=False, indent=2) diff --git a/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data.py b/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data.py index 3e7e9174..95454322 100644 --- a/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data.py +++ b/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data.py @@ -24,24 +24,21 @@ import paho.mqtt.client as mqtt from pydantic import TypeAdapter import matplotlib.pyplot as plt + # import from filip -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient, QuantumLeapClient from filip.clients.mqtt import IoTAMQTTClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.context import NamedCommand -from filip.models.ngsi_v2.subscriptions import Subscription, Message, Subject, \ - Notification -from filip.models.ngsi_v2.iot import \ - Device, \ - PayloadProtocol, \ - ServiceGroup -from filip.utils.cleanup import \ - clear_context_broker, \ - clear_iot_agent, \ - clear_quantumleap +from filip.models.ngsi_v2.subscriptions import ( + Subscription, + Message, + Subject, + Notification, +) +from filip.models.ngsi_v2.iot import Device, PayloadProtocol, ServiceGroup +from filip.utils.cleanup import clear_context_broker, clear_iot_agent, clear_quantumleap + # import simulation model from tutorials.ngsi_v2.simulation_model import SimulationModel @@ -65,30 +62,27 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" UNIQUE_ID = str(uuid4()) TOPIC_CONTROLLER = f"fiware_workshop/{UNIQUE_ID}/controller" print(TOPIC_CONTROLLER) # path to read json-files from previous exercises -READ_GROUPS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_groups.json") -READ_DEVICES_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_devices.json") -READ_SUBSCRIPTIONS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_subscriptions.json") +READ_GROUPS_FILEPATH = Path("../e5_iot_thermal_zone_control_groups.json") +READ_DEVICES_FILEPATH = Path("../e5_iot_thermal_zone_control_devices.json") +READ_SUBSCRIPTIONS_FILEPATH = Path("../e5_iot_thermal_zone_control_subscriptions.json") # opening the files -with open(READ_GROUPS_FILEPATH, 'r') as groups_file, \ - open(READ_DEVICES_FILEPATH, 'r') as devices_file, \ - open(READ_SUBSCRIPTIONS_FILEPATH, 'r') as subscriptions_file: +with open(READ_GROUPS_FILEPATH, "r") as groups_file, open( + READ_DEVICES_FILEPATH, "r" +) as devices_file, open(READ_SUBSCRIPTIONS_FILEPATH, "r") as subscriptions_file: json_groups = json.load(groups_file) json_devices = json.load(devices_file) json_subscriptions = json.load(subscriptions_file) @@ -103,21 +97,22 @@ COM_STEP = 60 * 60 * 0.25 # 15 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) clear_quantumleap(url=QL_URL, fiware_header=fiware_header) # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN, - temp_start=TEMPERATURE_ZONE_START) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + temp_start=TEMPERATURE_ZONE_START, + ) # create clients and restore devices and groups from file groups = TypeAdapter(List[ServiceGroup]).validate_python(json_groups) @@ -140,11 +135,11 @@ # create a MQTTv5 client with paho-mqtt and the known groups and # devices. - mqttc = IoTAMQTTClient(protocol=mqtt.MQTTv5, - devices=[weather_station, - zone_temperature_sensor, - heater], - service_groups=[group]) + mqttc = IoTAMQTTClient( + protocol=mqtt.MQTTv5, + devices=[weather_station, zone_temperature_sensor, heater], + service_groups=[group], + ) # ToDo: Set user data if required. mqttc.username_pw_set(username=MQTT_USER, password=MQTT_PW) # Implement a callback function that gets triggered when the @@ -156,22 +151,21 @@ def on_command(client, obj, msg): Callback for incoming commands """ # Decode the message payload using the libraries builtin encoders - apikey, device_id, payload = \ - client.get_encoder(PayloadProtocol.IOTA_JSON).decode_message( - msg=msg) + apikey, device_id, payload = client.get_encoder( + PayloadProtocol.IOTA_JSON + ).decode_message(msg=msg) sim_model.heater_on = payload[heater.commands[0].name] # Acknowledge the command. Here commands are usually single # messages. The first key is equal to the commands name. - client.publish(device_id=device_id, - command_name=next(iter(payload)), - payload=payload) + client.publish( + device_id=device_id, command_name=next(iter(payload)), payload=payload + ) # Add the command callback to your MQTTClient. This will get # triggered for the specified device_id. - mqttc.add_command_callback(device_id=heater.device_id, - callback=on_command) + mqttc.add_command_callback(device_id=heater.device_id, callback=on_command) # You need to implement a controller that controls the # heater state with respect to the zone temperature. This will be @@ -196,49 +190,54 @@ def on_measurement(client, obj, msg): # send the command to the heater entity if update: command = NamedCommand(name=heater.commands[0].name, value=state) - cbc.post_command(entity_id=heater.entity_name, - entity_type=heater.entity_type, - command=command) + cbc.post_command( + entity_id=heater.entity_name, + entity_type=heater.entity_type, + command=command, + ) - mqttc.message_callback_add(sub=TOPIC_CONTROLLER, - callback=on_measurement) + mqttc.message_callback_add(sub=TOPIC_CONTROLLER, callback=on_measurement) # ToDo: Create http subscriptions that get triggered by updates of your # device attributes. Note that you can only post the subscription # to the context broker. # Subscription for weather station - cbc.post_subscription(subscription=Subscription( - subject=Subject(**{ - 'entities': [{'id': weather_station.entity_name, - 'type': weather_station.entity_type}] - }), - notification=Notification(**{ - 'http': {'url': 'http://quantumleap:8668/v2/notify'} - }), - throttling=0) + cbc.post_subscription( + subscription=Subscription( + subject=Subject( + **{ + "entities": [ + { + "id": weather_station.entity_name, + "type": weather_station.entity_type, + } + ] + } + ), + notification=Notification( + **{"http": {"url": "http://quantumleap:8668/v2/notify"}} + ), + throttling=0, + ) ) # Subscription for zone temperature sensor - cbc.post_subscription(subscription=Subscription( - ... - ) - ) + cbc.post_subscription(subscription=Subscription(...)) # Subscription for heater - cbc.post_subscription(subscription=Subscription( - ... - ) - ) + cbc.post_subscription(subscription=Subscription(...)) # connect to the mqtt broker and subscribe to your topic mqtt_url = urlparse(MQTT_BROKER_URL_EXPOSED) - mqttc.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # subscribe to topics # subscribe to all incoming command topics for the registered devices @@ -252,22 +251,23 @@ def on_measurement(client, obj, msg): # that holds the simulation time "sim_time" and the corresponding # temperature "temperature". You may use the `object_id` # or the attribute name as key in your payload. - for t_sim in range(sim_model.t_start, - sim_model.t_end + int(COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, sim_model.t_end + int(COM_STEP), int(COM_STEP) + ): # publish the simulated ambient temperature - mqttc.publish(device_id=weather_station.device_id, - payload={"temperature": sim_model.t_amb, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=weather_station.device_id, + payload={"temperature": sim_model.t_amb, "sim_time": sim_model.t_sim}, + ) # publish the simulated zone temperature - mqttc.publish(device_id=zone_temperature_sensor.device_id, - payload={"temperature": sim_model.t_zone, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=zone_temperature_sensor.device_id, + payload={"temperature": sim_model.t_zone, "sim_time": sim_model.t_sim}, + ) # publish the 'sim_time' for the heater device - mqttc.publish(device_id=heater.device_id, - payload={"sim_time": sim_model.t_sim}) + mqttc.publish(device_id=heater.device_id, payload={"sim_time": sim_model.t_sim}) time.sleep(0.3) # simulation step for next loop @@ -292,7 +292,7 @@ def on_measurement(client, obj, msg): history_weather_station = qlc.get_entity_by_id( entity_id=weather_station.entity_name, entity_type=weather_station.entity_type, - last_n=10000 + last_n=10000, ) # convert to pandas dataframe and print it @@ -300,18 +300,22 @@ def on_measurement(client, obj, msg): print(history_weather_station) # drop unnecessary index levels history_weather_station = history_weather_station.droplevel( - level=("entityId", "entityType"), axis=1) - history_weather_station['sim_time'] = pd.to_numeric( - history_weather_station['sim_time'], downcast="float") - history_weather_station['temperature'] = pd.to_numeric( - history_weather_station['temperature'], downcast="float") + level=("entityId", "entityType"), axis=1 + ) + history_weather_station["sim_time"] = pd.to_numeric( + history_weather_station["sim_time"], downcast="float" + ) + history_weather_station["temperature"] = pd.to_numeric( + history_weather_station["temperature"], downcast="float" + ) # ToDo: Plot the results. fig, ax = plt.subplots() - ax.plot(history_weather_station['sim_time']/60, - history_weather_station['temperature']) + ax.plot( + history_weather_station["sim_time"] / 60, history_weather_station["temperature"] + ) ax.title.set_text("Weather Station") - ax.set_xlabel('time in min') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in min") + ax.set_ylabel("ambient temperature in °C") plt.show() # ToDo: Retrieve the data for the zone temperature. @@ -321,17 +325,21 @@ def on_measurement(client, obj, msg): history_zone_temperature_sensor = ... # ToDo: Drop unnecessary index levels. history_zone_temperature_sensor = ... - history_zone_temperature_sensor['sim_time'] = pd.to_numeric( - history_zone_temperature_sensor['sim_time'], downcast="float") - history_zone_temperature_sensor['temperature'] = pd.to_numeric( - history_zone_temperature_sensor['temperature'], downcast="float") + history_zone_temperature_sensor["sim_time"] = pd.to_numeric( + history_zone_temperature_sensor["sim_time"], downcast="float" + ) + history_zone_temperature_sensor["temperature"] = pd.to_numeric( + history_zone_temperature_sensor["temperature"], downcast="float" + ) # ToDo: Plot the results. fig2, ax2 = plt.subplots() - ax2.plot(history_zone_temperature_sensor['sim_time']/60, - history_zone_temperature_sensor['temperature']) + ax2.plot( + history_zone_temperature_sensor["sim_time"] / 60, + history_zone_temperature_sensor["temperature"], + ) ax2.title.set_text("Zone Temperature Sensor") - ax2.set_xlabel('time in min') - ax2.set_ylabel('zone temperature in °C') + ax2.set_xlabel("time in min") + ax2.set_ylabel("zone temperature in °C") plt.show() # ToDo: Retrieve the data for the heater. @@ -339,23 +347,23 @@ def on_measurement(client, obj, msg): # convert to pandas dataframe and print it history_heater = history_heater.to_pandas() - history_heater = history_heater.replace(' ', 0) + history_heater = history_heater.replace(" ", 0) print(history_heater) # ToDo: Drop unnecessary index levels. - history_heater = history_heater.droplevel( - level=("entityId", "entityType"), axis=1) - history_heater['sim_time'] = pd.to_numeric( - history_heater['sim_time'], downcast="float") - history_heater['heater_on_info'] = pd.to_numeric( - history_heater['heater_on_info'], downcast="float") + history_heater = history_heater.droplevel(level=("entityId", "entityType"), axis=1) + history_heater["sim_time"] = pd.to_numeric( + history_heater["sim_time"], downcast="float" + ) + history_heater["heater_on_info"] = pd.to_numeric( + history_heater["heater_on_info"], downcast="float" + ) # ToDo: Plot the results. fig3, ax3 = plt.subplots() - ax3.plot(history_heater['sim_time']/60, - history_heater['heater_on_info']) + ax3.plot(history_heater["sim_time"] / 60, history_heater["heater_on_info"]) ax3.title.set_text("Heater") - ax3.set_xlabel('time in min') - ax3.set_ylabel('set point') + ax3.set_xlabel("time in min") + ax3.set_ylabel("set point") plt.show() clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) diff --git a/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data_solution.py b/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data_solution.py index b1978407..67a26429 100644 --- a/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data_solution.py +++ b/tutorials/ngsi_v2/e6_timeseries_data/e6_timeseries_data_solution.py @@ -24,24 +24,21 @@ import paho.mqtt.client as mqtt from pydantic import TypeAdapter import matplotlib.pyplot as plt + # import from filip -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient, \ - QuantumLeapClient +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient, QuantumLeapClient from filip.clients.mqtt import IoTAMQTTClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.context import NamedCommand -from filip.models.ngsi_v2.subscriptions import Subscription, Message, Notification, \ - Subject -from filip.models.ngsi_v2.iot import \ - Device, \ - PayloadProtocol, \ - ServiceGroup -from filip.utils.cleanup import \ - clear_context_broker, \ - clear_iot_agent, \ - clear_quantumleap +from filip.models.ngsi_v2.subscriptions import ( + Subscription, + Message, + Notification, + Subject, +) +from filip.models.ngsi_v2.iot import Device, PayloadProtocol, ServiceGroup +from filip.utils.cleanup import clear_context_broker, clear_iot_agent, clear_quantumleap + # import simulation model from tutorials.ngsi_v2.simulation_model import SimulationModel @@ -65,30 +62,29 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" UNIQUE_ID = str(uuid4()) TOPIC_CONTROLLER = f"fiware_workshop/{UNIQUE_ID}/controller" print(TOPIC_CONTROLLER) # path to read json-files from previous exercises -READ_GROUPS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_groups.json") -READ_DEVICES_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_devices.json") -READ_SUBSCRIPTIONS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_subscriptions.json") +READ_GROUPS_FILEPATH = Path("../e5_iot_thermal_zone_control_solution_groups.json") +READ_DEVICES_FILEPATH = Path("../e5_iot_thermal_zone_control_solution_devices.json") +READ_SUBSCRIPTIONS_FILEPATH = Path( + "../e5_iot_thermal_zone_control_solution_subscriptions.json" +) # opening the files -with open(READ_GROUPS_FILEPATH, 'r') as groups_file, \ - open(READ_DEVICES_FILEPATH, 'r') as devices_file, \ - open(READ_SUBSCRIPTIONS_FILEPATH, 'r') as subscriptions_file: +with open(READ_GROUPS_FILEPATH, "r") as groups_file, open( + READ_DEVICES_FILEPATH, "r" +) as devices_file, open(READ_SUBSCRIPTIONS_FILEPATH, "r") as subscriptions_file: json_groups = json.load(groups_file) json_devices = json.load(devices_file) json_subscriptions = json.load(subscriptions_file) @@ -103,21 +99,22 @@ COM_STEP = 60 * 60 * 0.25 # 15 min communication step in seconds # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) clear_quantumleap(url=QL_URL, fiware_header=fiware_header) # instantiate simulation model - sim_model = SimulationModel(t_start=T_SIM_START, - t_end=T_SIM_END, - temp_max=TEMPERATURE_MAX, - temp_min=TEMPERATURE_MIN, - temp_start=TEMPERATURE_ZONE_START) + sim_model = SimulationModel( + t_start=T_SIM_START, + t_end=T_SIM_END, + temp_max=TEMPERATURE_MAX, + temp_min=TEMPERATURE_MIN, + temp_start=TEMPERATURE_ZONE_START, + ) # create clients and restore devices and groups from file groups = TypeAdapter(List[ServiceGroup]).validate_python(json_groups) @@ -140,11 +137,11 @@ # create a MQTTv5 client with paho-mqtt and the known groups and # devices. - mqttc = IoTAMQTTClient(protocol=mqtt.MQTTv5, - devices=[weather_station, - zone_temperature_sensor, - heater], - service_groups=[group]) + mqttc = IoTAMQTTClient( + protocol=mqtt.MQTTv5, + devices=[weather_station, zone_temperature_sensor, heater], + service_groups=[group], + ) # ToDo: Set user data if required. mqttc.username_pw_set(username=MQTT_USER, password=MQTT_PW) # Implement a callback function that gets triggered when the @@ -156,22 +153,21 @@ def on_command(client, obj, msg): Callback for incoming commands """ # Decode the message payload using the libraries builtin encoders - apikey, device_id, payload = \ - client.get_encoder(PayloadProtocol.IOTA_JSON).decode_message( - msg=msg) + apikey, device_id, payload = client.get_encoder( + PayloadProtocol.IOTA_JSON + ).decode_message(msg=msg) sim_model.heater_on = payload[heater.commands[0].name] # Acknowledge the command. Here commands are usually single # messages. The first key is equal to the commands name. - client.publish(device_id=device_id, - command_name=next(iter(payload)), - payload=payload) + client.publish( + device_id=device_id, command_name=next(iter(payload)), payload=payload + ) # Add the command callback to your MQTTClient. This will get # triggered for the specified device_id. - mqttc.add_command_callback(device_id=heater.device_id, - callback=on_command) + mqttc.add_command_callback(device_id=heater.device_id, callback=on_command) # You need to implement a controller that controls the # heater state with respect to the zone temperature. This will be @@ -196,59 +192,79 @@ def on_measurement(client, obj, msg): # send the command to the heater entity if update: command = NamedCommand(name=heater.commands[0].name, value=state) - cbc.post_command(entity_id=heater.entity_name, - entity_type=heater.entity_type, - command=command) + cbc.post_command( + entity_id=heater.entity_name, + entity_type=heater.entity_type, + command=command, + ) - mqttc.message_callback_add(sub=TOPIC_CONTROLLER, - callback=on_measurement) + mqttc.message_callback_add(sub=TOPIC_CONTROLLER, callback=on_measurement) # ToDo: Create http subscriptions that get triggered by updates of your # device attributes. Note that you can only post the subscription # to the context broker. - cbc.post_subscription(subscription=Subscription( - subject=Subject(**{ - 'entities': [{'id': weather_station.entity_name, - 'type': weather_station.entity_type}] - }), - notification=Notification(**{ - 'http': {'url': 'http://quantumleap:8668/v2/notify'} - }), - throttling=0) + cbc.post_subscription( + subscription=Subscription( + subject=Subject( + **{ + "entities": [ + { + "id": weather_station.entity_name, + "type": weather_station.entity_type, + } + ] + } + ), + notification=Notification( + **{"http": {"url": "http://quantumleap:8668/v2/notify"}} + ), + throttling=0, + ) ) - cbc.post_subscription(subscription=Subscription( - subject=Subject(**{ - 'entities': [{'id': zone_temperature_sensor.entity_name, - 'type': zone_temperature_sensor.entity_type}] - }), - notification=Notification(**{ - 'http': {'url': 'http://quantumleap:8668/v2/notify'} - }), - throttling=0) + cbc.post_subscription( + subscription=Subscription( + subject=Subject( + **{ + "entities": [ + { + "id": zone_temperature_sensor.entity_name, + "type": zone_temperature_sensor.entity_type, + } + ] + } + ), + notification=Notification( + **{"http": {"url": "http://quantumleap:8668/v2/notify"}} + ), + throttling=0, + ) ) - cbc.post_subscription(subscription=Subscription( - subject=Subject(**{ - 'entities': [{'id': heater.entity_name, - 'type': heater.entity_type}] - }), - notification=Notification(**{ - 'http': {'url': 'http://quantumleap:8668/v2/notify'} - }), - throttling=0) + cbc.post_subscription( + subscription=Subscription( + subject=Subject( + **{"entities": [{"id": heater.entity_name, "type": heater.entity_type}]} + ), + notification=Notification( + **{"http": {"url": "http://quantumleap:8668/v2/notify"}} + ), + throttling=0, + ) ) # connect to the mqtt broker and subscribe to your topic mqtt_url = urlparse(MQTT_BROKER_URL_EXPOSED) - mqttc.connect(host=mqtt_url.hostname, - port=mqtt_url.port, - keepalive=60, - bind_address="", - bind_port=0, - clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, - properties=None) + mqttc.connect( + host=mqtt_url.hostname, + port=mqtt_url.port, + keepalive=60, + bind_address="", + bind_port=0, + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=None, + ) # subscribe to topics # subscribe to all incoming command topics for the registered devices @@ -262,22 +278,23 @@ def on_measurement(client, obj, msg): # that holds the simulation time "sim_time" and the corresponding # temperature "temperature". You may use the `object_id` # or the attribute name as key in your payload. - for t_sim in range(sim_model.t_start, - sim_model.t_end + int(COM_STEP), - int(COM_STEP)): + for t_sim in range( + sim_model.t_start, sim_model.t_end + int(COM_STEP), int(COM_STEP) + ): # publish the simulated ambient temperature - mqttc.publish(device_id=weather_station.device_id, - payload={"temperature": sim_model.t_amb, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=weather_station.device_id, + payload={"temperature": sim_model.t_amb, "sim_time": sim_model.t_sim}, + ) # publish the simulated zone temperature - mqttc.publish(device_id=zone_temperature_sensor.device_id, - payload={"temperature": sim_model.t_zone, - "sim_time": sim_model.t_sim}) + mqttc.publish( + device_id=zone_temperature_sensor.device_id, + payload={"temperature": sim_model.t_zone, "sim_time": sim_model.t_sim}, + ) # publish the 'sim_time' for the heater device - mqttc.publish(device_id=heater.device_id, - payload={"sim_time": sim_model.t_sim}) + mqttc.publish(device_id=heater.device_id, payload={"sim_time": sim_model.t_sim}) time.sleep(0.3) # simulation step for next loop @@ -302,7 +319,7 @@ def on_measurement(client, obj, msg): history_weather_station = qlc.get_entity_by_id( entity_id=weather_station.entity_name, entity_type=weather_station.entity_type, - last_n=10000 + last_n=10000, ) # convert to pandas dataframe and print it @@ -310,73 +327,79 @@ def on_measurement(client, obj, msg): print(history_weather_station) # drop unnecessary index levels history_weather_station = history_weather_station.droplevel( - level=("entityId", "entityType"), axis=1) - history_weather_station['sim_time'] = pd.to_numeric( - history_weather_station['sim_time'], downcast="float") - history_weather_station['temperature'] = pd.to_numeric( - history_weather_station['temperature'], downcast="float") + level=("entityId", "entityType"), axis=1 + ) + history_weather_station["sim_time"] = pd.to_numeric( + history_weather_station["sim_time"], downcast="float" + ) + history_weather_station["temperature"] = pd.to_numeric( + history_weather_station["temperature"], downcast="float" + ) # ToDo: Plot the results. fig, ax = plt.subplots() - ax.plot(history_weather_station['sim_time']/60, - history_weather_station['temperature']) + ax.plot( + history_weather_station["sim_time"] / 60, history_weather_station["temperature"] + ) ax.title.set_text("Weather Station") - ax.set_xlabel('time in min') - ax.set_ylabel('ambient temperature in °C') + ax.set_xlabel("time in min") + ax.set_ylabel("ambient temperature in °C") plt.show() # ToDo: Retrieve the data for the zone temperature. history_zone_temperature_sensor = qlc.get_entity_by_id( entity_id=zone_temperature_sensor.entity_name, entity_type=zone_temperature_sensor.entity_type, - last_n=10000 + last_n=10000, ) # ToDo: Convert to pandas dataframe and print it. - history_zone_temperature_sensor = \ - history_zone_temperature_sensor.to_pandas() + history_zone_temperature_sensor = history_zone_temperature_sensor.to_pandas() print(history_zone_temperature_sensor) # ToDo: Drop unnecessary index levels. history_zone_temperature_sensor = history_zone_temperature_sensor.droplevel( - level=("entityId", "entityType"), axis=1) - history_zone_temperature_sensor['sim_time'] = pd.to_numeric( - history_zone_temperature_sensor['sim_time'], downcast="float") - history_zone_temperature_sensor['temperature'] = pd.to_numeric( - history_zone_temperature_sensor['temperature'], downcast="float") + level=("entityId", "entityType"), axis=1 + ) + history_zone_temperature_sensor["sim_time"] = pd.to_numeric( + history_zone_temperature_sensor["sim_time"], downcast="float" + ) + history_zone_temperature_sensor["temperature"] = pd.to_numeric( + history_zone_temperature_sensor["temperature"], downcast="float" + ) # ToDo: Plot the results. fig2, ax2 = plt.subplots() - ax2.plot(history_zone_temperature_sensor['sim_time']/60, - history_zone_temperature_sensor['temperature']) + ax2.plot( + history_zone_temperature_sensor["sim_time"] / 60, + history_zone_temperature_sensor["temperature"], + ) ax2.title.set_text("Zone Temperature Sensor") - ax2.set_xlabel('time in min') - ax2.set_ylabel('zone temperature in °C') + ax2.set_xlabel("time in min") + ax2.set_ylabel("zone temperature in °C") plt.show() # ToDo: Retrieve the data for the heater. history_heater = qlc.get_entity_by_id( - entity_id=heater.entity_name, - entity_type=heater.entity_type, - last_n=10000 + entity_id=heater.entity_name, entity_type=heater.entity_type, last_n=10000 ) # convert to pandas dataframe and print it history_heater = history_heater.to_pandas() - history_heater = history_heater.replace(' ', 0) + history_heater = history_heater.replace(" ", 0) print(history_heater) # ToDo: Drop unnecessary index levels. - history_heater = history_heater.droplevel( - level=("entityId", "entityType"), axis=1) - history_heater['sim_time'] = pd.to_numeric( - history_heater['sim_time'], downcast="float") - history_heater['heater_on_info'] = pd.to_numeric( - history_heater['heater_on_info'], downcast="float") + history_heater = history_heater.droplevel(level=("entityId", "entityType"), axis=1) + history_heater["sim_time"] = pd.to_numeric( + history_heater["sim_time"], downcast="float" + ) + history_heater["heater_on_info"] = pd.to_numeric( + history_heater["heater_on_info"], downcast="float" + ) # ToDo: Plot the results. fig3, ax3 = plt.subplots() - ax3.plot(history_heater['sim_time']/60, - history_heater['heater_on_info']) + ax3.plot(history_heater["sim_time"] / 60, history_heater["heater_on_info"]) ax3.title.set_text("Heater") - ax3.set_xlabel('time in min') - ax3.set_ylabel('set point') + ax3.set_xlabel("time in min") + ax3.set_ylabel("set point") plt.show() clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) diff --git a/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot.py b/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot.py index 5c536a1f..aecbb190 100644 --- a/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot.py +++ b/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot.py @@ -25,22 +25,13 @@ from pydantic import TypeAdapter # import from filip -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.base import NamedMetadata -from filip.models.ngsi_v2.context import \ - ContextEntity, \ - NamedContextAttribute -from filip.models.ngsi_v2.iot import \ - Device, \ - ServiceGroup, \ - StaticDeviceAttribute +from filip.models.ngsi_v2.context import ContextEntity, NamedContextAttribute +from filip.models.ngsi_v2.iot import Device, ServiceGroup, StaticDeviceAttribute from filip.models.ngsi_v2.units import Unit -from filip.utils.cleanup import \ - clear_context_broker, \ - clear_iot_agent +from filip.utils.cleanup import clear_context_broker, clear_iot_agent # ## Parameters # ToDo: Enter your context broker host and port, e.g. http://localhost:1026. @@ -49,40 +40,36 @@ IOTA_URL = "http://localhost:4041" # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path # ToDo: Change the name of your service-path to something unique. If you run # on a shared instance this is very important in order to avoid user # collisions. You will use this service path through the whole tutorial. # If you forget to change it, an error will be raised! -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" # path to read json-files from previous exercises -READ_GROUPS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_groups.json") -READ_DEVICES_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_devices.json") -READ_ENTITIES_FILEPATH = \ - Path("../e3_context_entities.json") +READ_GROUPS_FILEPATH = Path("../e5_iot_thermal_zone_control_groups.json") +READ_DEVICES_FILEPATH = Path("../e5_iot_thermal_zone_control_devices.json") +READ_ENTITIES_FILEPATH = Path("../e3_context_entities.json") # opening the files -with open(READ_GROUPS_FILEPATH, 'r') as groups_file, \ - open(READ_DEVICES_FILEPATH, 'r') as devices_file, \ - open(READ_ENTITIES_FILEPATH, 'r') as entities_file: +with open(READ_GROUPS_FILEPATH, "r") as groups_file, open( + READ_DEVICES_FILEPATH, "r" +) as devices_file, open(READ_ENTITIES_FILEPATH, "r") as entities_file: json_groups = json.load(groups_file) json_devices = json.load(devices_file) json_entities = json.load(entities_file) # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) @@ -108,10 +95,12 @@ # ToDo: Get context entities from the Context Broker # (exclude the IoT device ones). - building = cbc.get_entity(entity_id="urn:ngsi-ld:building:001", - entity_type="Building") - thermal_zone = cbc.get_entity(entity_id="ThermalZone:001", - entity_type="ThermalZone") + building = cbc.get_entity( + entity_id="urn:ngsi-ld:building:001", entity_type="Building" + ) + thermal_zone = cbc.get_entity( + entity_id="ThermalZone:001", entity_type="ThermalZone" + ) # ToDo: Semantically connect the weather station and the building. By # adding a `hasWeatherStation` attribute of type `Relationship`. For the @@ -121,18 +110,17 @@ # create the context attribute for the building and add it to the # building entity has_weather_station = NamedContextAttribute( - name="hasWeatherStation", - type="Relationship", - value=weather_station.entity_name) + name="hasWeatherStation", type="Relationship", value=weather_station.entity_name + ) building.add_attributes(attrs=[has_weather_station]) # create a static attribute that connects the weather station to the # building cbc.update_entity(entity=building) - ref_building = StaticDeviceAttribute(name="refBuilding", - type="Relationship", - value=building.id) + ref_building = StaticDeviceAttribute( + name="refBuilding", type="Relationship", value=building.id + ) weather_station.add_attribute(ref_building) iotac.update_device(device=weather_station) @@ -144,9 +132,7 @@ # ToDo: Create a context attribute for the thermal zone and add it to the # thermal zone entity. - has_sensor = ... - - + has_sensor = ... thermal_zone.add_attributes(...) @@ -169,8 +155,6 @@ # thermal zone entity. has_heater = ... - - thermal_zone.add_attributes(...) # ToDo: Create a static attribute that connects the zone temperature zone to @@ -179,7 +163,6 @@ ref_thermal_zone = ... - heater.add_attribute(ref_thermal_zone) iotac.update_device(device=heater) @@ -189,12 +172,8 @@ # get code from Unit model for seconds code = Unit(name="second [unit of time]").code # add metadata to sim_time attribute of the all devices - metadata_sim_time = NamedMetadata(name="unitCode", - type="Text", - value=code) - attr_sim_time = weather_station.get_attribute( - attribute_name="sim_time" - ) + metadata_sim_time = NamedMetadata(name="unitCode", type="Text", value=code) + attr_sim_time = weather_station.get_attribute(attribute_name="sim_time") attr_sim_time.metadata = metadata_sim_time weather_station.update_attribute(attribute=attr_sim_time) zone_temperature_sensor.update_attribute(attribute=attr_sim_time) @@ -205,15 +184,12 @@ # ToDo: Add metadata to temperature attribute of the weather # station and the zone temperature sensor. metadata_t_amb = NamedMetadata(...) - attr_t_amb = weather_station.get_attribute( - attribute_name="temperature" - ) + attr_t_amb = weather_station.get_attribute(attribute_name="temperature") attr_t_amb.metadata = metadata_t_amb weather_station.update_attribute(attribute=attr_t_amb) metadata_t_zone = NamedMetadata(...) - attr_t_zone = zone_temperature_sensor.get_attribute( - attribute_name="...") + attr_t_zone = zone_temperature_sensor.get_attribute(attribute_name="...") attr_t_zone.metadata = ... zone_temperature_sensor.update_attribute(...) diff --git a/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot_solutions.py b/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot_solutions.py index 680a81ac..36ad005e 100644 --- a/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot_solutions.py +++ b/tutorials/ngsi_v2/e7_semantic_iot/e7_semantic_iot_solutions.py @@ -25,22 +25,13 @@ from pydantic import TypeAdapter # import from filip -from filip.clients.ngsi_v2 import \ - ContextBrokerClient, \ - IoTAClient +from filip.clients.ngsi_v2 import ContextBrokerClient, IoTAClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.base import NamedMetadata -from filip.models.ngsi_v2.context import \ - ContextEntity, \ - NamedContextAttribute -from filip.models.ngsi_v2.iot import \ - Device, \ - ServiceGroup, \ - StaticDeviceAttribute +from filip.models.ngsi_v2.context import ContextEntity, NamedContextAttribute +from filip.models.ngsi_v2.iot import Device, ServiceGroup, StaticDeviceAttribute from filip.models.ngsi_v2.units import Unit -from filip.utils.cleanup import \ - clear_context_broker, \ - clear_iot_agent +from filip.utils.cleanup import clear_context_broker, clear_iot_agent # ## Parameters # ToDo: Enter your context broker host and port, e.g. http://localhost:1026. @@ -53,36 +44,32 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it, an error will be raised! # FIWARE-Service -SERVICE = 'filip_tutorial' +SERVICE = "filip_tutorial" # FIWARE-Service path -SERVICE_PATH = '/' +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represents the "token" # for IoT devices to connect (send/receive data) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" # path to read json-files from previous exercises -READ_GROUPS_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_groups.json") -READ_DEVICES_FILEPATH = \ - Path("../e5_iot_thermal_zone_control_solution_devices.json") -READ_ENTITIES_FILEPATH = \ - Path("../e3_context_entities_solution_entities.json") +READ_GROUPS_FILEPATH = Path("../e5_iot_thermal_zone_control_solution_groups.json") +READ_DEVICES_FILEPATH = Path("../e5_iot_thermal_zone_control_solution_devices.json") +READ_ENTITIES_FILEPATH = Path("../e3_context_entities_solution_entities.json") # opening the files -with open(READ_GROUPS_FILEPATH, 'r') as groups_file, \ - open(READ_DEVICES_FILEPATH, 'r') as devices_file, \ - open(READ_ENTITIES_FILEPATH, 'r') as entities_file: +with open(READ_GROUPS_FILEPATH, "r") as groups_file, open( + READ_DEVICES_FILEPATH, "r" +) as devices_file, open(READ_ENTITIES_FILEPATH, "r") as entities_file: json_groups = json.load(groups_file) json_devices = json.load(devices_file) json_entities = json.load(entities_file) # ## Main script -if __name__ == '__main__': +if __name__ == "__main__": # create a fiware header object - fiware_header = FiwareHeader(service=SERVICE, - service_path=SERVICE_PATH) + fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) # clear the state of your service and scope clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header) clear_context_broker(url=CB_URL, fiware_header=fiware_header) @@ -108,10 +95,12 @@ # ToDo: Get context entities from the Context Broker # (exclude the IoT device ones). - building = cbc.get_entity(entity_id="urn:ngsi-ld:building:001", - entity_type="Building") - thermal_zone = cbc.get_entity(entity_id="ThermalZone:001", - entity_type="ThermalZone") + building = cbc.get_entity( + entity_id="urn:ngsi-ld:building:001", entity_type="Building" + ) + thermal_zone = cbc.get_entity( + entity_id="ThermalZone:001", entity_type="ThermalZone" + ) # ToDo: Semantically connect the weather station and the building. By # adding a `hasWeatherStation` attribute of type `Relationship`. For the @@ -121,18 +110,17 @@ # create the context attribute for the building and add it to the # building entity has_weather_station = NamedContextAttribute( - name="hasWeatherStation", - type="Relationship", - value=weather_station.entity_name) + name="hasWeatherStation", type="Relationship", value=weather_station.entity_name + ) building.add_attributes(attrs=[has_weather_station]) # create a static attribute that connects the weather station to the # building cbc.update_entity(entity=building) - ref_building = StaticDeviceAttribute(name="refBuilding", - type="Relationship", - value=building.id) + ref_building = StaticDeviceAttribute( + name="refBuilding", type="Relationship", value=building.id + ) weather_station.add_attribute(ref_building) iotac.update_device(device=weather_station) @@ -147,16 +135,17 @@ has_sensor = NamedContextAttribute( name="hasTemperatureSensor", type="Relationship", - value=zone_temperature_sensor.entity_name) + value=zone_temperature_sensor.entity_name, + ) thermal_zone.add_attributes(attrs=[has_sensor]) # ToDo: Create a static attribute that connects the zone temperature zone to # the thermal zone. cbc.update_entity(entity=thermal_zone) - ref_thermal_zone = StaticDeviceAttribute(name="refThermalZone", - type="Relationship", - value=thermal_zone.id) + ref_thermal_zone = StaticDeviceAttribute( + name="refThermalZone", type="Relationship", value=thermal_zone.id + ) zone_temperature_sensor.add_attribute(ref_thermal_zone) iotac.update_device(device=zone_temperature_sensor) @@ -169,18 +158,17 @@ # ToDo: Create a context attribute for the thermal zone and add it to the # thermal zone entity. has_heater = NamedContextAttribute( - name="hasHeater", - type="Relationship", - value=heater.entity_name) + name="hasHeater", type="Relationship", value=heater.entity_name + ) thermal_zone.add_attributes(attrs=[has_heater]) # ToDo: Create a static attribute that connects the zone temperature zone to # the thermal zone. cbc.update_entity(entity=thermal_zone) - ref_thermal_zone = StaticDeviceAttribute(name="refThermalZone", - type="Relationship", - value=thermal_zone.id) + ref_thermal_zone = StaticDeviceAttribute( + name="refThermalZone", type="Relationship", value=thermal_zone.id + ) heater.add_attribute(ref_thermal_zone) iotac.update_device(device=heater) @@ -190,12 +178,8 @@ # get code from Unit model for seconds code = Unit(name="second [unit of time]").code # add metadata to sim_time attribute of the all devices - metadata_sim_time = NamedMetadata(name="unitCode", - type="Text", - value=code) - attr_sim_time = weather_station.get_attribute( - attribute_name="sim_time" - ) + metadata_sim_time = NamedMetadata(name="unitCode", type="Text", value=code) + attr_sim_time = weather_station.get_attribute(attribute_name="sim_time") attr_sim_time.metadata = metadata_sim_time weather_station.update_attribute(attribute=attr_sim_time) zone_temperature_sensor.update_attribute(attribute=attr_sim_time) @@ -205,20 +189,13 @@ code = Unit(name="degree Celsius").code # ToDo: Add metadata to temperature attribute of the weather # station and the zone temperature sensor. - metadata_t_amb = NamedMetadata(name="unitCode", - type="Text", - value=code) - attr_t_amb = weather_station.get_attribute( - attribute_name="temperature" - ) + metadata_t_amb = NamedMetadata(name="unitCode", type="Text", value=code) + attr_t_amb = weather_station.get_attribute(attribute_name="temperature") attr_t_amb.metadata = metadata_t_amb weather_station.update_attribute(attribute=attr_t_amb) - metadata_t_zone = NamedMetadata(name="unitCode", - type="Text", - value=code) - attr_t_zone = zone_temperature_sensor.get_attribute( - attribute_name="temperature") + metadata_t_zone = NamedMetadata(name="unitCode", type="Text", value=code) + attr_t_zone = zone_temperature_sensor.get_attribute(attribute_name="temperature") attr_t_zone.metadata = metadata_t_zone zone_temperature_sensor.update_attribute(attribute=attr_t_zone) diff --git a/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language.py b/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language.py index ad44cb0a..5988e83b 100644 --- a/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language.py +++ b/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language.py @@ -21,6 +21,7 @@ # 3. Testing the expression language via MQTT messages # 4. Applying the expression language to device attributes in a multi-entity scenario """ + # Import packages import time import datetime @@ -28,9 +29,14 @@ from filip.clients.ngsi_v2 import IoTAClient, ContextBrokerClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.context import ContextEntity, NamedContextAttribute -from filip.models.ngsi_v2.iot import (Device, ServiceGroup, TransportProtocol, - PayloadProtocol, DeviceAttribute, - ExpressionLanguage) +from filip.models.ngsi_v2.iot import ( + Device, + ServiceGroup, + TransportProtocol, + PayloadProtocol, + DeviceAttribute, + ExpressionLanguage, +) from filip.utils.cleanup import clear_all from paho.mqtt import client as mqtt_client from paho.mqtt.client import CallbackAPIVersion @@ -50,15 +56,15 @@ # collisions. You will use this service through the whole tutorial. # If you forget to change it an error will be raised! # FIWARE Service -SERVICE = 'filip_tutorial' -SERVICE_PATH = '/' +SERVICE = "filip_tutorial" +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represent the "token" # for IoT devices to connect (send/receive data ) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" -if __name__ == '__main__': +if __name__ == "__main__": # FIWARE Header fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) @@ -70,11 +76,12 @@ cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # TODO: Setting expression language to JEXL at Service Group level - service_group1 = ServiceGroup(entity_type='Thing', - resource='/iot/json', - apikey=APIKEY, - #... - ) + service_group1 = ServiceGroup( + entity_type="Thing", + resource="/iot/json", + apikey=APIKEY, + # ... + ) iota_client.post_group(service_group=service_group1) # TODO: Create a device with two attributes 'location' and 'fillingLevel' that use @@ -82,26 +89,28 @@ # 'latitude' and 'level', while: # 1. 'location' is an array with 'longitude' and 'latitude'. # 2. 'fillingLevel' is 'level' divided by 100 - device1 = Device(device_id="waste_container_001", - entity_name="urn:ngsi-ld:WasteContainer:001", - entity_type="WasteContainer", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - #... - ) + device1 = Device( + device_id="waste_container_001", + entity_name="urn:ngsi-ld:WasteContainer:001", + entity_type="WasteContainer", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + # ... + ) iota_client.post_device(device=device1) # TODO: Setting expression language to JEXL at Device level with five attributes, while # 1. The attribute 'value' (Number) is itself multiplied by 5. The attribute # 2. 'consumption' (Text) is the trimmed version of the attribute 'spaces' (Text). # 3. The attribute 'iso_time' (Text) is the current 'timestamp' (Number) transformed into the ISO format. - device2 = Device(device_id="waste_container_002", - entity_name="urn:ngsi-ld:WasteContainer:002", - entity_type="WasteContainer", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - #... - ) + device2 = Device( + device_id="waste_container_002", + entity_name="urn:ngsi-ld:WasteContainer:002", + entity_type="WasteContainer", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + # ... + ) iota_client.post_device(device=device2) client = mqtt_client.Client(callback_api_version=CallbackAPIVersion.VERSION2) @@ -124,13 +133,15 @@ print(context_entity.model_dump_json(indent=4)) # Creating two SubWeatherStation entities - entity1 = ContextEntity(id="urn:ngsi-ld:SubWeatherStation:001", - type="SubWeatherStation") + entity1 = ContextEntity( + id="urn:ngsi-ld:SubWeatherStation:001", type="SubWeatherStation" + ) entity1.add_attributes(attrs=[NamedContextAttribute(name="vol", type="Number")]) cb_client.post_entity(entity1) - entity2 = ContextEntity(id="urn:ngsi-ld:SubWeatherStation:002", - type="SubWeatherStation") + entity2 = ContextEntity( + id="urn:ngsi-ld:SubWeatherStation:002", type="SubWeatherStation" + ) entity2.add_attributes(attrs=[NamedContextAttribute(name="vol", type="Number")]) cb_client.post_entity(entity2) @@ -139,13 +150,14 @@ # 'v1' and 'v2' are multiplied by 100 and should be linked with entities of # the SubWeatherStation. # The name of each attribute is 'vol'. - device3 = Device(device_id="weather_station_001", - entity_name="urn:ngsi-ld:WeatherStation:001", - entity_type="WeatherStation", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - #... - ) + device3 = Device( + device_id="weather_station_001", + entity_name="urn:ngsi-ld:WeatherStation:001", + entity_type="WeatherStation", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + # ... + ) iota_client.post_device(device=device3) client = mqtt_client.Client(callback_api_version=CallbackAPIVersion.VERSION2) @@ -161,6 +173,7 @@ time.sleep(2) # Printing context entities of OCB - for context_entity in cb_client.get_entity_list(entity_types=["WeatherStation", - "SubWeatherStation"]): + for context_entity in cb_client.get_entity_list( + entity_types=["WeatherStation", "SubWeatherStation"] + ): print(context_entity.model_dump_json(indent=4)) diff --git a/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language_solution.py b/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language_solution.py index b826d288..fda78712 100644 --- a/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language_solution.py +++ b/tutorials/ngsi_v2/e8_multientity_and_expression_language/e8_multientity_and_expression_language_solution.py @@ -21,6 +21,7 @@ # 3. Testing the expression language via MQTT messages # 4. Applying the expression language to device attributes in a multi-entity scenario """ + # Import packages import time import datetime @@ -28,9 +29,14 @@ from filip.clients.ngsi_v2 import IoTAClient, ContextBrokerClient from filip.models.base import FiwareHeader from filip.models.ngsi_v2.context import ContextEntity, NamedContextAttribute -from filip.models.ngsi_v2.iot import (Device, ServiceGroup, TransportProtocol, - PayloadProtocol, DeviceAttribute, - ExpressionLanguage) +from filip.models.ngsi_v2.iot import ( + Device, + ServiceGroup, + TransportProtocol, + PayloadProtocol, + DeviceAttribute, + ExpressionLanguage, +) from filip.utils.cleanup import clear_all from paho.mqtt import client as mqtt_client from paho.mqtt.client import CallbackAPIVersion @@ -46,15 +52,15 @@ MQTT_BROKER_PORT = 1883 # FIWARE Service -SERVICE = 'filip_tutorial' -SERVICE_PATH = '/' +SERVICE = "filip_tutorial" +SERVICE_PATH = "/" # ToDo: Change the APIKEY to something unique. This represent the "token" # for IoT devices to connect (send/receive data ) with the platform. In the # context of MQTT, APIKEY is linked with the topic used for communication. -APIKEY = 'your_apikey' +APIKEY = "your_apikey" -if __name__ == '__main__': +if __name__ == "__main__": # FIWARE Header fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH) @@ -66,10 +72,12 @@ cb_client = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header) # TODO: Setting expression language to JEXL at Service Group level - service_group1 = ServiceGroup(entity_type='Thing', - resource='/iot/json', - apikey=APIKEY, - expressionLanguage=ExpressionLanguage.JEXL) + service_group1 = ServiceGroup( + entity_type="Thing", + resource="/iot/json", + apikey=APIKEY, + expressionLanguage=ExpressionLanguage.JEXL, + ) iota_client.post_group(service_group=service_group1) # TODO: Create a device with two attributes 'location' and 'fillingLevel' that use @@ -77,42 +85,47 @@ # 'latitude' and 'level', while: # 1. 'location' is an array with 'longitude' and 'latitude'. # 2. 'fillingLevel' is 'level' divided by 100 - device1 = Device(device_id="waste_container_001", - entity_name="urn:ngsi-ld:WasteContainer:001", - entity_type="WasteContainer", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - attributes=[DeviceAttribute(name="latitude", type="Number"), - DeviceAttribute(name="longitude", type="Number"), - DeviceAttribute(name="level", type="Number"), - DeviceAttribute(name="location", type="Array", - expression="[longitude, latitude]"), - DeviceAttribute(name="fillingLevel", type="Number", - expression="level / 100") - ] - ) + device1 = Device( + device_id="waste_container_001", + entity_name="urn:ngsi-ld:WasteContainer:001", + entity_type="WasteContainer", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + attributes=[ + DeviceAttribute(name="latitude", type="Number"), + DeviceAttribute(name="longitude", type="Number"), + DeviceAttribute(name="level", type="Number"), + DeviceAttribute( + name="location", type="Array", expression="[longitude, latitude]" + ), + DeviceAttribute( + name="fillingLevel", type="Number", expression="level / 100" + ), + ], + ) iota_client.post_device(device=device1) # TODO: Setting expression language to JEXL at Device level with five attributes, while # 1. The attribute 'value' (Number) is itself multiplied by 5. The attribute # 2. 'consumption' (Text) is the trimmed version of the attribute 'spaces' (Text). # 3. The attribute 'iso_time' (Text) is the current 'timestamp' (Number) transformed into the ISO format. - device2 = Device(device_id="waste_container_002", - entity_name="urn:ngsi-ld:WasteContainer:002", - entity_type="WasteContainer", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - expressionLanguage=ExpressionLanguage.JEXL, - attributes=[DeviceAttribute(name="value", type="Number", - expression="5 * value"), - DeviceAttribute(name="spaces", type="Text"), - DeviceAttribute(name="consumption", type="Text", - expression="spaces|trim"), - DeviceAttribute(name="timestamp", type="Number"), - DeviceAttribute(name="iso_time", type="Text", - expression="timestamp|toisodate"), - ] - ) + device2 = Device( + device_id="waste_container_002", + entity_name="urn:ngsi-ld:WasteContainer:002", + entity_type="WasteContainer", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + expressionLanguage=ExpressionLanguage.JEXL, + attributes=[ + DeviceAttribute(name="value", type="Number", expression="5 * value"), + DeviceAttribute(name="spaces", type="Text"), + DeviceAttribute(name="consumption", type="Text", expression="spaces|trim"), + DeviceAttribute(name="timestamp", type="Number"), + DeviceAttribute( + name="iso_time", type="Text", expression="timestamp|toisodate" + ), + ], + ) iota_client.post_device(device=device2) client = mqtt_client.Client(callback_api_version=CallbackAPIVersion.VERSION2) @@ -121,13 +134,17 @@ client.loop_start() # TODO: Publish attributes 'level', 'longitude' and 'latitude' of device1 - client.publish(topic=f'/json/{APIKEY}/{device1.device_id}/attrs', - payload='{"level": 99, "longitude": 12.0, "latitude": 23.0}') + client.publish( + topic=f"/json/{APIKEY}/{device1.device_id}/attrs", + payload='{"level": 99, "longitude": 12.0, "latitude": 23.0}', + ) # TODO: Publish attributes 'value', 'spaces' and 'timestamp' (in ms) of device2 - client.publish(topic=f'/json/{APIKEY}/{device2.device_id}/attrs', - payload=f'{{ "value": 10, "spaces": " foobar ",' - f' "timestamp": {datetime.datetime.now().timestamp() * 1000} }}') + client.publish( + topic=f"/json/{APIKEY}/{device2.device_id}/attrs", + payload=f'{{ "value": 10, "spaces": " foobar ",' + f' "timestamp": {datetime.datetime.now().timestamp() * 1000} }}', + ) client.disconnect() @@ -138,13 +155,15 @@ print(context_entity.model_dump_json(indent=4)) # Creating two SubWeatherStation entities - entity1 = ContextEntity(id="urn:ngsi-ld:SubWeatherStation:001", - type="SubWeatherStation") + entity1 = ContextEntity( + id="urn:ngsi-ld:SubWeatherStation:001", type="SubWeatherStation" + ) entity1.add_attributes(attrs=[NamedContextAttribute(name="vol", type="Number")]) cb_client.post_entity(entity1) - entity2 = ContextEntity(id="urn:ngsi-ld:SubWeatherStation:002", - type="SubWeatherStation") + entity2 = ContextEntity( + id="urn:ngsi-ld:SubWeatherStation:002", type="SubWeatherStation" + ) entity2.add_attributes(attrs=[NamedContextAttribute(name="vol", type="Number")]) cb_client.post_entity(entity2) @@ -153,24 +172,35 @@ # 'v1' and 'v2' are multiplied by 100 and should be linked with entities of # the SubWeatherStation. # The name of each attribute is 'vol'. - device3 = Device(device_id="weather_station_001", - entity_name="urn:ngsi-ld:WeatherStation:001", - entity_type="WeatherStation", - transport=TransportProtocol.MQTT, - protocol=PayloadProtocol.IOTA_JSON, - expressionLanguage=ExpressionLanguage.JEXL, - attributes=[DeviceAttribute(object_id="v1", name="vol", type="Number", - expression="100 * v1", - entity_name="urn:ngsi-ld:SubWeatherStation:001", - entity_type="SubWeatherStation"), - DeviceAttribute(object_id="v2", name="vol", type="Number", - expression="100 * v2", - entity_name="urn:ngsi-ld:SubWeatherStation:002", - entity_type="SubWeatherStation"), - DeviceAttribute(object_id="v", name="vol", type="Number", - expression="100 * v") - ] - ) + device3 = Device( + device_id="weather_station_001", + entity_name="urn:ngsi-ld:WeatherStation:001", + entity_type="WeatherStation", + transport=TransportProtocol.MQTT, + protocol=PayloadProtocol.IOTA_JSON, + expressionLanguage=ExpressionLanguage.JEXL, + attributes=[ + DeviceAttribute( + object_id="v1", + name="vol", + type="Number", + expression="100 * v1", + entity_name="urn:ngsi-ld:SubWeatherStation:001", + entity_type="SubWeatherStation", + ), + DeviceAttribute( + object_id="v2", + name="vol", + type="Number", + expression="100 * v2", + entity_name="urn:ngsi-ld:SubWeatherStation:002", + entity_type="SubWeatherStation", + ), + DeviceAttribute( + object_id="v", name="vol", type="Number", expression="100 * v" + ), + ], + ) iota_client.post_device(device=device3) client = mqtt_client.Client(callback_api_version=CallbackAPIVersion.VERSION2) @@ -179,14 +209,17 @@ client.loop_start() # TODO: Publish values to all attributes of device3 - client.publish(topic=f'/json/{APIKEY}/{device3.device_id}/attrs', - payload='{"v1": 10, "v2": 20, "v": 30}') + client.publish( + topic=f"/json/{APIKEY}/{device3.device_id}/attrs", + payload='{"v1": 10, "v2": 20, "v": 30}', + ) client.disconnect() time.sleep(2) # Printing context entities of OCB - for context_entity in cb_client.get_entity_list(entity_types=["WeatherStation", - "SubWeatherStation"]): + for context_entity in cb_client.get_entity_list( + entity_types=["WeatherStation", "SubWeatherStation"] + ): print(context_entity.model_dump_json(indent=4)) diff --git a/tutorials/ngsi_v2/simulation_model.py b/tutorials/ngsi_v2/simulation_model.py index 82821c05..ea2bceb1 100644 --- a/tutorials/ngsi_v2/simulation_model.py +++ b/tutorials/ngsi_v2/simulation_model.py @@ -1,6 +1,7 @@ """ Simulation model to provide dynamic data throughout the tutorial """ + from math import cos import numpy as np @@ -26,13 +27,16 @@ class SimulationModel: temp_min: minimal ambient temperature in °C temp_start: initial zone temperature in °C """ - def __init__(self, - t_start: int = 0, - t_end: int = 24 * 60 * 60, - dt: int = 1, - temp_max: float = 10, - temp_min: float = -5, - temp_start: float = 20): + + def __init__( + self, + t_start: int = 0, + t_end: int = 24 * 60 * 60, + dt: int = 1, + temp_max: float = 10, + temp_min: float = -5, + temp_start: float = 20, + ): self.t_start = t_start self.t_end = t_end @@ -63,13 +67,20 @@ def do_step(self, t_sim: int): t_zone: zone temperature in °C """ for t in range(self.t_sim, t_sim, self.dt): - self.t_zone = self.t_zone + \ - self.dt * (self.ua * (self.t_amb - self.t_zone) + - self.on_off * self.q_h) / self.c_p + self.t_zone = ( + self.t_zone + + self.dt + * (self.ua * (self.t_amb - self.t_zone) + self.on_off * self.q_h) + / self.c_p + ) - self.t_amb = -(self.temp_max - self.temp_min) / 2 * \ - cos(2 * np.pi * t /(24 * 60 * 60)) + \ - self.temp_min + (self.temp_max - self.temp_min) / 2 + self.t_amb = ( + -(self.temp_max - self.temp_min) + / 2 + * cos(2 * np.pi * t / (24 * 60 * 60)) + + self.temp_min + + (self.temp_max - self.temp_min) / 2 + ) self.t_sim = t_sim