From 86240b539470c530fcb5ec5c25af2da39f3f9f03 Mon Sep 17 00:00:00 2001 From: fejao <mail@fejao.de> Date: Wed, 5 Mar 2025 17:14:50 +0100 Subject: [PATCH 1/5] Added .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fae8b69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.venv* +*OLD* +*fejao_notes* -- GitLab From e5526fbd641caffd821191e35bcfd5de626c3121 Mon Sep 17 00:00:00 2001 From: fejao <mail@fejao.de> Date: Wed, 5 Mar 2025 17:15:25 +0100 Subject: [PATCH 2/5] Updated .gitignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fae8b69..1e8df78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.venv* +*__pycache__* *OLD* *fejao_notes* -- GitLab From 430f74476d928206b0c4bed8d949e276e3702634 Mon Sep 17 00:00:00 2001 From: fejao <mail@fejao.de> Date: Wed, 5 Mar 2025 17:15:55 +0100 Subject: [PATCH 3/5] Added django --- django/api/__init__.py | 0 django/api/apps.py | 12 + django/api/urls.py | 13 + django/api/views.py | 442 ++++++++++++++++++ django/api_admin/__init__.py | 0 django/api_admin/apps.py | 12 + django/api_admin/urls.py | 13 + django/api_admin/views.py | 76 +++ django/api_auth/__init__.py | 0 django/api_auth/apps.py | 12 + django/api_auth/templates/login.html | 24 + django/api_auth/templates/logout.html | 14 + django/api_auth/urls.py | 22 + django/api_auth/views.py | 97 ++++ django/backend/__init__.py | 0 django/backend/asgi.py | 16 + django/backend/settings.py | 212 +++++++++ django/backend/urls.py | 55 +++ django/backend/wsgi.py | 16 + django/manage.py | 22 + django/requirements.txt | 10 + django/shared_models/__init__.py | 0 django/shared_models/apps.py | 12 + .../shared_models/migrations/0001_initial.py | 38 ++ django/shared_models/migrations/__init__.py | 0 django/shared_models/models.py | 49 ++ django/shared_models/serializers.py | 41 ++ 27 files changed, 1208 insertions(+) create mode 100644 django/api/__init__.py create mode 100644 django/api/apps.py create mode 100644 django/api/urls.py create mode 100644 django/api/views.py create mode 100644 django/api_admin/__init__.py create mode 100644 django/api_admin/apps.py create mode 100644 django/api_admin/urls.py create mode 100644 django/api_admin/views.py create mode 100644 django/api_auth/__init__.py create mode 100644 django/api_auth/apps.py create mode 100644 django/api_auth/templates/login.html create mode 100644 django/api_auth/templates/logout.html create mode 100644 django/api_auth/urls.py create mode 100644 django/api_auth/views.py create mode 100644 django/backend/__init__.py create mode 100644 django/backend/asgi.py create mode 100644 django/backend/settings.py create mode 100644 django/backend/urls.py create mode 100644 django/backend/wsgi.py create mode 100755 django/manage.py create mode 100644 django/requirements.txt create mode 100644 django/shared_models/__init__.py create mode 100644 django/shared_models/apps.py create mode 100644 django/shared_models/migrations/0001_initial.py create mode 100644 django/shared_models/migrations/__init__.py create mode 100644 django/shared_models/models.py create mode 100644 django/shared_models/serializers.py diff --git a/django/api/__init__.py b/django/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/api/apps.py b/django/api/apps.py new file mode 100644 index 0000000..f1d64a8 --- /dev/null +++ b/django/api/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/django/api/urls.py b/django/api/urls.py new file mode 100644 index 0000000..592c29f --- /dev/null +++ b/django/api/urls.py @@ -0,0 +1,13 @@ +""" + File for setting the URLs used from the api django app. +""" + +from django.urls import path +from . import views + + +urlpatterns = [ + path('box/search', views.ButtonBoxSearchView.as_view(), name='box_search'), + path('box/use', views.ButtonBoxUseView.as_view(), name='box_use'), + path('box/use/count', views.ButtonBoxUseCountView.as_view(), name='box_use_count'), +] diff --git a/django/api/views.py b/django/api/views.py new file mode 100644 index 0000000..aaeea0d --- /dev/null +++ b/django/api/views.py @@ -0,0 +1,442 @@ +""" + File for setting the view classes used from the api django app. +""" + +from datetime import datetime, timezone + +from shared_models.models import ButtonBox, ButtonBoxUse +from shared_models.serializers import ButtonBoxSerializer, ButtonBoxUseSerializer + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.renderers import JSONRenderer +from rest_framework_simplejwt.authentication import JWTAuthentication + + +### +### HELPERS +### +def helper_get_from_parsed(parsed_name: str, parsed_value: str, parsed_type: str): + """Helper method used from the ButtonBoxUseView class for setting the return values from different 'parsed_name' and 'parsed_type'. + It will fetch or update the values from the ButtonBoxUse model. + + Parameters + ---------- + parsed_name: str + String with the name parsed. + parsed_value: str + String with the value parsed. + parsed_type: str + String with the type parsed. + + Returns + ------- + ret_dict: dict + The Dictionary with the output from executing this helper method. + ret_status: rest_framework.status + The HTTP status from executing this helper method. + """ + + founded_box = None + ret_dict = None + ret_status = status.HTTP_500_INTERNAL_SERVER_ERROR + try: + if parsed_name == "id": + founded_box = ButtonBox.objects.get(id=parsed_value) + + elif parsed_name == "name": + founded_box = ButtonBox.objects.get(name=parsed_value) + + elif parsed_name == "pin_button": + founded_box = ButtonBox.objects.get(pin_button=parsed_value) + + elif parsed_name == "pin_led": + founded_box = ButtonBox.objects.get(pin_led=parsed_value) + + else: + ret_dict = { + 'error': True, + 'error_msg': f"ERROR: unknown parameter parsed: {parsed_name}." + } + ret_status = status.HTTP_406_NOT_ACCEPTABLE + + except ButtonBox.DoesNotExist: + ret_dict = { + 'error': True, + 'error_msg': f"No Box could be founded with the '{parsed_name}': {parsed_value}" + } + ret_status = status.HTTP_404_NOT_FOUND + + except Exception as e: + ret_dict = { + 'error': True, + 'error_msg': f"ERROR happend when trying to search for the box with {parsed_name}: {parsed_value} | Error: {e}" + } + ret_status = status.HTTP_406_NOT_ACCEPTABLE + + ### If error ocurred, finish + if ret_dict is not None: + pass + + else: + if parsed_type == "add": + try: + ### Add pushed from pin + added = ButtonBoxUse(box_used=founded_box) + added.save() + + ret_dict = { + 'error': False, + 'message': "Added button press", + "button-used": { + "id": added.id, + "box_id": added.box_used.id, + 'box_name': added.box_used.name, + 'box_pin_button': added.box_used.pin_button, + 'box_pin_led': added.box_used.pin_led, + 'date': str(added.date) + } + } + ret_status = status.HTTP_200_OK + + + except Exception as e: + ret_dict = { + 'error': True, + 'message': f"Error saving the press button with output: {e}", + } + ret_status = status.HTTP_418_IM_A_TEAPOT + + elif parsed_type == "list": + try: + ### Get list pushed button + box_uses = ButtonBoxUse.objects.filter(box_used=founded_box) + serializer = ButtonBoxUseSerializer(box_uses, many=True) + ret_dict = serializer.data + ret_status = status.HTTP_200_OK + + except Exception as e: + ret_dict = { + 'error': True, + 'error_msg': f"ERROR happend when trying to search for the box with {parsed_name}: {parsed_value} | Error: {e}" + } + ret_status = status.HTTP_406_NOT_ACCEPTABLE + + else: + ret_dict = { + 'error': True, + 'error_msg': "ERROR parsing response from 'helper_get_from_parsed' function" + } + ret_status = status.HTTP_501_NOT_IMPLEMENTED + + return ret_dict, ret_status + + +### +### VIEW CLASSES +### +class ButtonBoxSearchView(APIView): + """Class for setting the API views for Searching at the ButtonBox model. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + # Get parsed + box_id = request.query_params.get("id") + box_name = request.query_params.get("name") + box_pin_button = request.query_params.get("pin_button") + box_pin_led = request.query_params.get("pin_led") + + if box_id is not None and box_name is None and box_pin_button is None and box_pin_led is None: + box_founded = ButtonBox.objects.filter(id__icontains=box_id) + + elif box_id is None and box_name is not None and box_pin_button is None and box_pin_led is None: + box_founded = ButtonBox.objects.filter(name__icontains=box_name) + + elif box_id is None and box_name is None and box_pin_button is not None and box_pin_led is None: + box_founded = ButtonBox.objects.filter(pin_button__icontains=box_pin_button) + + elif box_id is None and box_name is None and box_pin_button is None and box_pin_led is not None: + box_founded = ButtonBox.objects.filter(pin_led__icontains=box_pin_led) + + # If not, return all + else: + box_founded = ButtonBox.objects.all() + + serializer = ButtonBoxSerializer(box_founded, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class ButtonBoxUseView(APIView): + """Class for setting the API views for Searching and Adding values at the ButtonBoxUse model. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + # Get parsed + box_id = request.query_params.get("id") + box_name = request.query_params.get("name") + box_pin_button = request.query_params.get("pin_button") + box_pin_led = request.query_params.get("pin_led") + + if box_id is not None and box_name is None and box_pin_button is None and box_pin_led is None: + founded_box = ButtonBox.objects.get(id=box_id) + + elif box_id is None and box_name is not None and box_pin_button is None and box_pin_led is None: + founded_box = ButtonBox.objects.get(name=box_name) + + elif box_id is None and box_name is None and box_pin_button is not None and box_pin_led is None: + founded_box = ButtonBox.objects.get(pin_button=box_pin_button) + + elif box_id is None and box_name is None and box_pin_button is None and box_pin_led is not None: + founded_box = ButtonBox.objects.get(pin_led=box_pin_led) + + else: + founded_box = None + + # Get from parsed, if None get all + if founded_box is None: + boxes_uses = ButtonBoxUse.objects.all() + serializer = ButtonBoxUseSerializer(boxes_uses, many=True) + + else: + box_uses = ButtonBoxUse.objects.filter(box_used=founded_box) + serializer = ButtonBoxUseSerializer(box_uses, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request, format=None): + """Method for dealing with the POST requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the POST request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + # Get the box name from the query parameters (if Nonne, default to empty str) + parsed_id = request.data.get("id") + parsed_name = request.data.get("name") + # parsed_pin_number = request.data.get("pin_number") + parsed_pin_button = request.data.get("pin_button") + parsed_pin_led = request.data.get("pin_led") + + if parsed_id is None and parsed_name is None and parsed_pin_button is None and parsed_pin_led is None: + ret_dict = { + "error": True, + "message": "Unable to add box used from this end-point withtout parsed info from 'name', 'pin_button' or 'pin_led'" + } + ret_status = status.HTTP_400_BAD_REQUEST + + elif parsed_id is not None and parsed_name is not None and parsed_pin_button is not None and parsed_pin_led is not None: + ret_dict = { + "error": True, + "message": "Unable to add box used from this end-point with MULTIPLE parsed infos from 'id', 'name', 'pin_button' and 'pin_led'. Use only one" + } + ret_status = status.HTTP_400_BAD_REQUEST + + ### ID + elif parsed_id is not None and parsed_name is None and parsed_pin_button is None and parsed_pin_led is None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='id', + parsed_value=parsed_id, + parsed_type='add' + ) + + ### NAME + elif parsed_name is not None and parsed_pin_button is None and parsed_pin_led is None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='name', + parsed_value=parsed_name, + parsed_type='add' + ) + + ### PIN_BUTTON + elif parsed_name is None and parsed_pin_button is not None and parsed_pin_led is None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='pin_button', + parsed_value=parsed_pin_button, + parsed_type='add' + ) + + ### PIN_LED + elif parsed_name is None and parsed_pin_button is None and parsed_pin_led is not None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='pin_led', + parsed_value=parsed_pin_led, + parsed_type='add' + ) + + else: + ret_dict = { + "error": True, + "message": "Option not implemented..." + } + ret_status = status.HTTP_501_NOT_IMPLEMENTED + + return Response(ret_dict, status=ret_status) + + +class ButtonBoxUseCountView(APIView): + """Class for setting the API views for getting the amount of uses from a Button Box. + It is mostly used from Grafana for getting the fancy requests from date windows. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + ### + ### EXAMPLE of the grafana used URL ;) + ### + ### http://localhost:8000/api/box/use/count?from=${__from}&to=${__to} + ### + + ### GET PARSED + query_param_from = request.query_params.get("from") + query_param_to = request.query_params.get("to") + + if query_param_from is not None and query_param_to is None: + ret_list = [ + { + 'error': True, + 'error_msg': "Query parameter for 'from' is parsed and 'to' is not parsed. You have to use BOUTH or NONE" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + elif query_param_from is None and query_param_to is not None: + ret_list = [ + { + 'error': True, + 'error_msg': "Query parameter for 'to' is parsed and 'from' is not parsed. You have to use BOUTH or NONE" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + ### RETURN ALL + elif query_param_from is None and query_param_to is None: + + ret_list = [] + for button_box in ButtonBox.objects.all(): + ret_list.append( + { + "name": button_box.name, + "count": ButtonBoxUse.objects.filter(box_used=button_box).count() + } + ) + + ret_status = status.HTTP_200_OK + + ### RETURN FROM TIME FRAME + elif query_param_from is not None and query_param_to is not None: + + try: + date_from = datetime.fromtimestamp(int(query_param_from) / 1000, timezone.utc) + + except Exception as e: + ret_list = [ + { + 'error': True, + 'error_msg': f"Error parsing epoch time to datetime from parsed 'date_from' parameter with error: {e}" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + try: + date_to = datetime.fromtimestamp(int(query_param_to) / 1000, timezone.utc) + + except Exception as e: + ret_list = [ + { + 'error': True, + 'error_msg': f"Error parsing epoch time to datetime from parsed 'date_to' parameter with error: {e}" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + ret_list = [] + for button_box in ButtonBox.objects.all(): + button_name = button_box.name + count = 0 + for button_use in ButtonBoxUse.objects.filter(date__gt=date_from, date__lt=date_to): + if button_name == button_use.box_name: + count += 1 + ret_list.append( + { + "name": button_name, + "count": count, + "date": button_use.date + } + ) + ret_status = status.HTTP_200_OK + + else: + ret_list = [ + { + 'error': True, + 'error_msg': "Not implemented" + } + ] + ret_status = status.HTTP_501_NOT_IMPLEMENTED + + return Response(ret_list, status=ret_status) diff --git a/django/api_admin/__init__.py b/django/api_admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/api_admin/apps.py b/django/api_admin/apps.py new file mode 100644 index 0000000..bc61874 --- /dev/null +++ b/django/api_admin/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class ApiAdminConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "api_admin" diff --git a/django/api_admin/urls.py b/django/api_admin/urls.py new file mode 100644 index 0000000..6301cd4 --- /dev/null +++ b/django/api_admin/urls.py @@ -0,0 +1,13 @@ +""" + File for setting the URLs used from the api_admin django app. +""" + +from django.urls import path +from . import views + + +urlpatterns = [ + path('box', views.AdminButtonBoxListCreateDeleteView.as_view(), name='admin_box'), + path('box/edit/<int:pk>', views.AdminButtonBoxRetrieveUpdateDestroyView.as_view(), name='admin_box_edit'), + path('box/use', views.AdminButtonBoxUseListCreateView.as_view(), name='admin_box_use'), +] diff --git a/django/api_admin/views.py b/django/api_admin/views.py new file mode 100644 index 0000000..cc8d154 --- /dev/null +++ b/django/api_admin/views.py @@ -0,0 +1,76 @@ +""" + File for setting the view classes used from the api_admin django app. +""" + + +from shared_models.models import ButtonBox, ButtonBoxUse +from shared_models.serializers import ButtonBoxSerializer, ButtonBoxUseSerializer +from rest_framework import generics, status +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from backend.settings import DEFAULT_DJANGO_DEBUG + + +class AdminButtonBoxListCreateDeleteView(generics.ListCreateAPIView): + """Class for setting the API views for Create, Read and Delete the entries at the ButtonBox model. + """ + + permission_classes = [IsAuthenticated] + + queryset = ButtonBox.objects.all() + serializer_class = ButtonBoxSerializer + + def delete(self, request, *args, **kwargs): + """Method for deleting all ButtonBox model objects. + """ + + # Delete only by debug mode + if DEFAULT_DJANGO_DEBUG: + ButtonBox.objects.all().delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + else: + ret_dict = { + "error": False, + "message": "You can only delete all in debub mode --> set the 'DEFAULT_DJANGO_DEBUG' to 'True'" + } + return Response(ret_dict, status=status.HTTP_412_PRECONDITION_FAILED) + + +class AdminButtonBoxRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView): + """Class for setting the API views for Updating entries at the ButtonBox model. + """ + + permission_classes = [IsAuthenticated] + + queryset = ButtonBox.objects.all() + serializer_class = ButtonBoxSerializer + ### pk --> primary_key + lookup_field = "pk" + + +class AdminButtonBoxUseListCreateView(generics.ListCreateAPIView): + """Class for setting the API views for Creating entries at the ButtonBoxUse model. + """ + + permission_classes = [IsAuthenticated] + + queryset = ButtonBoxUse.objects.all() + serializer_class = ButtonBoxUseSerializer + + def delete(self, request, *args, **kwargs): + """Method for deleting all ButtonBoxUse model objects. + """ + + # Delete only by debug mode + if DEFAULT_DJANGO_DEBUG: + ButtonBoxUse.objects.all().delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + else: + ret_dict = { + "error": False, + "message": "You can only delete all in debub mode --> set the 'DEFAULT_DJANGO_DEBUG' to 'True'" + } + return Response(ret_dict, status=status.HTTP_412_PRECONDITION_FAILED) diff --git a/django/api_auth/__init__.py b/django/api_auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/api_auth/apps.py b/django/api_auth/apps.py new file mode 100644 index 0000000..0d4e91b --- /dev/null +++ b/django/api_auth/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class ApiAuthConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "api_auth" diff --git a/django/api_auth/templates/login.html b/django/api_auth/templates/login.html new file mode 100644 index 0000000..f798093 --- /dev/null +++ b/django/api_auth/templates/login.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <title>Login</title> +</head> + +<body> + <h2>Login</h2> + {% if user.is_authenticated %} + <p>Logged in as {{ user.username }}</p> + <a href="{% url 'logout' %}">Logout</a> + {% else %} + <p>You are not logged in.</p> + {% endif %} + <form method="post"> + {% csrf_token %} + {{ form.as_p }} + <button type="submit">Login</button> + </form> +</body> + +</html> \ No newline at end of file diff --git a/django/api_auth/templates/logout.html b/django/api_auth/templates/logout.html new file mode 100644 index 0000000..71a9b56 --- /dev/null +++ b/django/api_auth/templates/logout.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <title>Logout</title> +</head> + +<body> + <h2>You have been logged out.</h2> + <a href="{% url 'login' %}">Log in again</a> +</body> + +</html> \ No newline at end of file diff --git a/django/api_auth/urls.py b/django/api_auth/urls.py new file mode 100644 index 0000000..a7872fb --- /dev/null +++ b/django/api_auth/urls.py @@ -0,0 +1,22 @@ +""" + File for setting the URLs used from the api_auth django app. +""" + +### TOKEN +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + +### LOGIN +from django.contrib.auth import views as auth_views + +from django.urls import path +from . import views + + +urlpatterns = [ + path('test/open', views.TestOpenView.as_view(), name='test-open'), + path('test/closed', views.TestClosedView.as_view(), name='test-closed'), + path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'), + path('logout/', views.custom_logout, name='logout'), + path("token/", TokenObtainPairView.as_view(), name='token_obtain_pair'), + path("token/refresh/", TokenRefreshView.as_view(), name='token_refresh'), +] diff --git a/django/api_auth/views.py b/django/api_auth/views.py new file mode 100644 index 0000000..1946c4f --- /dev/null +++ b/django/api_auth/views.py @@ -0,0 +1,97 @@ + +""" +TODO: Add this !!! +""" +from rest_framework import status +from rest_framework.views import APIView +from rest_framework import permissions +# from rest_framework.permissions import IsAuthenticated +from rest_framework.renderers import JSONRenderer +from rest_framework_simplejwt.authentication import JWTAuthentication +from django.views.decorators.http import require_GET +from django.contrib.auth import logout +from django.shortcuts import redirect +from django.http import JsonResponse + + +class TestOpenView(APIView): + """Class for testing an end-point without authentication. + """ + + permission_classes = [permissions.AllowAny] + + def get(self, request=None, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + ret_dict = { + "error": False, + "message": "This is the auth test open end-point" + } + + return JsonResponse(ret_dict, status=status.HTTP_200_OK) + + +class TestClosedView(APIView): + """Class for testing the authentication. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request=None, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + ret_dict = { + "error": False, + "message": "You are authenticated" + } + + return JsonResponse(ret_dict, status=status.HTTP_200_OK) + + +# from rest_framework.response import Response +@require_GET +def custom_logout(request): + """Method for logging out from django. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + + Returns + ------- + redirect: + Redirects to the login page. + """ + + logout(request) + # return HttpResponse("You have been logged out.") # Temporary for debugging + return redirect('/auth/login/') diff --git a/django/backend/__init__.py b/django/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/backend/asgi.py b/django/backend/asgi.py new file mode 100644 index 0000000..20e4b44 --- /dev/null +++ b/django/backend/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for backend project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") + +application = get_asgi_application() diff --git a/django/backend/settings.py b/django/backend/settings.py new file mode 100644 index 0000000..146db36 --- /dev/null +++ b/django/backend/settings.py @@ -0,0 +1,212 @@ +""" +Django settings for myproject project. + +Generated by 'django-admin startproject' using Django 5.1.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" +import os +from pathlib import Path +from datetime import timedelta +from dotenv import load_dotenv + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-9^f+%n36vxg-^q-iu1$x4&+--&u+i4ds@!xhe@+i8w(-wg00bu" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] + +### +### LOAD VARIABLES +### + +# Try loading from the .env file, if it exists... +load_dotenv() + +try: + DEFAULT_TOKEN_EXPIRE_MIN = int(os.getenv("DEFAULT_TOKEN_EXPIRE_MIN")) + +except Exception: + DEFAULT_TOKEN_EXPIRE_MIN = 10080 + +try: + DEFAULT_TOKEN_REFRESH_DAYS = int(os.getenv("DEFAULT_TOKEN_REFRESH_DAYS")) + +except Exception: + DEFAULT_TOKEN_REFRESH_DAYS = 7 + +try: + fetched = int(os.getenv("DEFAULT_DJANGO_DEBUG")) + if fetched in ('YES', 'yes', 'true', 'True', 'TRUE'): + DEFAULT_DJANGO_DEBUG = True + else: + DEFAULT_DJANGO_DEBUG = False + +except Exception: + DEFAULT_DJANGO_DEBUG = False + +print("\n---> DJANGO VARIABLES") +print(f"DEFAULT_TOKEN_EXPIRE_MIN: {DEFAULT_TOKEN_EXPIRE_MIN}") +print(f"DEFAULT_TOKEN_REFRESH_DAYS: {DEFAULT_TOKEN_REFRESH_DAYS}") +print(f"DEFAULT_DJANGO_DEBUG: {DEFAULT_DJANGO_DEBUG}") +print("<---DJANGO VARIABLES\n") + +# Application definition +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + ### ADDED + "rest_framework", + ### SWAGGER + "drf_yasg", + ### JWT TOKEN + "rest_framework_simplejwt", + ### SSL + "sslserver", + ### MY STUFF + "shared_models", + "api", + "api_auth", + "api_admin", +] + +### +### AUTH +### +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', # Require authentication for all endpoints + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', + ), +} +SESSION_ENGINE = "django.contrib.sessions.backends.db" +LOGIN_REDIRECT_URL = '/swagger/' # Redirect here after login +LOGIN_URL = '/auth/login/' # The URL to redirect to for login +LOGOUT_REDIRECT_URL = '/auth/login/' # Redirect here after logout +### +### TOKEN +### +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=DEFAULT_TOKEN_EXPIRE_MIN), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=DEFAULT_TOKEN_REFRESH_DAYS), + 'SLIDING_TOKEN_LIFETIME': timedelta(days=30), + 'SLIDING_TOKEN_REFRESH_LIFETIME_LATE_USER': timedelta(days=3), + 'SLIDING_TOKEN_LIFETIME_LATE_USER': timedelta(days=30), +} + + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + ### + ### ADDED + ### + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = "backend.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "backend.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +# TIME_ZONE = "UTC" +TIME_ZONE = 'Europe/Berlin' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/django/backend/urls.py b/django/backend/urls.py new file mode 100644 index 0000000..4b69704 --- /dev/null +++ b/django/backend/urls.py @@ -0,0 +1,55 @@ +""" +URL configuration for backend project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from django.contrib import admin +from django.urls import path, include + + +schema_view = get_schema_view( + openapi.Info( + title="c3buttons", + default_version='1.0.0', + description="Swagger Documentation for c3buttons", + # terms_of_service="https://link.to.something", + contact=openapi.Contact(name="fejao", email="dont@bug.me"), + license=openapi.License(name="MIT"), + ), + ### AUTH + public=False, + permission_classes=(permissions.IsAuthenticated,), + +) + + +urlpatterns = [ + path("admin/", admin.site.urls), + ### + ### SWAGGER + ### + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('swagger.json/', schema_view.without_ui(cache_timeout=0), name='schema-json'), + ### + ### APPS + ### + path('auth/', include('api_auth.urls')), + # path("api/box/", include("api.urls")), + path("api/", include("api.urls")), + path("api-admin/", include("api_admin.urls")), +] diff --git a/django/backend/wsgi.py b/django/backend/wsgi.py new file mode 100644 index 0000000..ae9503c --- /dev/null +++ b/django/backend/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for backend project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") + +application = get_wsgi_application() diff --git a/django/manage.py b/django/manage.py new file mode 100755 index 0000000..1917e46 --- /dev/null +++ b/django/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/django/requirements.txt b/django/requirements.txt new file mode 100644 index 0000000..54dae68 --- /dev/null +++ b/django/requirements.txt @@ -0,0 +1,10 @@ +dotenv +### DJANGO +django +djangorestframework +### SWAGGER +drf-yasg +### TOKEN +djangorestframework-simplejwt +### SSL +django-sslserver diff --git a/django/shared_models/__init__.py b/django/shared_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/shared_models/apps.py b/django/shared_models/apps.py new file mode 100644 index 0000000..6668dfb --- /dev/null +++ b/django/shared_models/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class SharedModelsConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "shared_models" diff --git a/django/shared_models/migrations/0001_initial.py b/django/shared_models/migrations/0001_initial.py new file mode 100644 index 0000000..57ae4ce --- /dev/null +++ b/django/shared_models/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.6 on 2025-03-03 18:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ButtonBox', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40, unique=True)), + ('pin_button', models.PositiveIntegerField(unique=True)), + ('pin_led', models.PositiveIntegerField(unique=True)), + ], + options={ + 'db_table': 'button_box', + }, + ), + migrations.CreateModel( + name='ButtonBoxUse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True)), + ('box_used', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shared_models.buttonbox')), + ], + options={ + 'db_table': 'button_box_use', + }, + ), + ] diff --git a/django/shared_models/migrations/__init__.py b/django/shared_models/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/shared_models/models.py b/django/shared_models/models.py new file mode 100644 index 0000000..f7010ef --- /dev/null +++ b/django/shared_models/models.py @@ -0,0 +1,49 @@ +""" + Serializers for the models shared among this django. +""" + +from django.db import models + + +class ButtonBox(models.Model): + """The model for storing the information about the button box. + """ + name = models.CharField(max_length=40, unique=True) + # pin_number = models.PositiveIntegerField(unique=True) + pin_button = models.PositiveIntegerField(unique=True) + pin_led = models.PositiveIntegerField(unique=True) + + class Meta: + """ Meta class for the Button Box model. + Used for setting the name ot the table at the DB. + """ + db_table = "button_box" + + def __str__(self): + """Override from the default __str__ method for returning the name of the box instead of it's ID. + """ + return str(self.name) + + +class ButtonBoxUse(models.Model): + """The model for storing the information about the button box uses. + """ + + box_used = models.ForeignKey( + ButtonBox, + on_delete=models.CASCADE, + ) + date = models.DateTimeField(auto_now_add=True, blank=True) + + class Meta: + """ Meta class for the Button Box Use model. + Used for setting the name ot the table at the DB. + """ + db_table = "button_box_use" + + @property + def box_name(self): + """Property method for fetching the name of the box for the box used. + """ + box_obj = ButtonBox.objects.get(name=self.box_used) + return box_obj.name diff --git a/django/shared_models/serializers.py b/django/shared_models/serializers.py new file mode 100644 index 0000000..4bc1c3b --- /dev/null +++ b/django/shared_models/serializers.py @@ -0,0 +1,41 @@ +""" + Serializer used for the used shared models among the apps. +""" + +from rest_framework import serializers +from .models import ButtonBox, ButtonBoxUse + + +class ButtonBoxSerializer(serializers.ModelSerializer): + """The serializer for the information about the button box. + """ + + class Meta: + """ Meta class for the ButtonBoxSerializer. + Used for returning only the information needed for the ButtonBox model. + """ + model = ButtonBox + fields = [ + # "id", + "name", + # "pin_number", + "pin_button", + "pin_led", + ] + + +class ButtonBoxUseSerializer(serializers.ModelSerializer): + """The serializer for the information about the button box uses. + """ + + class Meta: + """ Meta class for the ButtonBoxSerializer. + Used for returning only the information needed for the ButtonBoxUse model. + """ + model = ButtonBoxUse + fields = [ + # "id", + "box_used", + "box_name", + "date", + ] -- GitLab From 748967d861ff9c357c4d236069e395c79d0b99b6 Mon Sep 17 00:00:00 2001 From: fejao <mail@fejao.de> Date: Wed, 5 Mar 2025 17:19:11 +0100 Subject: [PATCH 4/5] Added docker --- docker/README.md | 0 docker/django/Dockerfile | 30 +++++++++++ docker/docker-compose.yml | 108 ++++++++++++++++++++++++++++++++++++++ docker/gpio/Dockerfile | 50 ++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 docker/README.md create mode 100644 docker/django/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100644 docker/gpio/Dockerfile diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docker/django/Dockerfile b/docker/django/Dockerfile new file mode 100644 index 0000000..646c058 --- /dev/null +++ b/docker/django/Dockerfile @@ -0,0 +1,30 @@ +FROM python:3.10.15-bullseye + +# Set variables +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ="Europe/Berlin" + +# Install dependencies +RUN apt-get update -qq && apt-get upgrade -qqy \ + && apt-get install -qqy \ + python3-pip \ + htop \ + vim \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Create the folder at the container +WORKDIR /c3buttons + +# Install python requirements +COPY ../../django/requirements.txt ./ +RUN pip3 install -r requirements.txt + +# Copy App +COPY ../../django/ . + +### TEST +# CMD ["sleep", "10000000"] + +### PROD +CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..dd06342 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,108 @@ +--- + +services: + django: + image: c3buttons-django:latest + container_name: c3buttons-django + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8000/auth/test/open || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + ### DISABLE PORTS FOR PRODUCTION + ports: + - '8000:8000' + # volumes: + # - ./django/data/db.sqlite3:/c3buttons/db.sqlite3 + environment: + ### + ### PLEASE CHANGE THIS + ### + - DEFAULT_TOKEN_REFRESH_DAYS=7 + - DEFAULT_TOKEN_EXPIRE_MIN=10080 # 10080 min --> 7 days + - DEFAULT_DJANGO_DEBUG=true + + gpio: + image: c3buttons-gpio:latest + container_name: c3buttons-gpio + restart: unless-stopped + ######### + # healthcheck: + # test: ["CMD-SHELL", "curl -f http://localhost:8000/auth/test/open || exit 1"] + # interval: 30s + # timeout: 5s + # retries: 5 + ######### + # depends_on: + # django: + # condition: service_healthy + links: + - django + privileged: true + devices: + - /dev/gpiomem:/dev/gpiomem + volumes: + # - /dev/gpiomem:/dev/gpiomem + ### DEBUG + - ../gpio_buttons:/c3buttons + environment: + ### + ### PLEASE CHANGE THIS + ### + - 'DATABASE_API_TOKEN=<PLEASE_CHANGE_THIS>' + - 'BOXES_USED="BUTTON_BOX_1, BUTTON_BOX_2, BUTTON_BOX_3"' + # - 'BOXES_USED="BUTTON_BOX_1, BUTTON_BOX_2"' + - 'BUTTON_BOX_1={"pin_button": 16, "pin_led": 14}' + - 'BUTTON_BOX_2={"pin_button": 20, "pin_led": 19}' + - 'BUTTON_BOX_3={"pin_button": 21, "pin_led": 13}' + + grafana: + image: grafana/grafana:latest + container_name: c3buttons-grafana + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + # depends_on: + # django: + # condition: service_healthy + links: + - django + ### DISABLE PORTS FOR PRODUCTION + ports: + - '3000:3000' + volumes: + - ./grafana/data:/var/lib/grafana + environment: + - FOO=BAR + - GF_FEATURE_TOGGLES_PUBLICDASHBOARDS=true + - TLS_SKIP_VERIFY_INSECURE=true + + proxy: + image: 'jc21/nginx-proxy-manager:latest' + container_name: c3buttons-proxy + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:81 || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + # depends_on: + # django: + # condition: service_healthy + # grafana: + # condition: service_healthy + links: + - django + - grafana + ports: + - '80:80' + - '443:443' + ### This is the admin port, disable for Production + - '81:81' + volumes: + - ./proxy/data:/data + - ./proxy/letsencrypt:/etc/letsencrypt diff --git a/docker/gpio/Dockerfile b/docker/gpio/Dockerfile new file mode 100644 index 0000000..d1771ad --- /dev/null +++ b/docker/gpio/Dockerfile @@ -0,0 +1,50 @@ +FROM ubuntu:24.04 + +# Set variables +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ="Europe/Berlin" + +# Install dependencies +RUN apt-get update -qq && apt-get upgrade -qqy \ +# python3-dev \ +# python3-venv \ + && apt-get install -qqy \ + python3-pip \ + python3-lgpio \ + python3-pigpio \ + python3-rpi.gpio \ + htop \ + vim \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# New debian base FUCKING shit +RUN rm -rfv /usr/lib/python3*/EXTERNALLY-MANAGED + +# Change user for installing python packages -> Set user and group +ARG user=appuser +ARG group=appuser +ARG uid=2000 +ARG gid=2000 +RUN groupadd -g ${gid} ${group} +# <--- the '-m' create a user home directory +RUN useradd -u ${uid} -g ${group} -s /bin/sh -m ${user} + +# Create the folder at the container +WORKDIR /home/appuser/c3buttons + +# Install python requirements +COPY ../../gpio_buttons/requirements.txt ./ +RUN pip3 install -r requirements.txt + +# Install GPIO +RUN pip3 install gpiozero --break-system-packages + +# Copy App +COPY ../../gpio_buttons/ . + +### TEST +CMD ["sleep", "1000000"] + +### PROD +# CMD ["python3", "main.py"] -- GitLab From 5e29c0534645c709bee582ee301b62053a8b8724 Mon Sep 17 00:00:00 2001 From: fejao <mail@fejao.de> Date: Wed, 5 Mar 2025 17:20:47 +0100 Subject: [PATCH 5/5] Added gpio_buttons --- gpio_buttons/.env | 52 ++++ gpio_buttons/logs/example.log | 0 gpio_buttons/main.py | 92 +++++++ gpio_buttons/requirements.txt | 7 + gpio_buttons/utils/boxes/__init__.py | 0 gpio_buttons/utils/boxes/app_boxes.py | 98 +++++++ gpio_buttons/utils/boxes/box_button.py | 70 +++++ gpio_buttons/utils/boxes/box_led.py | 72 +++++ gpio_buttons/utils/boxes/box_pin.py | 42 +++ gpio_buttons/utils/configuration/__init__.py | 0 .../utils/configuration/app_configuration.py | 178 +++++++++++++ .../utils/configuration/app_logger.py | 159 +++++++++++ gpio_buttons/utils/configuration/config.yml | 27 ++ .../utils/configuration/default_values.py | 57 ++++ gpio_buttons/utils/configuration/from_file.py | 32 +++ .../configuration/from_file_environment.py | 250 ++++++++++++++++++ .../utils/configuration/from_file_yaml.py | 83 ++++++ gpio_buttons/utils/database/__init__.py | 0 gpio_buttons/utils/database/app_database.py | 64 +++++ gpio_buttons/utils/database/from_api.py | 110 ++++++++ gpio_buttons/utils/database/from_database.py | 31 +++ .../utils/database/from_local_file.py | 159 +++++++++++ pycodestyle.cfg | 8 + 23 files changed, 1591 insertions(+) create mode 100644 gpio_buttons/.env create mode 100644 gpio_buttons/logs/example.log create mode 100755 gpio_buttons/main.py create mode 100644 gpio_buttons/requirements.txt create mode 100644 gpio_buttons/utils/boxes/__init__.py create mode 100644 gpio_buttons/utils/boxes/app_boxes.py create mode 100644 gpio_buttons/utils/boxes/box_button.py create mode 100644 gpio_buttons/utils/boxes/box_led.py create mode 100644 gpio_buttons/utils/boxes/box_pin.py create mode 100644 gpio_buttons/utils/configuration/__init__.py create mode 100644 gpio_buttons/utils/configuration/app_configuration.py create mode 100644 gpio_buttons/utils/configuration/app_logger.py create mode 100644 gpio_buttons/utils/configuration/config.yml create mode 100644 gpio_buttons/utils/configuration/default_values.py create mode 100644 gpio_buttons/utils/configuration/from_file.py create mode 100644 gpio_buttons/utils/configuration/from_file_environment.py create mode 100644 gpio_buttons/utils/configuration/from_file_yaml.py create mode 100644 gpio_buttons/utils/database/__init__.py create mode 100644 gpio_buttons/utils/database/app_database.py create mode 100644 gpio_buttons/utils/database/from_api.py create mode 100644 gpio_buttons/utils/database/from_database.py create mode 100644 gpio_buttons/utils/database/from_local_file.py create mode 100644 pycodestyle.cfg diff --git a/gpio_buttons/.env b/gpio_buttons/.env new file mode 100644 index 0000000..f1c91a9 --- /dev/null +++ b/gpio_buttons/.env @@ -0,0 +1,52 @@ + +DESCRIPTION='<PLEASE_SET_THIS>' + +# DEBUG='false' +DEBUG='true' +VERBOSE='false' + +### +### DATABASE_TYPE --> 'local_file' = using local running django with SQLite | 'api' = access it with API +### +# DATABASE_TYPE='local_file' +DATABASE_TYPE='api' + +### +### DATABASE_TYPE: local_file +### +# Pi +DATABASE_LOCAL_DB_FILE_PATH='/home/pi/Coding/c3buttons/django/db.sqlite3' +# # Local +# DATABASE_LOCAL_DB_FILE_PATH='/home/fejao/Coding/CCC/git_cccv/c3buttons/django/db.sqlite3' + +### +### DATABASE_TYPE: api +### +# Pi +DATABASE_API_URL='http://django:8000' +DATABASE_API_TOKEN='<PLEASE_SER_THIS>' +# # Local +# DATABASE_API_URL='http://localhost:8000' +# DATABASE_API_TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzQxNjMxMTI3LCJpYXQiOjE3NDEwMjYzMjcsImp0aSI6Ijk5MzFjMjhkNGE2MDQ4MTg5ZGYwYTEzZDcyNzE2NDI5IiwidXNlcl9pZCI6MX0.fNIm-6bai4jJkL5NMSW1K9mWIP_XmHzPEkTxgzqJd_4' + +### +### BOXES CONFIG +### +BOX_LED_BLINK_TIMES=3 + +# BOX_LED_ENABLED='false' +BOX_LED_ENABLED='true' + +# BOX_LED_AWAYS_ON='true' +BOX_LED_AWAYS_ON='false' + +### BOX_BUTTON_BOUNCE_TIME in microseconds +BOX_BUTTON_BOUNCE_TIME= '0.1' + +### +### BOXES USED +### +BUTTON_BOX_1='{"pin_button": 16, "pin_led": 14}' +BUTTON_BOX_2='{"pin_button": 20, "pin_led": 19}' +# This is the list of the boxes used, if you add new ones, please alse add it's configuration +BOXES_USED='BUTTON_BOX_1, BUTTON_BOX_2' diff --git a/gpio_buttons/logs/example.log b/gpio_buttons/logs/example.log new file mode 100644 index 0000000..e69de29 diff --git a/gpio_buttons/main.py b/gpio_buttons/main.py new file mode 100755 index 0000000..426b199 --- /dev/null +++ b/gpio_buttons/main.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +import sys +### +from signal import pause + +try: + from utils.configuration.app_configuration import AppConfiguration +except ImportError as e: + print("Unable to import 'AppConfiguration' from 'utils.configuration.app_configuration.py' file at the 'main.py'") + sys.exit(1) + +try: + from utils.database.app_database import AppDatabase +except ImportError as e: + print("Unable to import 'AppDatabase' from 'utils.database.app_database.py' file at the 'main.py'") + sys.exit(1) + +try: + from utils.boxes.app_boxes import AppBoxes +except ImportError as e: + print("Unable to import 'AppDatabase' from 'utils.database.app_database.py' file at the 'main.py'") + sys.exit(1) + + +############# +############# +############# + + +BTN_COUNTER = 0 + +def btn_pressed(): + global BTN_COUNTER + print(f"button pressed | counter: {BTN_COUNTER}") + BTN_COUNTER += 1 + +def btn_pressed_2(): + global BTN_COUNTER + print(f"button pressed_2 | counter: {BTN_COUNTER}") + BTN_COUNTER += 1 + +# main function +def main(): + print("\n-------> START \n") + + # Get the configuration, from file or parsed... + configuration = AppConfiguration() + + ### + ### DEBUG + ### + from pprint import pp + # import pdb; pdb.set_trace() + # pp(configuration.dict_values) + + # Set the used DB from the configuration... + database = AppDatabase(configuration) + + ### + ### LOCALSQLite + ### + # ### WORKS + # foo = database.db.table_select_all(configuration.database_table_name_button_box) + # import pdb; pdb.set_trace() + # pp(foo) + + ### + ### API + ### + # # import pdb; pdb.set_trace() + # test_data = { + # # "id": "1" + # "id": "2" + # } + # foo = database.db.post(endpoint="api/box/use", json=test_data) + # pp(foo) + + # bar = database.db.get(endpoint="api/box/use/count") + # pp(bar) + + # Set the Button Boxes + boxes = AppBoxes(configuration, database) + + import pdb; pdb.set_trace() + pp(boxes) + + print("\n <------- FINISH \n") + + +if __name__=="__main__": + main() diff --git a/gpio_buttons/requirements.txt b/gpio_buttons/requirements.txt new file mode 100644 index 0000000..51d4fb6 --- /dev/null +++ b/gpio_buttons/requirements.txt @@ -0,0 +1,7 @@ +# argparse +### +# logging +pyaml-env +dotenv +requests +# gpiozero diff --git a/gpio_buttons/utils/boxes/__init__.py b/gpio_buttons/utils/boxes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gpio_buttons/utils/boxes/app_boxes.py b/gpio_buttons/utils/boxes/app_boxes.py new file mode 100644 index 0000000..73244cb --- /dev/null +++ b/gpio_buttons/utils/boxes/app_boxes.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +""" + utils.boxes.app_boxes.py file for the c3buttons system. +""" + +### +### DEPENDENCIES +### +import sys +from signal import pause + +### LOCAL +try: + from utils.configuration.app_logger import set_logger_from_config +except ImportError as e: + print("Unable to import 'setup_logger' from 'utils.configuration.app_logger.py' file at 'utils.database.app_database.py' file") + sys.exit(1) + +try: + from .box_led import BoxLed +except ImportError as e: + print("Unable to import 'BoxLed' from 'utils.boxes.box_led.py' file at 'utils.boxes.app_boxes.py' file" ) + sys.exit(1) + +try: + from .box_button import BoxButton +except ImportError as e: + print("Unable to import 'BoxButton' from 'utils.boxes.box_button.py' file at 'utils.boxes.app_boxes.py' file" ) + sys.exit(1) + + +class AppBoxes(): + """ + Class for setting the Boxes of the app. + """ + + def __init__(self, configuration: object, database: object) -> None: + """ + Function for initialiazing the AppConfiguration class. + Sets the class variables. + + Returns + ------- + None + """ + + self.configuration = configuration + self.database = database + + self.logger = set_logger_from_config(name=__name__, config=self.configuration) + + self.buttons = {} + self.set_boxes() + self.set_button_functions() + + def set_boxes(self): + + self.logger.debug("set_boxes") + + for box_name in self.configuration.boxes_used: + if self.configuration.boxes.get(box_name) is not None: + box_info = self.configuration.boxes.get(box_name) + if box_info.get('pin_button') is None: + print(f"\n\n The 'pin_button' value for box with name '{box_name}' was not set" ) + sys.exit(1) + + if box_info.get('pin_led') is None: + print(f"\n\n The 'pin_led' value for box with name '{box_name}' was not set" ) + sys.exit(1) + + box_led = None + if self.configuration.box_led_enabled: + box_led = BoxLed( + configuration=self.configuration, + box_name=box_name, + ) + + box_button = BoxButton( + configuration=self.configuration, + box_name=box_name, + database=self.database, + box_led=box_led, + ) + + self.buttons[box_name] = box_button + + def set_button_functions(self): + + self.logger.debug("set_button_functions") + + try: + for btn_name, btn in self.buttons.items(): + btn.button.when_pressed = btn.activated + self.logger.debug(f"Button functions for box: '{btn_name}' activated...") + + pause() + finally: + pass diff --git a/gpio_buttons/utils/boxes/box_button.py b/gpio_buttons/utils/boxes/box_button.py new file mode 100644 index 0000000..8a9d3ff --- /dev/null +++ b/gpio_buttons/utils/boxes/box_button.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" + utils.database.from_api.py file for the c3 buttons. +""" + + +### +### DEPENDENCIES +### +import sys +from gpiozero import Button +from datetime import datetime + +### LOCAL +try: + from .box_pin import BoxPin +except ImportError as e: + print("Unable to import 'BoxPin' from 'utils.boxes.box_pin.py' file at 'utils.boxes.box_button.py' file") + sys.exit(1) + + +class BoxButton(BoxPin): + """ + TODO: Documentation + """ + + def __init__(self, configuration: dict, box_name: str, database: object, box_led: str = None) -> None: + """ + TODO: Documentation + """ + + ### Init super + super().__init__(name=__name__, configuration=configuration, box_name=box_name) + + self.logger.debug(f"__init__ - box_name: '{box_name}'") + + self.box_led = box_led + + self.database = database + + self.set_pin_usage() + + def set_pin_usage(self): + self.logger.debug(f"set_pin_usage - box_name: '{self.box_name}'") + + self.button_press_count = 0 + try: + self.bounce_time = float(self.configuration.box_button_bounce_time) + + except Exception as e: + print(f"\n\n Error setting the bounce_time from button with exception: {e}") + sys.exit(1) + + self.button = Button(self.pin_button, bounce_time=self.bounce_time) + + def update_db_from_activated(self): + self.logger.debug(f"update_db_from_activated - box_name '{self.box_name}' | pin_button: '{self.pin_button}'") + + self.database.add_button_box_usage(box_name=self.box_name) + + def activated(self): + self.logger.debug(f"activated - box_name '{self.box_name}' | pin_button: '{self.pin_button}' | count: '{self.button_press_count}'") + self.button_press_count += 1 + + ### Update DB + self.database.db.add_button_box_usage(box_name=self.box_name) + + ### Set LED activation + if self.box_led: + self.box_led.activated() diff --git a/gpio_buttons/utils/boxes/box_led.py b/gpio_buttons/utils/boxes/box_led.py new file mode 100644 index 0000000..440edcf --- /dev/null +++ b/gpio_buttons/utils/boxes/box_led.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" + utils.database.from_api.py file for the c3 buttons. +""" + + +### +### DEPENDENCIES +### +import sys +import time +from gpiozero import LED + +### LOCAL +try: + from .box_pin import BoxPin +except ImportError as e: + print("Unable to import 'BoxPin' from 'utils.boxes.box_pin.py' file at 'utils.boxes.box_button.py' file" ) + sys.exit(1) + + +class BoxLed(BoxPin): + """ + TODO: Documentation + """ + + def __init__(self, configuration: dict, box_name: str) -> None: + """ + TODO: Documentation + """ + + ### Init super + super().__init__(name=__name__, configuration=configuration, box_name=box_name) + + self.logger.debug(f"__init__ - box_name: '{box_name}'") + + self.blink_times = int(self.configuration.box_led_blink_times) + + self.set_pin_usage() + + def set_pin_usage(self): + self.logger.debug(f"set_pin_usage - box_name: '{self.box_name}'") + + self.led = LED(self.pin_led) + + if self.configuration.box_led_always_on: + self.led.on() + + def activated(self): + self.logger.debug(f"activated - box_name: '{self.box_name}' | pin_led: '{self.pin_led}'") + + i = 0 + if self.configuration.box_led_always_on: + while i in range(self.blink_times): + self.led.off() + time.sleep(0.5) + self.led.on() + time.sleep(0.5) + i += 1 + + self.led.on() + + else: + while i in range(self.blink_times): + self.led.on() + time.sleep(0.5) + self.led.off() + time.sleep(0.5) + i += 1 + + self.led.off() + diff --git a/gpio_buttons/utils/boxes/box_pin.py b/gpio_buttons/utils/boxes/box_pin.py new file mode 100644 index 0000000..f4f800e --- /dev/null +++ b/gpio_buttons/utils/boxes/box_pin.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +""" + utils.configuration.from_file.py file for the c3 buttons. + This is the super class used from the other classes that fetches configurations from a file. +""" +import sys +from abc import ABC, abstractmethod + +### LOCAL +try: + from utils.configuration.app_logger import set_logger_from_config +except ImportError as e: + print("Unable to import 'set_logger_from_dict' from 'utils.configuration.app_logger.py' file at 'utils.boxes.box_pin.py' file" ) + sys.exit(1) + + +class BoxPin(ABC): + """ TODO + """ + + def __init__(self, name: str, configuration: object, box_name: str) -> None: + + self.configuration = configuration + self.logger = set_logger_from_config(name=name, config=self.configuration) + self.get_box_config(box_name) + + def get_box_config(self, box_name): + self.logger.debug(f"get_box_config - box_name: {box_name}") + + self.box_name = box_name + box_info = self.configuration.boxes.get(box_name) + self.pin_button = box_info.get('pin_button') + self.pin_led = box_info.get('pin_led') + self.enabled_led=self.configuration.box_led_enabled + + @abstractmethod + def set_pin_usage(self): + pass + + @abstractmethod + def activated(self): + pass diff --git a/gpio_buttons/utils/configuration/__init__.py b/gpio_buttons/utils/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gpio_buttons/utils/configuration/app_configuration.py b/gpio_buttons/utils/configuration/app_configuration.py new file mode 100644 index 0000000..0995a98 --- /dev/null +++ b/gpio_buttons/utils/configuration/app_configuration.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" + utils.configuration.app_configuration.py file for the c3buttons system. + In this file the configuration will be setted from this order: + config.yml -> environment vars +""" + +### +### DEPENDENCIES +### +import sys + +### LOCAL +try: + from .default_values import DEFAULT_VALUES +except ImportError as e: + print("Unable to import 'utils.configuration.default_vals.py' from 'utils.configuration.app_configuration.py' file") + sys.exit(1) + +try: + from .app_logger import set_logger_from_default +except ImportError as e: + print("Unable to import 'setup_logger' from 'utils.configuration.app_logger.py' file at the 'utils.configuration.app_configuration.py'") + sys.exit(1) + +try: + from .from_file_environment import ConfigurationFromDotEnv +except ImportError as e: + print("Unable to import 'ConfigurationFromFile' from 'utils.configuration.from_file_environment.py' file at the 'utils.configuration.app_configuration.py'") + sys.exit(1) + +try: + from .from_file_yaml import ConfigurationFromYamlFile +except ImportError as e: + print("Unable to import 'ConfigurationFromYamlFile' from 'utils.configuration.from_file_yaml.py' file at the 'utils.configuration.app_configuration.py'") + sys.exit(1) + + +class AppConfiguration(): + """ + Class for setting the configuration of the app. + """ + + def __init__(self) -> None: + """ + Function for initialiazing the AppConfiguration class. + Sets the class variables. + + Returns + ------- + None + """ + self.logger = set_logger_from_default(name=__name__, values_default=DEFAULT_VALUES) + + self.logger.info("---> STARTED") + + self.__configuration_colors__() + + self.set_attr_from_parsed() + + if self.debug: + self.__debug_attrs__() + + def check_missing_attrs(self, from_env: dict, from_yaml: dict) -> dict: + + self.logger.debug("check_missin_attrs") + + ret_dict = {} + + # If in from_env and not in from_yaml + for name, value in from_env.dict_values.items(): + + if name not in from_yaml.dict_values.keys(): + ret_dict[name] = value + + # if name in from_yaml.dict_values.keys(): + else: + + if name == "boxes": + yaml_value = from_yaml.dict_values.get("boxes") + if len(value) >= len(yaml_value): + boxes_dict = value + boxes_list = from_env.dict_values.get("boxes_used") + self.logger.info("---> Using boxes configuration from OS environments") + + else: + boxes_dict = yaml_value + boxes_list = from_yaml.dict_values.get("boxes_used") + self.logger.info("---> Using boxes configuration from config.yml file") + + # ret_dict[name] = new_value + ret_dict["boxes"] = boxes_dict + ret_dict["boxes_used"] = boxes_list + + elif name == "boxes_used": + pass + + else: + ret_dict[name] = value + + + ### DEBUG + # from pprint import pp + # print(f"name: {name} | ret_dict: {ret_dict}") + + # If in from_yaml and not in from_env + for name, value in from_yaml.dict_values.items(): + if name not in ret_dict.keys(): + ret_dict[name] = value + + # from pprint import pp + # import pdb; pdb.set_trace() + # pp(ret_dict) + + return ret_dict + + def attr_set_from_dict(self, dict_values: dict) -> None: + + self.logger.debug("attr_set_from_dict") + + for name, value in dict_values.items(): + setattr(self, name.lower(), value) + + def set_attr_from_parsed(self) -> None: + + self.logger.debug("set_attr_from_parsed") + + ### Get attrs from env + from_env = ConfigurationFromDotEnv(default_values= DEFAULT_VALUES) + + ### If not using, set it from the os.env + if not DEFAULT_VALUES.get("DEFAULT_USING_PARSED_YAML"): + dict_values = from_env.dict_values + + else: + ### Fetch params from YAML file + from_yaml = ConfigurationFromYamlFile(default_values= DEFAULT_VALUES) + dict_values = self.check_missing_attrs(from_env=from_env, from_yaml=from_yaml) + + ### Set the attributes as a dict aswell + self.attr_set_from_dict(dict_values) + + self.dict_values = dict_values + + def __configuration_colors__(self) -> None: + """ + Method for setting the terminal output with colors. + + Returns + ------- + None + """ + self.logger.debug("__configuration_colors__") + + self.color_gray = "\033[90m" + self.color_red = "\033[91m" + self.color_green = "\033[92m" + self.color_orange = "\033[93m" + self.color_blue = "\033[94m" + self.color_pink = "\033[95m" + self.color_yellow = "\033[93m" + self.color_end = "\033[0m" + + def __debug_attrs__(self) -> None: + """ + Function for printing the configuration parameters from the app. + + Returns + ------- + None + """ + self.logger.debug("__debug_attrs__") + ### PRINT ARGS + print("\n" + self.color_blue + "CONFIGURATION ATTRS:" + self.color_end) + for name, value in self.dict_values.items(): + print(self.color_green + f"\t- {name}: " + self.color_end + f"'{value}'") + + print("\n") diff --git a/gpio_buttons/utils/configuration/app_logger.py b/gpio_buttons/utils/configuration/app_logger.py new file mode 100644 index 0000000..6f3da52 --- /dev/null +++ b/gpio_buttons/utils/configuration/app_logger.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" + utils.configuration.app_logger.py file for the c3 printing system. +""" + +### +### DEPENDENCIES +### +import sys +import logging + + +def setup_logger(name: str, + level: str | None = "INFO", + log_formatter: str | None = None, + log_file: str | None = None, + log_file_append: bool | None = False + ) -> logging.Logger: + + if level == "DEBUG": + lvl = logging.DEBUG + + elif level == "INFO": + lvl = logging.INFO + + elif level == "WARNING": + lvl = logging.WARNING + + elif level == "ERROR": + lvl = logging.ERROR + + else: + print(f"\nERROR ---> Could not set logger for: {name}.\n LogLevel parsed '{level}' is unknown...\n\n") + sys.exit() + + # Create a logger + logger = logging.getLogger(name) + + # Set the logging level + logger.setLevel(lvl) + + # Avoid adding multiple handlers + if not logger.hasHandlers(): + + # Set log formatter + if log_formatter is None: + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + else: + formatter = logging.Formatter(log_formatter) + + if log_file is not None: + if log_file_append: + fh = logging.FileHandler(log_file, mode="a") + else: + fh = logging.FileHandler(log_file) + + fh.setLevel(lvl) + fh.setFormatter(formatter) + logger.addHandler(fh) + + # Create a console handler (optional, if you still want console output) + ch = logging.StreamHandler() + ch.setLevel(lvl) + # # Change to WARNING to only see warnings/errors in the terminal + # ch.setLevel(logging.WARNING) + # ch.setLevel(logging.INFO) + + # Add formatter to both handlers + ch.setFormatter(formatter) + + # Add handlers to the logger + logger.addHandler(ch) + + return logger + + +def set_logger_from_default(name: str, values_default: dict) -> logging.Logger: + + log_file = None + if values_default.get('DEFAULT_LOG_TO_FILE'): + log_file = values_default.get('DEFAULT_LOG_FILE_PATH') + + if values_default.get('DEFAULT_DEBUG'): + logger = setup_logger( + name=name, + level="DEBUG", + log_formatter=values_default.get('DEFAULT_LOG_FORMAT'), + log_file=log_file, + ) + + else: + logger = setup_logger( + name=name, + # level=logging.DEBUG, + log_formatter=values_default.get('DEFAULT_LOG_FORMAT'), + log_file=log_file, + ) + + return logger + + +def set_logger_from_config(name: str, config: object) -> logging.Logger: + + log_file = None + log_file_append = True + + if config.log_to_file: + log_file = config.log_file_path + log_file_append = config.log_file_append_mode + + if config.debug: + logger = setup_logger( + name=name, + level="DEBUG", + log_formatter=config.log_format, + log_file=log_file, + log_file_append=log_file_append, + ) + + else: + logger = setup_logger( + name=name, + # level="DEBUG", + log_formatter=config.log_format, + log_file=log_file, + log_file_append=log_file_append, + ) + + return logger + + +def set_logger_from_dict(name: str, dict_values: dict) -> logging.Logger: + + log_file = None + log_file_append = True + if dict_values.get('log_to_file'): + log_file = dict_values.get('log_file_path') + log_file_append = dict_values.get('log_file_append_mode') + + if dict_values.get('debug'): + logger = setup_logger( + name=name, + level="DEBUG", + log_formatter=dict_values.get('log_format'), + log_file=log_file, + log_file_append=log_file_append, + ) + + else: + logger = setup_logger( + name=name, + # level="DEBUG", + log_formatter=dict_values.get('log_format'), + log_file=log_file, + log_file_append=log_file_append, + ) + + return logger \ No newline at end of file diff --git a/gpio_buttons/utils/configuration/config.yml b/gpio_buttons/utils/configuration/config.yml new file mode 100644 index 0000000..21a79ee --- /dev/null +++ b/gpio_buttons/utils/configuration/config.yml @@ -0,0 +1,27 @@ +--- + +default: + debug: true + verbose: true + enabled_led: True + description: "Updated default app description" + + boxes_used: "box_1, box_2" + + box_1: + pin_button: 21 + pin_led: 22 + + box_2: + pin_button: 23 + pin_led: 23 + + + ### + ### DEBUG + ### + foo: "bar" + # boxes_used: "box_1, box_2, box_3" + # box_3: + # pin_button: 24 + # pin_led: 25 diff --git a/gpio_buttons/utils/configuration/default_values.py b/gpio_buttons/utils/configuration/default_values.py new file mode 100644 index 0000000..13c9a88 --- /dev/null +++ b/gpio_buttons/utils/configuration/default_values.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" + utils.config.default_vals.py file for the c3 buttons. + Used for setting the default values from the 'utils.app_configuration.py' file. +""" + +DEFAULT_VALUES = { + 'DEFAULT_DESCRIPTION': 'UPDATE_THIS', + # 'DEFAULT_PATH_CONFIG_ENV': './.env', + 'DEFAULT_PATH_CONFIG_ENV': '.env', + ### + ### config.yml + ### + 'DEFAULT_USING_PARSED_YAML': False, + 'DEFAULT_PATH_CONFIG_YML': './utils/configuration/config.yml', + ### + ### DEBUG/VEBOSE + ### + 'DEFAULT_DEBUG': False, + 'DEFAULT_VERBOSE': False, + ### + ### LOG + ### + # "DEFAULT_LOG_TO_FILE": True, + "DEFAULT_LOG_TO_FILE": False, + "DEFAULT_LOG_FILE_APPEND_MODE": True, + "DEFAULT_LOG_FILE_PATH": "./logs/example.log", + "DEFAULT_LOG_FORMAT": '%(asctime)s - %(levelname)s - %(name)s - %(message)s', + ### + ### DB + ### + "DEFAULT_DATABASE_TYPE":"local_file", + "DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX": "button_box", + "DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX_USE": "button_box_use", + "DEFAULT_DATABASE_LOCAL_DB_FILE_PATH":"Please set --> DATABASE_LOCAL_DB_FILE_PATH", + "DEFAULT_DATABASE_API_URL":"http://localhost:8000", + "DEFAULT_DATABASE_API_TOKEN":"Please set --> DATABASE_API_TOKEN", + ### + ### BOXES CONFIG + ### + 'DEFAULT_BOX_LED_ENABLED': False, + 'DEFAULT_BOX_LED_AWAYS_ON': False, + 'DEFAULT_BOX_LED_BLINK_TIMES': 5, + + ### DEFAULT_BOX_BUTTON_BOUNCE_TIME in microseconds + 'DEFAULT_BOX_BUTTON_BOUNCE_TIME': "0.1", + + ### + ### BOXES USED + ### + "DEFAULT_BOXES_USED": "DEFAULT_BOX_1, DEFAULT_BOX_2, DEFAULT_BOX_3, DEFAULT_BOX_4, DEFAULT_BOX_5", + "DEFAULT_BOX_1": '{"pin_button": 14, "pin_led": 15}', + "DEFAULT_BOX_2": '{"pin_button": 18, "pin_led": 23}', + "DEFAULT_BOX_3": '{"pin_button": 24, "pin_led": 25}', + "DEFAULT_BOX_4": '{"pin_button": 8, "pin_led": 7}', + "DEFAULT_BOX_5": '{"pin_button": 12, "pin_led": 16}', +} diff --git a/gpio_buttons/utils/configuration/from_file.py b/gpio_buttons/utils/configuration/from_file.py new file mode 100644 index 0000000..b32f656 --- /dev/null +++ b/gpio_buttons/utils/configuration/from_file.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" + utils.configuration.from_file.py file for the c3 buttons. + This is the super class used from the other classes that fetches configurations from a file. +""" +import sys +from abc import ABC, abstractmethod + +### LOCAL +try: + from .app_logger import set_logger_from_default +except ImportError as e: + print("Unable to import 'setup_logger' from 'utils.app_logger.py' file at 'utils.configuration.from_file.py' file" ) + sys.exit(1) + + +class ConfigurationFromFile(ABC): + """ TODO + """ + + def __init__(self, name: str, default_values: dict) -> None: + + self.default_values = default_values + # self.logger = set_logger_from_default(name=__name__, values_default=self.default_values) + self.logger = set_logger_from_default(name=name, values_default=self.default_values) + + self.path_config_file = None + self.dict_values = {} + + @abstractmethod + def get_values(self): + pass diff --git a/gpio_buttons/utils/configuration/from_file_environment.py b/gpio_buttons/utils/configuration/from_file_environment.py new file mode 100644 index 0000000..c8612a7 --- /dev/null +++ b/gpio_buttons/utils/configuration/from_file_environment.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" + utils.configuration.from_file.py file for the c3 buttons. +""" + +### +### DEPENDENCIES +### +import sys +import os +import json + +try: + from dotenv import load_dotenv +except ImportError as e: + print("Unable to import 'dotenv'") + print("Please install it with 'pip(3) install dotenv --user'") + sys.exit(1) + +### LOCAL +try: + from .from_file import ConfigurationFromFile +except ImportError as e: + print("Unable to import 'ConfigurationFromFile' from 'utils.configuration.from_file.py' file at 'utils.configuration.from_file_environment.py' file" ) + sys.exit(1) + + +class ConfigurationFromDotEnv(ConfigurationFromFile): + """ TODO + """ + + def __init__(self, default_values: dict) -> None: + """ + TODO: Documentation + """ + + ### Init super + super().__init__(name=__name__, default_values=default_values) + + self.path_config_file = self.default_values.get('DEFAULT_PATH_CONFIG_ENV') + self.dict_values["boxes"] = {} + self.get_values() + + def __get_env_bool__(self, name_config: str, name_config_default: str) -> bool: + """ + Method to fetch the environment variable from a boolean type + + Returns + ------- + bool: + Return a boolean value from fetching the setted environment variable + """ + self.logger.debug(f"__get_env_bool__ - {name_config}") + try: + fetched = os.getenv(name_config) + + if fetched: + if fetched in ('YES', 'yes', 'true', 'True', 'TRUE'): + ret_bool = True + elif fetched in ('NO', 'no', 'false', 'False', 'FALSE'): + ret_bool = False + else: + print(f"\n\n Unable to fetch the parsed environment '{name_config}'. It is set but not 'true' or 'false' \n\n") + sys.exit(1) + else: + ret_bool = self.default_values.get(name_config_default) + + except Exception as e: + print(f"\n\n Unable to fetch the parsed environment '{name_config}' \n| The exception is: '{e}' \n\n") + sys.exit(1) + + return ret_bool + + def __get_env__(self, name_config: str, name_config_default: str) -> (str | int): + """ + Method to fetch the environment variable from a string or int type. + + Returns + ------- + str | int: + Return a the value from fetching the setted environment variable. + """ + self.logger.debug(f"__get_env__ - {name_config}") + try: + fetched = os.getenv(name_config) + + ### If not setted, get it from config + if fetched is None: + fetched = self.default_values.get(name_config_default) + + except Exception as e: + print(f"\n\n Unable to fetch the parsed environment '{name_config}' \n| The exception is: '{e}' \n\n") + sys.exit(1) + + return fetched + + def __get_env_dict__(self, name_config: str, name_config_default: str): + """ + Method to fetch the environment variable from a string or int type. + + Returns + ------- + str | int: + Return a the value from fetching the setted environment variable. + """ + self.logger.debug(f"__get_env_dict__ - {name_config}") + + fetched = self.__get_env__( + name_config=name_config, + name_config_default=name_config_default + ) + + try: + ret_dict = json.loads(fetched) + + except Exception as e: + print(f"\n\n Unable to parse the dictionary environment from: '{name_config}' \n| The exception is: '{e}' \n\n") + sys.exit(1) + + self.dict_values["boxes"][name_config.lower()] = ret_dict + + def get_values(self) -> None: + """ + Used for fetching the parsed values from the system. + If the value is not parsed, it will use the default values settend at: utils.configuration.default_vals.py + """ + self.logger.debug("get_values") + + if not load_dotenv(dotenv_path=self.path_config_file): + print("\n\n ERROR!: \ņ\n Unable to load the '.env' file at the path: '{self.path_config_file}'. Change it at the '.configuration.default_vals.py' file") + sys.exit() + + self.dict_values["description"] = self.__get_env__( + name_config="DESCRIPTION", + name_config_default="DEFAULT_DESCRIPTION" + ) + ### + ### DEBUG/VEBOSE + ### + self.dict_values["debug"] = self.__get_env_bool__( + name_config="DEBUG", + name_config_default="DEFAULT_DEBUG" + ) + self.dict_values["verbose"] = self.__get_env_bool__( + name_config="VERBOSE", + name_config_default="DEFAULT_VERBOSE" + ) + ### + ### LOG + ### + self.dict_values["log_file_append_mode"] = self.__get_env_bool__( + name_config="LOG_FILE_APPEND_MODE", + name_config_default="DEFAULT_LOG_FILE_APPEND_MODE" + ) + self.dict_values["log_to_file"] = self.__get_env_bool__( + name_config="LOG_TO_FILE", + name_config_default="DEFAULT_LOG_TO_FILE" + ) + self.dict_values["log_file_path"] = self.__get_env__( + name_config="LOG_FILE_PATH", + name_config_default="DEFAULT_LOG_FILE_PATH" + ) + self.dict_values["log_format"] = self.__get_env__( + name_config="LOG_FORMAT", + name_config_default="DEFAULT_LOG_FORMAT" + ) + ### + ### DB + ### + database_type = self.__get_env__( + name_config="DATABASE_TYPE", + name_config_default="DEFAULT_DATABASE_TYPE" + ) + + if database_type == "local_file": + self.dict_values["database_table_name_button_box"] = self.__get_env__( + name_config="DATABASE_TABLE_NAME_BUTTON_BOX", + name_config_default="DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX" + ) + self.dict_values["database_table_name_button_box_use"] = self.__get_env__( + name_config="DATABASE_TABLE_NAME_BUTTON_BOX_USE", + name_config_default="DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX_USE" + ) + self.dict_values["database_local_db_file_path"] = self.__get_env__( + name_config="DATABASE_LOCAL_DB_FILE_PATH", + name_config_default="DEFAULT_DATABASE_LOCAL_DB_FILE_PATH" + ) + + elif database_type == "api": + self.dict_values["database_api_url"] = self.__get_env__( + name_config="DATABASE_API_URL", + name_config_default="DEFAULT_DATABASE_API_URL" + ) + self.dict_values["database_api_token"] = self.__get_env__( + name_config="DATABASE_API_TOKEN", + name_config_default="DEFAULT_DATABASE_API_TOKEN" + ) + + else: + print(f"\n\n Unable to set the database_type from '{database_type}'\n Accepted values are: 'local_file' or 'api' \n\n") + sys.exit(1) + + self.dict_values["database_type"] = database_type + + ### + ### BOXES CONFIG + ### + self.dict_values["box_button_bounce_time"] = self.__get_env__( + name_config="BOX_BUTTON_BOUNCE_TIME", + name_config_default="DEFAULT_BOX_BUTTON_BOUNCE_TIME" + ) + self.dict_values["box_led_enabled"] = self.__get_env_bool__( + name_config="BOX_LED_ENABLED", + name_config_default="DEFAULT_BOX_LED_ENABLED" + ) + self.dict_values["box_led_always_on"] = self.__get_env_bool__( + name_config="BOX_LED_AWAYS_ON", + name_config_default="DEFAULT_BOX_LED_AWAYS_ON" + ) + self.dict_values["box_led_blink_times"] = self.__get_env__( + name_config="BOX_LED_BLINK_TIMES", + name_config_default="DEFAULT_BOX_LED_BLINK_TIMES" + ) + ### + ### BOXES USED + ### + # Set boxes_used as a list + self.dict_values["boxes_used"] = self.__get_env__( + name_config="BOXES_USED", + name_config_default="DEFAULT_BOXES_USED" + ).replace(' ','').replace("'", "").replace('"', '').split(',') + + + # import pdb; pdb.set_trace() + # self.dict_values["boxes_used"] + # os.getenv("BOXES_USED") + # load_dotenv(dotenv_path=self.path_config_file) + # # "'BUTTON_BOX_1, BUTTON_BOX_2'" + + # Adds dict_values with 'boxes' infos from 'boxes_used' list + for box_name in self.dict_values.get("boxes_used"): + + self.app_description = self.__get_env_dict__( + name_config=box_name, + name_config_default="DEFAULT_" + box_name + ) + + # Fix lower boxes names from boxes_used list + for index, item in enumerate(self.dict_values.get("boxes_used")): + self.dict_values.get("boxes_used")[index] = item.lower() diff --git a/gpio_buttons/utils/configuration/from_file_yaml.py b/gpio_buttons/utils/configuration/from_file_yaml.py new file mode 100644 index 0000000..0b37c9f --- /dev/null +++ b/gpio_buttons/utils/configuration/from_file_yaml.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" + utils.configuration.from_file_yaml.py file for the c3 buttons. +""" + +### +### DEPENDENCIES +### +import sys +import json + +try: + import pyaml_env +except ImportError as e: + print("Unable to import 'pyaml_env'") + print("Please install it with 'pip(3) install pyaml_env --user'") + sys.exit(1) + +### LOCAL +try: + from .from_file import ConfigurationFromFile +except ImportError as e: + print("Unable to import 'ConfigurationFromFile' from 'utils.configuration.from_file.py' file at the 'utils.configuration.from_file_yaml.py' file" ) + sys.exit(1) + + +class ConfigurationFromYamlFile(ConfigurationFromFile): + + def __init__(self, default_values: dict) -> None: + + ### Init super + super().__init__(name=__name__, default_values=default_values) + + self.path_config_file = self.default_values.get('DEFAULT_PATH_CONFIG_YML') + self.dict_values = self.get_values() + + def __get_env_dict__(self, parsed: dict) -> dict: + + self.logger.debug(f"__get_env_dict__ - {parsed}") + + parsed['boxes_used'] = parsed.get("boxes_used").replace(' ',''). split(',') + parsed['boxes'] = {} + + for box_name in parsed.get("boxes_used"): + + box_info = parsed.get(box_name) + if box_info is None: + print(f"\n\n Error getting the configuration from the box '{box_name}' from the 'config.yml' file. \n If you want to use this box, configure it also.\n\n") + sys.exit(1) + + if isinstance(box_info, dict): + ret_dict = box_info + + else: + try: + ret_dict = json.loads(box_info) + + except Exception as e: + print(f"\n\n Unable to parse the dictionary from the '{box_name}' with value: '{box_info}' \n| The exception is: '{e}' \n\n") + sys.exit(1) + + parsed["boxes"][box_name.lower()] = ret_dict + parsed.pop(box_name) + + return parsed + + def get_values(self) -> dict: + + self.logger.debug(f"get_values") + + try: + parsed_all = pyaml_env.parse_config(self.path_config_file) + + except Exception as e: + print(f"\n\n Error reading the 'yaml' configurtion file. \n Exception: {e}") + sys.exit(1) + + default_parsed = parsed_all.get('default') + if default_parsed is None: + print(f"\n\n Error getting the configuration from the 'config.yml' file. \n Exception: {e}") + sys.exit(1) + + return self.__get_env_dict__(parsed=default_parsed) diff --git a/gpio_buttons/utils/database/__init__.py b/gpio_buttons/utils/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gpio_buttons/utils/database/app_database.py b/gpio_buttons/utils/database/app_database.py new file mode 100644 index 0000000..d7047e6 --- /dev/null +++ b/gpio_buttons/utils/database/app_database.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" + utils.database.app_database.py file for the c3 printing system. +""" + +import sys +import logging + +### LOCAL +try: + from utils.configuration.app_logger import set_logger_from_config +except ImportError as e: + print("Unable to import 'setup_logger' from 'utils.configuration.app_logger.py' file at 'utils.database.app_database.py' file") + sys.exit(1) + +try: + from .from_local_file import DataBaseLocalSqLite +except ImportError as e: + print("Unable to import 'DataBaseUsed' from 'utils.database.from_local_file.py' file at 'utils.database.app_database.py' file") + sys.exit(1) + +try: + from .from_api import DataBaseApi +except ImportError as e: + print("Unable to import 'DataBaseApi' from 'utils.database.from_api.py' file at 'utils.database.app_database.py' file") + sys.exit(1) + + +class AppDatabase(): + """ + Class for setting the configuration of the app. + """ + + def __init__(self, configuration) -> None: + """ + Function for initialiazing the AppConfiguration class. + Sets the class variables. + + Returns + ------- + None + """ + + self.configuration = configuration + + self.logger = set_logger_from_config(name=__name__, config=self.configuration) + + self.set_database() + + def set_database(self) -> None: + + self.logger.debug("set_database") + + if self.configuration.database_type == "local_file": + self.logger.info("---> Using local SQLite3 file as Database") + self.db = DataBaseLocalSqLite(configuration=self.configuration) + + elif self.configuration.database_type == "api": + self.logger.info("---> Using API calls as Database") + self.db = DataBaseApi(configuration=self.configuration) + + else: + print(f"\n\n ERROR! ---> Unable to set database from the parsed 'database_type' with value: {self.configuration.database_type}") + sys.exit(1) diff --git a/gpio_buttons/utils/database/from_api.py b/gpio_buttons/utils/database/from_api.py new file mode 100644 index 0000000..3d9839e --- /dev/null +++ b/gpio_buttons/utils/database/from_api.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" + utils.database.from_api.py file for the c3 buttons. +""" + + +### +### DEPENDENCIES +### +import sys +import requests + +### LOCAL +try: + from .from_database import DataBaseUsed +except ImportError as e: + print("Unable to import 'DataBaseUsed' from 'utils.database.from_database.py' file at 'utils.database.from_api.py' file" ) + sys.exit(1) + + +class DataBaseApi(DataBaseUsed): + """ + TODO: Documentation + """ + + def __init__(self, configuration: dict) -> None: + """ + TODO: Documentation + """ + + ### Init super + super().__init__(name=__name__, configuration=configuration) + + self.__check_token__() + + def __check_token__(self) -> None: + """ + TODO: Documentation + """ + + self.logger.debug("__check_token__") + + response = self.get(endpoint="auth/test/closed") + if response is None: + self.logger.error("__check_token__ - Token is NOT working...") + print("\nERROR ---> Initial Token is not working.\n You should Generate a new one and set in the configuration...\n\n") + sys.exit() + + else: + if response.get('error'): + print("\nERROR ---> Some error happend trying to get the initial token.\n ") + sys.exit() + + else: + self.logger.info("__check_token__ - Token is working...") + + def __get_token_headers__(self) -> dict: + + return { + "Content-Type": "application/json; charset=utf-8", + "Authorization": f"Bearer {self.configuration.database_api_token}" + } + + def get(self, endpoint, params=None) -> (dict | None): + + self.logger.debug(f"get - endpoint: '{endpoint}' | params: '{params}'") + + url = f"{self.configuration.database_api_url}/{endpoint}" + try: + response = requests.get( + url, + headers=self.__get_token_headers__(), + params=params + ) + response.raise_for_status() + return response.json() + + except requests.RequestException as e: + self.logger.error(f"GET request failed with exception: {e}") + return None + + def post(self, endpoint, json=None, data=None) -> (dict | None): + + self.logger.debug(f"post - endpoint: '{endpoint}' | json: '{json}' | data: '{data}'") + + url = f"{self.configuration.database_api_url}/{endpoint}" + try: + response = requests.post( + url, + json=json, + data=data, + headers=self.__get_token_headers__() + ) + response.raise_for_status() + return response.json() + + except requests.RequestException as e: + self.logger.error(f"POST request failed with exception: {e}") + return None + + def add_button_box_usage(self, box_name: str): + + self.logger.debug(f"add_button_box_usage - box_name: '{box_name}'") + + post_data = { + "name": box_name + } + resp = self.post(endpoint="api/box/use", json=post_data) + if resp.get('error'): + self.logger.error(f"Error adding button usage with response: {resp}") diff --git a/gpio_buttons/utils/database/from_database.py b/gpio_buttons/utils/database/from_database.py new file mode 100644 index 0000000..447d934 --- /dev/null +++ b/gpio_buttons/utils/database/from_database.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +""" + utils.configuration.from_file.py file for the c3 buttons. + This is the super class used from the other classes that fetches configurations from a file. +""" + +import sys +from abc import ABC, abstractmethod + +try: + from utils.configuration.app_logger import set_logger_from_config +except ImportError as e: + print("Unable to import 'set_logger_from_dict' from 'utils.configuration.app_logger.py' file at 'utils.database.from_api.py' file" ) + sys.exit(1) + +class DataBaseUsed(ABC): + """ TODO + """ + + def __init__(self, name: str, configuration: dict) -> None: + + self.configuration = configuration + # self.logger = set_logger_from_config(name=__name__, config=self.configuration) + self.logger = set_logger_from_config(name=name, config=self.configuration) + self.debug = configuration.debug + self.conn = None + self.cursor = None + + @abstractmethod + def add_button_box_usage(self, box_name: str): + pass diff --git a/gpio_buttons/utils/database/from_local_file.py b/gpio_buttons/utils/database/from_local_file.py new file mode 100644 index 0000000..7017e52 --- /dev/null +++ b/gpio_buttons/utils/database/from_local_file.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" + utils.database.from_local_file.py file for the c3 buttons. +""" + +### +### DEPENDENCIES +### +import sys +import sqlite3 +from datetime import datetime + +### LOCAL +try: + from .from_database import DataBaseUsed +except ImportError as e: + print("Unable to import 'DataBaseUsed' from 'utils.database.from_database.py' file at 'utils.database.from_local_file.py' file" ) + sys.exit(1) + + +class DataBaseLocalSqLite(DataBaseUsed): + """ TODO + """ + + def __init__(self, configuration: dict) -> None: + + ### Init super + super().__init__(name=__name__, configuration=configuration) + + self.file_path = self.configuration.database_local_db_file_path + + ### + ### CONNECTION + ### + def conn_create(self): + self.conn = sqlite3.connect(self.file_path) + self.cursor = self.conn.cursor() + + def conn_close(self): + + if self.conn: + self.cursor.close() + self.conn.close() + self.cursor = None + self.conn = None + + ### + ### QUERY + ### + def query_fetch_one(self, query: str) -> None: + + # self.logger.debug(f"query_fetch_one - query: {query}") + self.logger.debug("query_fetch_one") + + if self.conn is None: + self.conn_create() + + try: + resp = self.cursor.execute(query) + return resp.fetchone()[0] + + except Exception as e: + self.logger.error(f"ERROR executing query: {query} | with execption: {e}") + + def query_fetch_all(self, query: str) -> None: + + # self.logger.debug(f"query_fetch_all - query: {query}") + self.logger.debug("query_fetch_all") + + if self.conn is None: + self.conn_create() + + try: + self.cursor.execute(query) + resp = self.cursor.execute(query) + + return resp.fetchall() + + except Exception as e: + self.logger.error(f"ERROR executing query: {query} | with execption: {e}") + + finally: + self.conn_close() + + def query_exec(self, query: str) -> None: + + # self.logger.debug(f"query_exec - query: {query}") + self.logger.debug("query_exec") + + if self.conn is None: + self.conn_create() + + try: + self.cursor.execute(query) + + except Exception as e: + self.logger.error(f"ERROR executing query: {query} | with execption: {e}") + + finally: + if self.conn: + self.conn.commit() + + ### + ### TABLES + ### + def table_select_all(self, table_name: str) -> (list | None): + + self.logger.debug(f"table_select_all - table_name: {table_name}") + + if self.conn is None: + self.conn_create() + + query = "SELECT * FROM " + table_name + ";" + + return self.query_fetch_all(query=query) + + def table_get_button_box_id(self, box_name: str): + + self.logger.debug(f"table_get_button_box_id - box_name '{box_name}'") + + table_name = self.configuration.database_table_name_button_box + + query = """ + SELECT id FROM {table_name} WHERE {field_name}="{field_value}"; + """.format( + table_name=table_name, + field_name="name", + field_value=box_name + ) + + # box_id = self.query_fetch_one(query=query) + # self.conn_close() + # return box_id + + ################## + ################## + ################## + + return self.query_fetch_one(query=query) + + def add_button_box_usage(self, box_name: str): + + self.logger.debug(f"table_add_button_box_usage - box_name '{box_name}'") + + if self.conn is None: + self.conn_create() + + button_box_id = self.table_get_button_box_id(box_name=box_name) + values = (str(datetime.now()), button_box_id) + + query = """ + INSERT INTO "{table_name}" {columns_list} VALUES {values} + """.format( + table_name=self.configuration.database_table_name_button_box_use, + columns_list=("date", "box_used_id"), + values=values + ) + self.query_exec(query) + self.conn_close() diff --git a/pycodestyle.cfg b/pycodestyle.cfg new file mode 100644 index 0000000..77841ab --- /dev/null +++ b/pycodestyle.cfg @@ -0,0 +1,8 @@ +[pycodestyle] +count = False +; E266 too many leading '#' for block comment +; E401 multiple imports on one line +ignore = E266, E401 +max-line-length = 160 +; max-line-length = 81 +statistics = True \ No newline at end of file -- GitLab