diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2a75f2c1d51eaf2227c79aeb2fc9c79ee028bb8f..d09a3703e262914a1fa1c468045221d889aec5ab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,21 +32,23 @@ before_script: test: - tags: + tags: - env:docker script: #- python setup.py test - - pip install flake8 # you can also use tox + - pip install flake8==5.0.4 flake8-nb==0.5.2 # you can also use tox - pwd - ls -lh - - flake8 ./trial_json.py + - flake8 --max-line-length 88 ./functions/*.py + - flake8-nb --max-line-length 88 ausarbeitung.ipynb -run: - tags: - - env:docker - script: - - pip install -r requirements.txt - - python trial_json.py + +#run: + #tags: + #- env:docker + #script: + #- pip install -r requirements.txt + #- python trial_json.py # an alternative approach is to install and run: # - pip install dist/* # run the command here @@ -55,7 +57,7 @@ run: # - dist/*.whl #pages: -# tags: +# tags: # - env:docker # script: # - pip install sphinx sphinx-rtd-theme diff --git a/README.md b/README.md index 1056af8d37e38368082cb14b287ac87d2976bdc2..1d2b414165a21e5946fcb03f6247a186becd7480 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ # Lerneinheit FAIRe Qualitäts-KPIs ## Einführung +Siehe Aufgabenstellung in Moodle. ## Materialien +Die Aufgabenstellung im PDF-Format steht in Moodle bereit. + +In diesem GitLab Repo finden Sie: +- Package *functions* (`functions/`): Beinhaltet die Module *classes* und *calculation_rules* +- Modul classes (`functions/classes.py`): Werkzeuge zum Aufbau und zur Bearbeitung der LEGO Konstruktionen +- Modul calculation_rules (`functions/calculation_rules.py`): Funktionen zum Berechnen der FAIR Quality KPIs +- Datenblätter (`datasheets/`): Datenblätter ausgewählter LEGO Komponenten im JSON-Format +- Jupyter Notebook (`ausarbeitung.ipynb`): Zur Bearbeitung der Aufgaben und Abgabe. Beinhaltet ein Minimalbeispiel zur Verwendung der Werkzeuge. -## Anwendung ## Ausarbeitung Die Ausarbeitung erfolgt im Notebook `ausarbeitung.ipynb`. In diesem ist bereits eine Gliederung vorgegeben. diff --git a/ausarbeitung.ipynb b/ausarbeitung.ipynb index 44ca8bb1f1d7347f00823cd0523a9ea2c7505866..1ecb07536961fc57a727edc9ed7cfec5cb21ab03 100644 --- a/ausarbeitung.ipynb +++ b/ausarbeitung.ipynb @@ -5,7 +5,7 @@ "id": "c9328cd1", "metadata": {}, "source": [ - "# FAIRe Qualitäts-KPIs\n", + "# FAIR Quality KPIs\n", "Name:\n", "Datum:" ] @@ -16,28 +16,43 @@ "metadata": {}, "source": [ "## Einführung\n", - "FAIRe Qualitäts-KPIs schaffen eine transparente und nachvollziehbare Entscheidungsgrundlage. In dieser Lerneinheit sollen sie die Methodik erlernen, indem sie LEGO-Autos zusammenbauen, deren Qualitäts-KPIs bestimmen und miteinander vergleichen.\n", + "FAIR Quality KPIs schaffen eine transparente und nachvollziehbare Entscheidungsgrundlage. In dieser Lerneinheit sollen sie die Methodik erlernen, indem sie LEGO Autos zusammenbauen, deren Quality KPIs bestimmen und miteinander vergleichen.\n", "\n", "### Werkzeug für den Zusammenbau\n", "Der Zusammenbau der Autos erfolgt virtuell. Als Werkzeug steht Ihnen das Modul `classes` zur Verfügung. In diesem sind unterschiedliche Klassen und Methoden implementiert (Objektorientierung). Sie erzeugen jeweils Objekte der Klassen. Ein Objekt ist dabei die virtuelle Abbildung eines realen Bauteiles. Die Eigenschaften des Bauteiles werden über die Eigenschaften des Objektes abgebildet.\n", "\n", - "### Berechnung der Qualitäts-KPIs\n", - "KPIs (Key Performance Indikatoren) sind Kenngrößen des Systems. Sie werden über Berechnungsvorschriften (`calculation_rules`) bestimmt. Diese Berechnungsvorschriften sind von Ihnen als python-Funktionen im Modul `calculation_results` zu implementieren.\n", + "### Berechnung der Quality KPIs\n", + "KPIs (Key Performance Indikatoren) sind Kenngrößen des Systems. Sie werden über Berechnungsvorschriften bestimmt. Diese Berechnungsvorschriften sind von Ihnen als python-Funktionen im Modul `calculation_rules` zu implementieren.\n", "\n", "### Datenblätter\n", "Für den Zusammenbau stehen Ihnen verschiedene Bauteile in unterschiedlichen Ausführungen zur Verfügung. Sie nutzen die Datenblätter, die als `json-Dateien` zur Verfügung gestellt werden, um Ihre Autos zusammenzubauen." ] }, { + "attachments": {}, "cell_type": "markdown", "id": "1c702114", "metadata": {}, "source": [ "## Eigene Module und Minimalbeispiel\n", - "Für die Ausarbeitung nutzen wir zwei eigene Module (Python-Skripte), die im Ordner `functions` gespeichert sind.\n", - "Das Modul `calculation_rules`erweitern Sie während der Ausarbeitung. Um die Änderungen zu nutzen, müssen Sie das Notebook neu starten.\n", + "Für die Ausarbeitung nutzen wir zwei eigene Module (Python-Dateien), die im Ordner `functions` gespeichert sind.\n", + "Das Modul `calculation_rules` erweitern Sie während der Ausarbeitung. Um die Änderungen zu nutzen, müssen Sie das Notebook neu starten.\n", + "Im Modul `classes` befinden sich die komplette Klassen und Funktionen zur Verwendung.\n", "\n", - "Mit einem Minimalbeispiel wird Ihnen gezeigt, wie sie die Module nutzen." + "Mit einem Minimalbeispiel wird Ihnen gezeigt, wie sie die Module nutzen. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6d3310be", + "metadata": {}, + "source": [ + "### Modul classes\n", + "Enthält `LegoComponent, LegoAssembly, AggregationLayer, KPIEncoder` und die Funktion `print_assembly_tree`. \n", + "`LegoComponent` bildet einzelne Komponenten ab, während `LegoAssembly` zusammengesetzte Aggregationsebenen abdeckt, also Bauteil, Baugruppe und System. Zur Unterscheidung dient die Klasse Aggregationlayer, diese ist für `LegoComponent` immer `Component` (Komponente), muss für `LegoAssembly` entsprechend auf `SYSTEM`(System) , `ASSEMBLY`(Baugruppe) oder `SUBASSEMBLY`(Bauteil) gesetzt werden.\n", + "\n", + "Wir bauen aus Achse, Rahmen und Reifen einen Tretroller zusammen." ] }, { @@ -48,25 +63,171 @@ "outputs": [], "source": [ "# import modules\n", + "import json\n", + "import pprint\n", "from functions import calculation_rules\n", - "from functions import classes\n", "\n", - "# TO DO import further modules if necessary\n" + "# Importing all modules one by one to provide an overview\n", + "# The next commented line would provide the same result in one line\n", + "# from functions.classes import *\n", + "from functions.classes import LegoComponent\n", + "from functions.classes import LegoAssembly\n", + "from functions.classes import AggregationLayer\n", + "from functions.classes import KPIEncoder\n", + "from functions.classes import print_assembly_tree\n", + "\n", + "# When you are writing code yourself later, you might want to copy\n", + "# these imports to avoid rerunning the full notebook after restart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b1f9aff", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the wheels and axles as single components first\n", + "# Look up the specific item you want from the provided json files,\n", + "# we can load the full file into a dict\n", + "with open(\"datasheets/axles.json\") as json_file:\n", + " axles = json.load(json_file)\n", + "# Pick a specific axle via its 'item number'\n", + "print(axles[\"32073\"])\n", + "\n", + "# Create the component with the dict:\n", + "front_axle = LegoComponent(\"front axle\", axles[\"32073\"])\n", + "# Both label and the data dict are optional parameters.\n", + "# You can view, add or edit the properties just any dict:\n", + "print(front_axle.properties[\"label\"])\n", + "front_axle.properties[\"color\"] = \"grey\"\n", + "\n", + "\n", + "# Create the second axle\n", + "back_axle = LegoComponent()\n", + "back_axle.properties[\"label\"] = \"back axle\"\n", + "back_axle.properties.update(axles[\"32073\"])\n", + "# Do not use = here, otherwise you'd overwrite existing properties.\n", + "# Instead use update to have all entries added to properties\n", + "back_axle.properties[\"color\"] = \"grey\"\n", + "# Viewing dicts in one line is not easy to read,\n", + "# a better output comes with pretty print (pprint):\n", + "pprint.pprint(back_axle.properties)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4a8e8c8", + "metadata": {}, + "outputs": [], + "source": [ + "# Now wheels\n", + "with open(\"datasheets/wheels.json\") as json_file:\n", + " wheels = json.load(json_file)\n", + "# Adding the color here already as dict, and the surface as key-value argument.\n", + "# Multiple of both parameters are supported, but only in this order.\n", + "# front_wheel = LegoComponent(\n", + "# 'front wheel', wheels['2903'], {'color':'yellow'},winter='true')\n", + "\n", + "front_wheel = LegoComponent(\n", + " \"front wheel\", wheels[\"2903\"], surface=\"rough\", paint=\"glossy\"\n", + ")\n", + "pprint.pprint(front_wheel.properties)\n", + "\n", + "# We included a clone function to both Lego classes, so you can easily\n", + "# create duplicate objects. Passing the new label is optional\n", + "back_wheel = front_wheel.clone(\"back wheel\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25bd06c5", + "metadata": {}, + "outputs": [], + "source": [ + "# Create subassemblies and add the wheels and axles\n", + "\n", + "# The AggregationLayer must be used, passing the label\n", + "# or additional properties is optional\n", + "front_wheel_assembly = LegoAssembly(\n", + " AggregationLayer.SUBASSEMBLY,\n", + " \"front wheel assembly\",\n", + " assembly_method=\"stick together like lego blocks\",\n", + ")\n", + "# Add LegoComponents to the LegoAssembly, single or as list\n", + "front_wheel_assembly.add([front_wheel, front_axle])\n", + "# You can access the components of an assembly like this:\n", + "print(front_wheel_assembly.components[1].properties[\"label\"])\n", + "\n", + "# Assemblies can be cloned as well (including their children),\n", + "# but don't forget to adjust labels or you might be stuck with\n", + "# a 'front wheel' in your 'back wheel assembly'\n", + "\n", + "# Stick together back wheel parts\n", + "back_wheel_assembly = LegoAssembly(\n", + " AggregationLayer.SUBASSEMBLY, \"back wheel assembly\"\n", + " )\n", + "back_wheel_assembly.add([back_wheel, back_axle])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b6648e1", + "metadata": {}, + "outputs": [], + "source": [ + "# Create frame component and assemble the system\n", + "\n", + "with open(\"datasheets/frame.json\") as json_file:\n", + " frame = json.load(json_file)\n", + "scooter_frame = LegoComponent(\"scooter frame\", frame[\"3702\"], {\"color\": \"red\"})\n", + "\n", + "# The scooter is our system level assembly, you can choose the AggregationLayer\n", + "# But the hiercarchy (SYSTEM > ASSEMBLY > SUBASSEMBLY) must be in order.\n", + "# Components can be added to all LegoAssembly objects\n", + "\n", + "scooter = LegoAssembly(\n", + " AggregationLayer.SYSTEM,\n", + " \"scooter\",\n", + " manufacturer=\"FST\",\n", + " comment=\"Faster! Harder! Scooter!\",\n", + ")\n", + "# add frame and subassemblies\n", + "scooter.add([scooter_frame, front_wheel_assembly, back_wheel_assembly])" ] }, { + "cell_type": "code", + "execution_count": null, + "id": "71324895", + "metadata": {}, + "outputs": [], + "source": [ + "# Look at the assembly\n", + "\n", + "# We can get all LegoComponents from this assembly.\n", + "# Without parameter 'max_depth' only direct children will be listed.\n", + "scooter.get_component_list(5)" + ] + }, + { + "attachments": {}, "cell_type": "markdown", - "id": "3b69752c", + "id": "001f1c77", "metadata": {}, "source": [ - "### Modul calculation_results\n", - "Sie können die unterschiedlichen Funktionen (Berechnungsvorschriften) aufrufen. Beachten Sie dabei die Übergabe- und Rückgabewerte." + "### Modul calculation_rules\n", + "\n", + "Um für unser System \"Tretroller\" ein KPI für das Gesamtgewicht zu erzeugen, wurde in `functions.calculation_rules` die Funktion `kpi_sum` definiert. Zusammen mit den Hilfsfunktionen der Klasse können wir nun das KPI Gewicht für das System hinzufügen. Die Massen der einzelnen Komponenten sind in den Datenblättern unter `mass [g]` enthalten." ] }, { "cell_type": "code", "execution_count": null, - "id": "294c680b", + "id": "7b60d0fb", "metadata": {}, "outputs": [], "source": [ @@ -75,29 +236,106 @@ ] }, { - "cell_type": "markdown", - "id": "a2c87a3c", + "cell_type": "code", + "execution_count": null, + "id": "fe4edad6", "metadata": {}, + "outputs": [], "source": [ - "### Modul classes\n", - "Wir bauen ein Auto zusammen." + "# Add mass to assemblies\n", + "\n", + "# List all components' mass\n", + "combined_weight = 0\n", + "for c in scooter.get_component_list(-1):\n", + " combined_weight += c.properties[\"mass [g]\"]\n", + "print(\"Gesamtgewicht: \", combined_weight, \"g\")\n", + "# Add KPI to system\n", + "scooter.properties[\"mass [g]\"] = combined_weight\n", + "\n", + "# We can also add the mass to the subassemblies.\n", + "# children() returns a dict with a list of added\n", + "# components and assemblies.\n", + "for a in scooter.children()[\"assemblies\"]:\n", + " a_mass = 0\n", + " for c in a.get_component_list(-1):\n", + " a_mass += c.properties[\"mass [g]\"]\n", + " a.properties[\"mass [g]\"] = a_mass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c26b36c8", + "metadata": {}, + "outputs": [], + "source": [ + "scooter.get_component_list(-1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d56419f", + "metadata": {}, + "outputs": [], + "source": [ + "# Look at the full assembly with KPI\n", + "\n", + "# Print the full assembly with all levels\n", + "print_assembly_tree(scooter)" ] }, { "cell_type": "code", "execution_count": null, - "id": "8db386db", + "id": "b31416d3", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "# Dump to json file with to_dict() and KPIEncoder\n", + "with open(\"scooter.json\", \"w\") as fp:\n", + " json.dump(scooter.to_dict(), fp, cls=KPIEncoder)\n", + "# full view is too big for Juypter (try it)\n", + "# pprint.pprint(scooter.to_dict())" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "53793ae8", + "metadata": {}, + "source": [ + "In dieser exportierten json-Datei ('scooter.json') sind die Werte maschinen- und menschenlesbar hinterlegt.\n", + "Zusammen mit der Berechnungsvorschrift in `calculation_rules` ist auch die Entstehung des KPI nachvollziehbar und wiederverwendbar dokumentiert und damit 'FAIR'." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "92c77051", + "metadata": {}, + "source": [ + "#### Zusätzliche Details" + ] }, { "cell_type": "code", "execution_count": null, - "id": "8f2ae9f4", + "id": "b91fed73", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "# Each child knows its parent\n", + "first_child = scooter.children()[\"assemblies\"][0]\n", + "print(\"Child:\", first_child)\n", + "print(\"Parent:\", first_child.parent)\n", + "# Also we can access the \"top\" parent\n", + "latest_child = first_child.children()[\"components\"][0]\n", + "print(\"Top parent: \", latest_child.get_root_assembly())\n", + "\n", + "# Each part created has a unique identifier (the long number in [])\n", + "# Don't try add the identical part again." + ] }, { "cell_type": "markdown", @@ -352,7 +590,7 @@ "metadata": { "hide_input": false, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -366,7 +604,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.0" + "version": "3.11.0" }, "varInspector": { "cols": { @@ -396,6 +634,11 @@ "_Feature" ], "window_display": false + }, + "vscode": { + "interpreter": { + "hash": "386d359a8531ffdc4805ead3a16e7983e89a5ab7bba0cbec0e7ad9597b7a2b64" + } } }, "nbformat": 4, diff --git a/datasheets/axles.json b/datasheets/axles.json new file mode 100644 index 0000000000000000000000000000000000000000..50d3abf8507f5354846010f27accc2a817858f02 --- /dev/null +++ b/datasheets/axles.json @@ -0,0 +1,82 @@ +{ + "32073":{ + "item number":32073, + "item description":"Axle 5 studs", + "category":"axle", + "price [Euro]":0.001, + "mass [g]":0.66, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32073#T=S&O={%22iconly%22:0}", + "dimension [studs]":5 + }, + "44294":{ + "item number":44294, + "item description":"Axle 7 studs", + "category":"axle", + "price [Euro]":0.01, + "mass [g]":1.05, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=44294#T=S&O={%22iconly%22:0}", + "dimension [studs]":7 + }, + "3707":{ + "item number":3707, + "item description":"Axle 8 studs", + "category":"axle", + "price [Euro]":0.01, + "mass [g]":1.18, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3707#T=S&O={%22iconly%22:0}", + "dimension [studs]":8 + }, + "60485":{ + "item number":60485, + "item description":"Axle 9 studs", + "category":"axle", + "price [Euro]":0.01, + "mass [g]":1.3, + "delivery time [days]":7, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=60485#T=S&O={%22iconly%22:0}", + "dimension [studs]":9 + }, + "3737":{ + "item number":3737, + "item description":"Axle 10 studs", + "category":"axle", + "price [Euro]":0.01, + "mass [g]":1.49, + "delivery time [days]":7, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3737#T=S&O={%22iconly%22:0}", + "dimension [studs]":10 + }, + "23948":{ + "item number":23948, + "item description":"Axle 11 studs", + "category":"axle", + "price [Euro]":0.15, + "mass [g]":1.65, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=23948#T=S&O={%22iconly%22:0}", + "dimension [studs]":11 + }, + "3708":{ + "item number":3708, + "item description":"Axle 12 studs", + "category":"axle", + "price [Euro]":0.02, + "mass [g]":1.82, + "delivery time [days]":7, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3708#T=S&O={%22iconly%22:0}", + "dimension [studs]":12 + }, + "50451":{ + "item number":50451, + "item description":"Axle 16 studs", + "category":"axle", + "price [Euro]":0.75, + "mass [g]":2.37, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=50451#T=S&O={%22iconly%22:0}", + "dimension [studs]":16 + } +} \ No newline at end of file diff --git a/datasheets/batteries.json b/datasheets/batteries.json new file mode 100644 index 0000000000000000000000000000000000000000..6535f15c1578fb37549a9c69311194510d1e7682 --- /dev/null +++ b/datasheets/batteries.json @@ -0,0 +1,68 @@ +{ + "8878-1":{ + "item number":"8878-1", + "item description":"Power Functions Rechargeable Battery Box", + "category":"battery", + "price [Euro]":55, + "mass [g]":83.94, + "delivery time [days]":8, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?S=8878-1#T=S&O={%22iconly%22:0}", + "output voltage [V]":7.4, + "dimensions [cm]":"15 x 15 x 3,5" + }, + "8881-1":{ + "item number":"8881-1", + "item description":"Power Functions Battery Box", + "category":"battery", + "price [Euro]":12, + "mass [g]":179.4, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?S=8881-1#T=S&O={%22iconly%22:0}", + "output voltage [V]":9.0, + "dimensions [cm]":"8,8 x 6,3 x 3,2" + }, + "88000-1":{ + "item number":"88000-1", + "item description":"Lego AAA Battery Box", + "category":"battery", + "price [Euro]":30, + "mass [g]":97.24, + "delivery time [days]":6, + "data source":"https:\/\/www.brickowl.de\/catalog\/lego-aaa-battery-box-set-88000", + "output voltage [V]":9.0, + "dimensions [cm]":"15,3 x 15,1 x 3,9" + }, + "2847c01":{ + "item number":"2847c01", + "item description":"Electric 9V Battery Box", + "category":"battery", + "price [Euro]":2, + "mass [g]":172.7, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=2847c01&name=Electric%209V%20Battery%20Box%204%20x%2014%20x%204%20with%20Red%20Buttons%20and%20Contact%20Plate%20with%20Dark%20Gray%20Base%20(2846%20\/%202847c00)&category=%5BElectric,%20Battery%20Box%5D#T=S&O={%22iconly%22:0}", + "output voltage [V]":9.0, + "dimensions [cm]":"11,2 x 3,2 x 3,2" + }, + "59510":{ + "item number":59510, + "item description":"LEGO Power Functions Battery Box ", + "category":"battery", + "price [Euro]":18, + "mass [g]":165.3, + "delivery time [days]":3, + "data source":"https:\/\/www.brickowl.de\/catalog\/lego-power-functions-battery-box-with-beam-connectors-with-on-off-sticker-59510", + "output voltage [V]":9.0, + "dimensions [cm]":"8,8 x 6,4 x 3,2" + }, + "84599":{ + "item number":84599, + "item description":"Electric 9V Power Functions Battery Box (Rechargeable)", + "category":"battery", + "price [Euro]":120, + "mass [g]":82.0, + "delivery time [days]":8, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=84599&name=Electric%209V%20Battery%20Box%20Power%20Functions%20(Rechargeable)%20with%20Dark%20Bluish%20Gray%20Bottom&category=%5BElectric,%20Battery%20Box%5D#T=S&O={%22iconly%22:0}", + "output voltage [V]":9.0, + "dimensions [cm]":"15 x 15 x 3,5" + } +} \ No newline at end of file diff --git a/datasheets/create_json_from_excel.py b/datasheets/create_json_from_excel.py new file mode 100644 index 0000000000000000000000000000000000000000..0d74bde458406289de3710093fb6b24559607be2 --- /dev/null +++ b/datasheets/create_json_from_excel.py @@ -0,0 +1,138 @@ +"""Create json metadata files from an excel file.""" +import os +import sys +from collections import namedtuple +from typing import Dict, List, NamedTuple + +import pandas as pd +# pandas is using another dependency called openpyxl +# both need to be installed + + +COLUMN_FOR_INDEX_IN_EXCEL = 0 +KEYWORD_TO_IGNORE_COLUMN = "ignore" +KEYWORD_TO_IGNORE_SHEET = "ignore" +HELP_TXT = """ + 1. The source excel file. (e.g. test.xlsx) + 2. The destination folder for the json file. (e.g. test_folder) + 3. Override the folder if existing. (OPTIONAL, False) + + Providing only '-- help' will show this text and exit. +""" + + +def read_terminal_arguments() -> NamedTuple: + """Read terminal arguments. + + 1. File to read from. + 2. Name of the folder to store the json files. + 3. Override the destination folder if existing, defaults False. + """ + terminal_args = namedtuple( + "terminal_args", + ["source", "destination", "override"], + defaults=["test.xlsx", "test", True] + ) + if len(sys.argv) == 1: # No arguments passed. + return terminal_args() # noqa + if sys.argv[1] in ["help", "--help", "-h", "?", "h"]: + print(HELP_TXT) + raise SystemExit + try: + arg_1 = sys.argv[1] + except IndexError: + raise SystemExit + try: + arg_2 = sys.argv[2] + except IndexError: + raise SystemExit + try: + arg_3 = sys.argv[3] + except IndexError: + arg_3 = False + + terminal_args = terminal_args( + source=arg_1, + destination=arg_2, + override=bool(arg_3) + ) + return terminal_args + + +def read_excel_file(path: str) -> Dict[str, pd.DataFrame]: + """Read the sheets of the Excel file into multiple dataframes.""" + if not os.path.exists(path): + raise FileNotFoundError( + f"No file found at '{path}'" + ) + sheets_as_dataframes = {} + with pd.ExcelFile(path) as xls: + for sheet_name in xls.sheet_names: + if sheet_name.startswith(KEYWORD_TO_IGNORE_SHEET): + continue + excel_file = pd.read_excel( + xls, + sheet_name, + index_col=COLUMN_FOR_INDEX_IN_EXCEL + ) + sheets_as_dataframes.update({sheet_name: excel_file}) + return sheets_as_dataframes + + +def make_json(dataframes: Dict[str, pd.DataFrame]) -> Dict[str, str]: + """Create the json strings from the excel data.""" + json_files = {} + for sheet_name, dataframe in dataframes.items(): + dataframe.dropna(inplace=True, axis=1) + dropped_columns = [column for column in dataframe.columns + if column.startswith(KEYWORD_TO_IGNORE_COLUMN)] + dataframe.drop(dropped_columns, axis=1, inplace=True, + errors="ignore") + print(sheet_name) + json_string = dataframe.to_json(orient="index", indent=4, force_ascii=False) + json_files.update({sheet_name: json_string}) + return json_files + + +def save_json_files(folder_name: str, jsons_files: Dict[str, str], + override_files: bool) -> None: + """Save the json strings to a '.json' file format.""" + path_to_json_folder = os.path.join( + os.path.abspath(__file__), + os.pardir, + folder_name + ) + path_to_json_folder = os.path.abspath(path_to_json_folder) + path_exists = os.path.exists(path_to_json_folder) + if path_exists and not override_files: + raise FileExistsError( + f"Folder: {path_to_json_folder} exists. Pass override argument " + f"to override the folder. See --help for more." + ) + if not path_exists: + os.mkdir(path_to_json_folder) + for json_name, json_sting in jsons_files.items(): + file_name = f"{json_name}.json" + file_path = os.path.join(path_to_json_folder, file_name) + with open(file_path, "w") as f: + f.write(json_sting) + + +def main(): + """The main function.""" + # Get terminal arguments. + terminal_arguments = read_terminal_arguments() + # Read the Excel file. + path_to_file = os.path.abspath(os.path.join( + os.path.abspath(__file__), + os.pardir, + terminal_arguments.source) + ) + excel_sheets_as_dataframes = read_excel_file(path_to_file) + excel_sheets_as_json = make_json(excel_sheets_as_dataframes) + save_json_files(terminal_arguments.destination, excel_sheets_as_json, + terminal_arguments.override) + + +if __name__ == "__main__": + t = main() diff --git a/datasheets/datasheet_testelement.json b/datasheets/datasheet_testelement.json deleted file mode 100644 index f40fb8479071904686d076bc57d33ed4b84589f0..0000000000000000000000000000000000000000 --- a/datasheets/datasheet_testelement.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Pumps":[ - {"Name": "Pump_1", - "Manufacturer": "Company A", - "Unit": "Percentage", - "Efficiency": 43}, - {"Name": "Pump_2", - "Manufacturer": "Company B", - "Unit": "Percentage", - "Efficiency": 56 - } - ] -} diff --git a/datasheets/frame.json b/datasheets/frame.json new file mode 100644 index 0000000000000000000000000000000000000000..6e86c90cfe0e076aed58dd5fc87d7a1fe7c292e4 --- /dev/null +++ b/datasheets/frame.json @@ -0,0 +1,272 @@ +{ + "39790":{ + "item number":39790, + "item description":"Technic, Liftarm, Modified Frame Thick 11 x 15 Open Center", + "category":"frame", + "price [Euro]":2.19, + "mass [g]":12.96, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=39790#T=C", + "dimension [studs]":"11 x 15 x 1" + }, + "32532":{ + "item number":32532, + "item description":"Technic, Brick 6 x 8 Open Center", + "category":"frame", + "price [Euro]":0.18, + "mass [g]":8.0, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32532#T=C", + "dimension [studs]":"6 x 8 x 1" + }, + "32531":{ + "item number":32531, + "item description":"Technic, Brick 4 x 6 Open Center", + "category":"frame", + "price [Euro]":0.1, + "mass [g]":5.0, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32531&idColor=11#T=C&C=11", + "dimension [studs]":"4 x 6 x 1" + }, + "3700":{ + "item number":3700, + "item description":"Technic, Brick 1 x 2 with Hole", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.82, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3700#T=C", + "dimension [studs]":"1 x 2" + }, + "3701":{ + "item number":3701, + "item description":"Technic, Brick 1 x 4 with Holes", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":1.46, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3701#T=C", + "dimension [studs]":"1 x 4 x 1" + }, + "3702":{ + "item number":3702, + "item description":"Technic, Brick 1 x 8 with Holes", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":2.85, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3702#T=C", + "dimension [studs]":"1 x 8 x 1" + }, + "2730":{ + "item number":2730, + "item description":"Technic, Brick 1 x 10 with Holes", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":3.67, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=2730#T=C", + "dimension [studs]":"1 x 10 x 1" + }, + "3895":{ + "item number":3895, + "item description":"Technic, Brick 1 x 12 with Holes", + "category":"frame", + "price [Euro]":0.03, + "mass [g]":4.2, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3895#T=C", + "dimension [studs]":"1 x 12 x 1" + }, + "32018":{ + "item number":32018, + "item description":"Technic, Brick 1 x 14 with Holes", + "category":"frame", + "price [Euro]":0.03, + "mass [g]":4.92, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32018#T=C", + "dimension [studs]":"1 x 14 x 1" + }, + "3703":{ + "item number":3703, + "item description":"Technic, Brick 1 x 16 with Holes", + "category":"frame", + "price [Euro]":0.07, + "mass [g]":5.87, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3703#T=C", + "dimension [studs]":"1 x 16 x 1" + }, + "32524":{ + "item number":32524, + "item description":"Technic, Liftarm Thick 1 x 7", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":1.79, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32524&name=Technic,%20Liftarm%20Thick%201%20x%207&category=%5BTechnic,%20Liftarm%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"1 x 7" + }, + "40490":{ + "item number":40490, + "item description":"Technic, Liftarm Thick 1 x 9", + "category":"frame", + "price [Euro]":0.02, + "mass [g]":2.59, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=40490&name=Technic,%20Liftarm%20Thick%201%20x%209&category=%5BTechnic,%20Liftarm%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"1 x 9" + }, + "32525":{ + "item number":32525, + "item description":"Technic, Liftarm Thick 1 x 11", + "category":"frame", + "price [Euro]":0.02, + "mass [g]":2.8, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32525&name=Technic,%20Liftarm%20Thick%201%20x%2011&category=%5BTechnic,%20Liftarm%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"1 x 11" + }, + "41239":{ + "item number":41239, + "item description":"Technic, Liftarm Thick 1 x 13", + "category":"frame", + "price [Euro]":0.05, + "mass [g]":3.3, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=41239&name=Technic,%20Liftarm%20Thick%201%20x%2013&category=%5BTechnic,%20Liftarm%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"1 x 13" + }, + "32278":{ + "item number":32278, + "item description":"Technic, Liftarm Thick 1 x 15", + "category":"frame", + "price [Euro]":0.05, + "mass [g]":4.0, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32278&name=Technic,%20Liftarm%20Thick%201%20x%2015&category=%5BTechnic,%20Liftarm%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"1 x 15" + }, + "3713":{ + "item number":3713, + "item description":"Technic Bush", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.14, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3713&name=Technic%20Bush&category=%5BTechnic%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":1 + }, + "32123":{ + "item number":32123, + "item description":"Technic Bush 1\/2 Smooth", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.01, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=4265c&name=Technic%20Bush%201\/2%20Smooth&category=%5BTechnic%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":1 + }, + "3749":{ + "item number":3749, + "item description":"Technic, Axle 1L with Pin without Friction Ridges", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.22, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3749&name=Technic,%20Axle%20%201L%20with%20Pin%20without%20Friction%20Ridges&category=%5BTechnic,%20Axle%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"0,75 x 2 x 0,55" + }, + "6538":{ + "item number":6538, + "item description":"Technic, Axle Connector 2L (Ridged Undetermined Type)", + "category":"frame", + "price [Euro]":0.03, + "mass [g]":0.4, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=6538#T=C", + "dimension [studs]":"?" + }, + "6536":{ + "item number":6536, + "item description":"Technic, Axle and Pin Connector Perpendicular", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.39, + "delivery time [days]":7, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=6536&name=Technic,%20Axle%20and%20Pin%20Connector%20Perpendicular&category=%5BTechnic,%20Connector%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"1x 2 x 1" + }, + "32138":{ + "item number":32138, + "item description":"Technic, Pin Double with Axle Hole", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.96, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32138&name=Technic,%20Pin%20Double%20with%20Axle%20Hole&category=%5BTechnic,%20Pin%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"?" + }, + "14720":{ + "item number":14720, + "item description":"Technic, Liftarm, Modified H-Shape Thick 3 x 5 Perpendicular", + "category":"frame", + "price [Euro]":0.08, + "mass [g]":2.29, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=14720&name=Technic,%20Liftarm,%20Modified%20H-Shape%20Thick%203%20x%205%20Perpendicular&category=%5BTechnic,%20Liftarm%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"3 x 5 x 1" + }, + "48989":{ + "item number":48989, + "item description":"Technic, Pin Connector Perpendicular 3L with 4 Pins", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":1.22, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=48989&name=Technic,%20Pin%20Connector%20Perpendicular%203L%20with%204%20Pins&category=%5BTechnic,%20Connector%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"3 x 3 x 1" + }, + "55615":{ + "item number":55615, + "item description":"Technic, Pin Connector Perpendicular 3 x 3 Bent with 4 Pins", + "category":"frame", + "price [Euro]":0.1, + "mass [g]":1.9, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=55615&name=Technic,%20Pin%20Connector%20Perpendicular%203%20x%203%20Bent%20with%204%20Pins&category=%5BTechnic,%20Connector%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":"4 x 4 x 1" + }, + "32556":{ + "item number":32556, + "item description":"Technic, Pin 3L without Friction Ridges", + "category":"frame", + "price [Euro]":0.1, + "mass [g]":0.25, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32556&name=Technic,%20Pin%203L%20without%20Friction%20Ridges&category=%5BTechnic,%20Pin%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":1 + }, + "3673":{ + "item number":3673, + "item description":"Technic, Pin without Friction Ridges", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.16, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3673&name=Technic,%20Pin%20without%20Friction%20Ridges&category=%5BTechnic,%20Pin%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":1 + }, + "32054":{ + "item number":32054, + "item description":"Technic, Pin 4L with Friction Ridges and Stop Bush", + "category":"frame", + "price [Euro]":0.01, + "mass [g]":0.33, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32054&name=Technic,%20Pin%203L%20with%20Friction%20Ridges%20and%20Stop%20Bush&category=%5BTechnic,%20Pin%5D#T=S&O={%22iconly%22:0}", + "dimension [studs]":1 + } +} \ No newline at end of file diff --git a/datasheets/gears.json b/datasheets/gears.json new file mode 100644 index 0000000000000000000000000000000000000000..6f639555cdc9405eb527d91d1356db9748acd347 --- /dev/null +++ b/datasheets/gears.json @@ -0,0 +1,83 @@ +{ + "3647":{ + "item number":3647, + "item description":"Gear 8 Tooth", + "category":"gear", + "price [Euro]":0.14, + "mass [g]":0.16, + "delivery time [days]":11, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3647#T=C" + }, + "94925":{ + "item number":94925, + "item description":"Gear 16 Tooth", + "category":"gear", + "price [Euro]":0.2, + "mass [g]":0.7, + "delivery time [days]":12, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=94925#T=C" + }, + "32269":{ + "item number":32269, + "item description":"Gear 20 Tooth", + "category":"gear", + "price [Euro]":0.36, + "mass [g]":1.4, + "delivery time [days]":13, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32269#T=C" + }, + "3648":{ + "item number":3648, + "item description":"Gear 24 Tooth", + "category":"gear", + "price [Euro]":0.32, + "mass [g]":1.17, + "delivery time [days]":11, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3648#T=C" + }, + "3650":{ + "item number":3650, + "item description":"Gear 24 Tooth Crown", + "category":"gear", + "price [Euro]":0.09, + "mass [g]":1.03, + "delivery time [days]":13, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3650#T=C" + }, + "3649":{ + "item number":3649, + "item description":"Gear 40 Tooth", + "category":"gear", + "price [Euro]":0.81, + "mass [g]":3.76, + "delivery time [days]":11, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3649#T=C" + }, + "32498":{ + "item number":32498, + "item description":"Gear 36 Tooth", + "category":"gear", + "price [Euro]":0.88, + "mass [g]":3.5, + "delivery time [days]":12, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=32498#T=C" + }, + "6588":{ + "item number":6588, + "item description":"Gear Worm Gearbox", + "category":"gear", + "price [Euro]":1.63, + "mass [g]":4.5, + "delivery time [days]":11, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=6588#T=C" + }, + "4716":{ + "item number":4716, + "item description":"Gear Worm Screw", + "category":"gear", + "price [Euro]":0.54, + "mass [g]":0.6, + "delivery time [days]":12, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=4716#T=C" + } +} \ No newline at end of file diff --git a/datasheets/lego_item_list_FAIR-quality_kpis.xlsx b/datasheets/lego_item_list_FAIR-quality_kpis.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7e1126d596016d4bd0ffd80936cc3711b2e92a63 Binary files /dev/null and b/datasheets/lego_item_list_FAIR-quality_kpis.xlsx differ diff --git a/datasheets/lego_teileliste_faire_qualitaets_kpis.xlsx b/datasheets/lego_teileliste_faire_qualitaets_kpis.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7351adab582c31b5242f7a40c890f31e770b6c59 Binary files /dev/null and b/datasheets/lego_teileliste_faire_qualitaets_kpis.xlsx differ diff --git a/datasheets/motors.json b/datasheets/motors.json new file mode 100644 index 0000000000000000000000000000000000000000..f5926027a2e5bca06369a258d50e223ac85769cc --- /dev/null +++ b/datasheets/motors.json @@ -0,0 +1,50 @@ +{ + "8882-1":{ + "item number":"8882-1", + "item description":"Power Functions XL-Motor", + "category":"motor", + "related items":8881, + "idle current [mA]":80, + "idle speed [rev per min]":220, + "locking torque [Ncm]":40, + "price [Euro]":30, + "mass [g]":72.85, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?S=8882-1&name=Power%20Functions%20XL-Motor&category=%5BPower%20Functions%5D#T=S&O={%22iconly%22:0}", + "compatible battery":8881, + "input voltage [V]":9, + "dimensions [cm]":"13.6 x 12.2 x 3.9" + }, + "8883-1":{ + "item number":"8883-1", + "item description":"Power Functions M-Motor", + "category":"motor", + "related items":"8881, 8878, 45517, 88000", + "idle current [mA]":65, + "idle speed [rev per min]":405, + "locking torque [Ncm]":11, + "price [Euro]":20, + "mass [g]":35.0, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?S=8883-1&name=Power%20Functions%20M-Motor&category=%5BPower%20Functions%5D#T=S&O={%22iconly%22:0}", + "compatible battery":"8881, 8878, 45517, 88000", + "input voltage [V]":9, + "dimensions [cm]":"13.8 x 11.9 x 2.3" + }, + "88003-1":{ + "item number":"88003-1", + "item description":"Power Functions L-Motor", + "category":"motor", + "related items":"8881, 8878, 88000", + "idle current [mA]":120, + "idle speed [rev per min]":390, + "locking torque [Ncm]":18, + "price [Euro]":15, + "mass [g]":48.0, + "delivery time [days]":3, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?S=88003-1&name=Power%20Functions%20L-Motor&category=%5BPower%20Functions%5D#T=S&O={%22iconly%22:0}", + "compatible battery":"8881, 8878, 88000", + "input voltage [V]":9, + "dimensions [cm]":"13 x 11 x 2.5" + } +} \ No newline at end of file diff --git a/datasheets/wheels.json b/datasheets/wheels.json new file mode 100644 index 0000000000000000000000000000000000000000..6f1736bbdff212ff7d8f91d653c61bfd8c5e150a --- /dev/null +++ b/datasheets/wheels.json @@ -0,0 +1,68 @@ +{ + "4265":{ + "item number":4265, + "item description":"wheel 14", + "category":"wheel", + "related items":59895, + "price [Euro]":0.02, + "mass [g]":0.5, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=4265cc01#T=C", + "diameter [mm]":14.0 + }, + "3482":{ + "item number":3482, + "item description":"wheel 24", + "category":"wheel", + "related items":3483, + "price [Euro]":0.01, + "mass [g]":3.0, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=3482c01#T=C", + "diameter [mm]":24.0 + }, + "56904":{ + "item number":56904, + "item description":"wheel 43,2", + "category":"wheel", + "related items":30699, + "price [Euro]":0.11, + "mass [g]":13.0, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=56904c02#T=C", + "diameter [mm]":43.2 + }, + "41896":{ + "item number":41896, + "item description":"wheel 56", + "category":"wheel", + "related items":41897, + "price [Euro]":0.45, + "mass [g]":23.0, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=41896c04#T=C", + "diameter [mm]":56.0 + }, + "2903":{ + "item number":2903, + "item description":"wheel 81,6", + "category":"wheel", + "related items":2902, + "price [Euro]":1.31, + "mass [g]":30.0, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=2903c02#T=C", + "diameter [mm]":81.6 + }, + "88517":{ + "item number":88517, + "item description":"wheel 100,6", + "category":"wheel", + "related items":11957, + "price [Euro]":1.25, + "mass [g]":40.0, + "delivery time [days]":5, + "data source":"https:\/\/www.bricklink.com\/v2\/catalog\/catalogitem.page?P=88517c02#T=C", + "diameter [mm]":100.6 + } +} \ No newline at end of file diff --git a/functions/calculation_rules.py b/functions/calculation_rules.py index e9d20f109763fe70b9c31672035a9869081e63e1..092fc43faeb2308eb40199d1dd32a9fcf9f81d88 100644 --- a/functions/calculation_rules.py +++ b/functions/calculation_rules.py @@ -1,10 +1,51 @@ -''' +""" File consists of several functions for the calculation rules of FAIR Quality KPIs -''' +""" + + def test_function(): + """Test function to check module functionality""" print("You called the test function.") +def kpi_sum(*args): + """ + Calculates the sum of one or more integers or lists. + + Args: + *args: One or more integers or lists. + + Returns: + total (int): The sum of all given integers and/or the sum + of all items in all given lists. + + Raises: + TypeError: If an argument with unsupported type is passed + (i.e. anything other than int or list). + """ + total = 0 + for arg in args: + if isinstance(arg, int): # Check if argument is an integer + total += arg + elif isinstance(arg, list): # Check if argument is a list + total += sum(arg) + else: + raise TypeError( + f"Unsupported type {type(arg)} passed to kpi_sum()") + return total + + # if arguments are handed over not as a list: sum(list(args)) + + +# Add new functions for calculating some real complicated metrics + + if __name__ == "__main__": - print("This script contains functions for calculating the FAIR Quality KPIs. It is not to be executed independently.") - pass \ No newline at end of file + """ + Function to inform that functions in this module is + intended for import not usage as script + """ + print( + "This script contains functions for calculating the FAIR Quality KPIs." + "It is not to be executed independently." + ) diff --git a/functions/classes.py b/functions/classes.py index f02464845ab52f67ce440f22d2fc93ff4efd6b8b..7563deb4c7038d69925b316047c6da58d06d5246 100644 --- a/functions/classes.py +++ b/functions/classes.py @@ -1,8 +1,518 @@ -''' -File consists of several classes for the different elements of a device. -''' +""" +File consists of several classes to model elements and assembly +layers of a composed device. +""" +from __future__ import annotations +import uuid +import json +import operator +from enum import Enum, auto +from typing import List, Dict, Optional -if __name__ == "__main__": - print("This script contains classes for the different elements of a device. It is not to be executed independently.") - pass \ No newline at end of file +class AggregationLayer(Enum): + """Describes the levl of aggregation for the objects LegoComponent + and LegoAssembly and provides the 4 applicable layers. + """ + + SYSTEM = auto() + ASSEMBLY = auto() + SUBASSEMBLY = auto() + COMPONENT = auto() + + +class LegoComponent: + """ + A class for storing information about a single Lego component. + + ... + + Attributes + ---------- + uuid : UUID + A randomly generated unique identifier for the component. + parent : None | LegoAssembly + The parent of the component. Can be either None or a LegoAssembly object. + layer : AggregationLayer + An enumeration indicating the hierarchy level. For compoennts, this is + COMPONENT by default. + properties : dict + Dictionary that holds all properties of the component that can be saved + in a dictionary format. + + Methods + ------- + clone(new_label=None) + Returns a new instance of LegoComponent identical to the current instance. + get_root_assembly() + Returns the top-level assembly in which the component belongs. + to_dict() + Returns the current instance represented as a dictionary. + """ + + def __init__( + self, + label: Optional[str] = None, + datasheet: Optional[dict] = None, + *more_properties: dict, + **kwargs, + ) -> None: + """ + Constructs all the necessary attributes for the LegoComponent object. + + Parameters + ---------- + label : str, optional + The name of the component to add. + datasheet : dict, optional + Metadata describing the component, read from datasheet. + more_properties : tuple of dict, dict + Additional dictionaries representing custom properties. + kwargs + Arbitrary keyword arguments representing custom properties. + + Raises + ------ + ValueError + If the type of more_properties argument is not a dictionary. + """ + self.uuid: uuid.UUID = uuid.uuid4() + self.parent: None | LegoAssembly = None + self.layer: AggregationLayer = AggregationLayer.COMPONENT + self.properties: dict = {} + if label is not None: + self.properties["label"] = label + if datasheet is not None: + self.properties.update(datasheet) + for prop in more_properties: + if isinstance(prop, dict): + self.properties.update(prop) + else: + raise ValueError(f"Unexpected argument type: {type(more_properties)}") + for key, value in kwargs.items(): + self.properties[key] = value + + def clone(self, new_label: Optional[str] = None) -> LegoComponent: + """ + Returns a new instance of LegoComponent identical to the current instance. + + This method creates a new instance of LegoComponent that is identical to the + current instance with an optional different label. + The assigned uuid changes and elements are copied. + + Parameters + ---------- + new_label : str, optional + A new label string to use. Defaults to None. + + Returns + ------- + LegoComponent + A new instance of LegoComponent that is identical to the current instance. + + """ + if new_label is None: + new_label = self.properties["label"] + clone = LegoComponent(None, None, self.properties) + clone.properties["label"] = new_label + return clone + + def get_root_assembly(self): + """ + Returns the top-level assembly in which the component belongs. + + This method traverses the parent hierarchy of a LegoComponent object until it + finds the root-level LegoAssembly, returning it to the caller. A parent is + assigned when a LegoComponent or LegoItem is added to a LegoAssembly object. + + Returns + ------- + None | LegoAssembly + The root-level LegoAssembly or None if the component has no parent. + """ + if self.parent is None: + return None + current_assembly = self.parent + while current_assembly.parent is not None: + current_assembly = current_assembly.parent + return current_assembly + + def to_dict(self) -> Dict: + """ + Returns the current instance represented as a dictionary. + + This method returns a dictionary representation of the LegoComponent object + suitable for serialization as JSON. + + Returns + ------- + dict + A dictionary representation of the object. + """ + dict_ = { + "uuid": self.uuid, + "properties": self.properties, + "layer": self.layer, + } + return {"component": dict_} + + def __str__(self): + """ + Simple string representation of the object. + """ + return self.__repr__() + + def __repr__(self): + """ + String representation of the object including the component label and UUID. + """ + return f"LegoComponent {self.properties['label']} [{self.uuid}]" + + +class LegoAssembly: + """ + Represents a Lego assembly that can contain Lego Components and subassemblies. + + Attributes: + uuid (uuid.UUID): The unique ID of the assembly. + parent (LegoAssembly or None): The parent assembly containing this one, if any. + properties (dict): Optional properties for the assembly, such as a label. + layer (AggregationLayer): The aggregation layer of the assembly. + components (List[LegoComponent]): The list of contained components. + assemblies (List[LegoAssembly]): The list of contained subassemblies. + """ + + def __init__( + self, + layer: AggregationLayer, + label: Optional[str] = None, + *properties: dict, + **kwargs, + ) -> None: + """ + Initializes a new LegoAssembly instance. + + Args: + layer (AggregationLayer): The aggregation layer of the assembly. + label (Optional[str], optional): Optional label for the assembly. + Defaults to None. + properties (dict): Optional properties for the assembly, such as a label. + **kwargs: Optional keyword arguments for additional properties. + """ + self.uuid: uuid.UUID = uuid.uuid4() + self.parent: None | LegoAssembly = None + self.properties: dict = {} + self.layer: AggregationLayer = layer + if label is not None: + self.properties["label"] = label + for prop in properties: + if isinstance(prop, dict): + self.properties.update(prop) + else: + raise ValueError(f"Unexpected argument type: {type(properties)}") + for key, value in kwargs.items(): + self.properties[key] = value + self.components: List[LegoComponent] = [] + self.assemblies: List[LegoAssembly] = [] + + def add_component(self, component: LegoComponent | List[LegoComponent]) -> None: + """ + Adds a Lego component to the current assembly. + Use add() to handle both components and assemblies. + + Args: + component (LegoComponent or List[LegoComponent]): + The component or list of components to add. + + Raises: + TypeError: If the argument is not a LegoComponent instance + or a list of LegoComponent instances. + AssertionError: If the component or a component with the same UUID + is already contained in the assembly or any of its child assemblies. + """ + if isinstance(component, list): + for c in component: + self.add_component(c) + return + + if not isinstance(component, LegoComponent): + raise TypeError( + f"Argument should be of type {LegoComponent.__name__}, " + f"got {type(component).__name__} instead." + ) + + if self.get_root_assembly().contains_uuid(component.uuid): + raise AssertionError( + f"This assembly or a subassembly already contains " + f"the component with ID " + f"{component.uuid}." + ) + component.parent = self + self.components.append(component) + + def add_assembly(self, assembly: LegoAssembly | List[LegoAssembly]) -> None: + """ + Adds a subassembly to the current assembly. + Use add() to handle both components and assemblies. + + Args: + assembly (LegoAssembly or List[LegoAssembly]): + The subassembly or list of subassemblies to add. + + Raises: + TypeError: If the argument is not a LegoAssembly instance + or a list of LegoAssembly instances. + AssertionError: If the subassembly or a subassembly with the same UUID + is already contained in the assembly or any of its child assemblies. + """ + if isinstance(assembly, list): + for a in assembly: + self.add_assembly(a) + return + + if not isinstance(assembly, LegoAssembly): + raise TypeError( + f"Argument should be of type {LegoAssembly.__name__}, " + f"got {type(assembly).__name__} instead." + ) + + if self.get_root_assembly().contains_uuid(assembly.uuid): + raise AssertionError( + f"This assembly or a subassembly already contains " + f"the assembly with ID {assembly.uuid}." + ) + assembly.parent = self + self.assemblies.append(assembly) + + def add( + self, part: LegoAssembly | LegoComponent | List[LegoAssembly | LegoComponent] + ) -> None: + """ + Adds either a Lego component, a subassembly or a (mixed) list of them to the + current assembly. Uses internal functions add_component() and add_assembly(). + + Args: + part (LegoAssembly or LegoComponent or List[LegoAssembly or LegoComponent]): + The part or parts to add. + + Raises: + TypeError: If the argument is not a LegoAssembly instance or a LegoComponent + instance, or a (mixed) list of them. + """ + if isinstance(part, LegoComponent): + self.add_component(part) + elif isinstance(part, LegoAssembly): + self.add_assembly(part) + elif isinstance(part, list): + for p in part: + self.add(p) + else: + raise TypeError( + f"Argument should be of types {LegoAssembly.__name__}, " + f"{LegoComponent.__name__} or a list of them. " + f"Got {type(part).__name__} instead." + ) + + def children(self) -> Dict[str, List[LegoComponent] | List[LegoAssembly]]: + """ + Returns a dictionary of the assembly's children (components and + sub-assemblies), sorted by category. + + Returns: + A dictionary with two keys - "components" and "assemblies" - and + corresponding lists of LegoComponents and LegoAssemblies as values. + """ + return {"components": self.components, "assemblies": self.assemblies} + + def get_component_list(self, max_depth: int = -1) -> List[LegoComponent]: + """ + Gets a full list of all components contained in the assembly or any of + its sub-assemblies. + + Args: + max_depth: An integer indicating the maximum depth of recursion. + -1 means unlimited depth. 0 returns only direc children. + + Returns: + A list of all LegoComponents contained in the current assembly or + its sub-assemblies. + """ + component_list = [] + component_list.extend(self.components) + if max_depth > 0 or max_depth < 0: + for assembly in self.assemblies: + component_list.extend(assembly.get_component_list(max_depth - 1)) + return component_list + + def get_root_assembly(self) -> LegoAssembly: + """ + Returns the root LegoAssembly of the current assembly by recursively + searching up the tree until the top-most parent (the root) is found. + + Returns: + The root LegoAssembly of the current LegoAssembly instance. + """ + current_assembly = self + while current_assembly.parent is not None: + current_assembly = current_assembly.parent + return current_assembly + + def contains_uuid(self, uuid_: uuid.UUID): + """ + Recursively searches through the assembly and component + tree to determine whether the specified UUID exists anywhere + within it. + + Args: + uuid_: A UUID object representing the ID to search for. + + Returns: + True if the UUID exists in the assembly or any of its sub-assemblies, + False otherwise. + """ + component_ids = list(map(lambda c: c.uuid, self.components)) + if uuid_ in component_ids: + return True + assembly_ids = list(map(lambda a: a.uuid, self.assemblies)) + if uuid_ in assembly_ids: + return True + for assembly in self.assemblies: + if assembly.contains_uuid(uuid_): + return True + return False + + def to_dict(self) -> Dict: + """ + Serializes the current LegoAssembly instance and its descendants into a dict. + + Returns: + A dictionary representation of the current assembly, including all + of its component and sub-assembly children. + """ + dict_ = { + "uuid": self.uuid, + "properties": self.properties, + "layer": self.layer, + } + dict_["components"] = [component.to_dict() for component in self.components] + dict_["assemblies"] = [assembly.to_dict() for assembly in self.assemblies] + return {"assembly": dict_} + + def __repr__(self): + """ + String representation of the object including the component label and UUID. + """ + return f"LegoAssembly {self.properties['label']} [{self.uuid}]" + + def clone(self, label: Optional[str] = None) -> LegoAssembly: + """ + Creates a deep clone of the current LegoAssembly instance, including + all of its component and sub-assembly children. The assigned uuid changes. + Optionally a new label can be passed. + + Args: + label: The label (name) for the cloned assembly. If none is passed, + uses the same label as the original assembly. + + Returns: + The cloned LegoAssembly instance. + """ + if label is None: + label = self.properties["label"] + clone = LegoAssembly(self.layer, None, self.properties) + clone.properties["label"] = label + for component in self.components: + clone.add_component(component.clone()) + for assembly in self.assemblies: + clone.add_assembly(assembly.clone()) + return clone + + +def print_assembly_tree(root, level=0, is_last=False): + """ + Prints the assembly tree starting from root with a visualization + implemented with text characters. + + Args: + root (LegoAssembly): The root of the assembly tree to print. + level (int): The indentation level. Defaults to 0. + is_last (bool): Determines whether the current node is the last in level. + Defaults to False. + """ + if not isinstance(root, LegoAssembly): + raise TypeError( + f"Argument should be of type {LegoAssembly.__name__}, " + f"got {type(root).__name__} instead." + ) + """ Print the items. """ + assembly_padding = "" + if level > 0: + assembly_padding += "│ " * (level - 1) + if is_last: + assembly_padding += "└── " + else: + assembly_padding += "├── " + print(f"{assembly_padding}{root}") + """ Recursively print child components. """ + for i, assembly in enumerate(root.assemblies): + is_last_ = i == len(root.assemblies) - 1 and len(root.components) == 0 + print_assembly_tree(assembly, level + 1, is_last_) + """ Print the components. """ + for i, item in enumerate(root.components): + component_padding = "│ " * level if not is_last else " " + component_padding += "├── " if i < len(root.components) - 1 else "└── " + print(f"{component_padding}{item}") + + +def correct_aggregation_hierarchy(root: LegoAssembly, strict: bool = False): + """ + Recursively checks whether the aggregation hierarchy from `root` is correct. + + Args: + root (LegoAssembly): The root of the assembly tree. + strict (bool): If True, the function will return False if any assembly + or component with a layer level equal to root's is found. + Defaults to False. + + Returns: + True if the aggregation hierarchy is correct. False otherwise. + """ + if not isinstance(root, LegoAssembly): + raise TypeError( + f"Argument should be of type {LegoAssembly.__name__}, " + f"got {type(root).__name__} instead." + ) + higher_level = operator.le + if strict: + higher_level = operator.lt + for component in root.components: + if not higher_level(root.layer.value, component.layer.value): + return False + for assembly in root.assemblies: + if not higher_level(root.layer.value, assembly.layer.value): + return False + if not correct_aggregation_hierarchy(assembly, strict): + return False + return True + + +class KPIEncoder(json.JSONEncoder): + """ + JSON encoder that handles special class types for KPI serialization. + """ + + def default(self, o): + """ + Overrides default method to handle special conversion cases. + + Args: + o : Object to be converted. + + Returns: + Converted object or super method if no applicable case is found. + """ + if isinstance(o, uuid.UUID): + return "kpi-" + str(o) + if isinstance(o, (AggregationLayer)): + return "kpi-" + o.name + return super().default(o) diff --git a/main_kpi.py b/main_kpi.py deleted file mode 100644 index 9322121ae30d376f5b393b548493aea20f4608a4..0000000000000000000000000000000000000000 --- a/main_kpi.py +++ /dev/null @@ -1,11 +0,0 @@ -''' -FAIR Quality KPIs - -''' - -def main (): - print("Hello ;-)") - pass - -if __name__ == "__main__": - main() \ No newline at end of file