Von Flask zu Django -> Wert an Webseite zurück geben

Django, Flask, Bottle, WSGI, CGI…
Benutzeravatar
Dennis89
User
Beiträge: 1648
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,
Wenn du whitenoise + static aktivierst, kann Django auch auch (relativ performant) andere Dinge ausliefern.
Dann muss ich allerdings beim laden der Seite, die Informationen, die `whitenoise` liefert, an den Browser senden oder mit Django rendern. Ich denke ich starte mal ein neues Test-Projekt und schau mir meine verschiedenen Gedankengänge in Verbindung mit euren Hinweisen an.
Und gunicorn + django lässt sich super in einen Container packen.
Schön dass du das ansprichst. Das ist ja immer wieder ein Thema bei mir, in dem ich mich versuche tiefer einzuarbeiten.
Auf dem betreffenden Server (der ist nur lokal und nicht aus dem Internet erreichbar) läuft eine Webapplikation, die ist standardmäßig auf Port 80 und einer Subdomain erreichbar und nutzt Apache.
Ich dachte mir, wenn Apache läuft, dann lasse ich meine Seite auf einem anderen Port mit eigener Subdomain ausliefern und konfiguriere in Apache einen ReverseProxy. Sprich ich rufe `domain/meineWebseite` auf und Apache liefert dann meine Seite aus, die auf beispielsweise Port 190 konfiguriert wurde. Hat auf dem Raspberry auch funktioniert. In meiner `venv` läuft Gunicorn.

Den Gedanke von Container mag ich sehr. Ich habe immer im Hinterkopf, ich bin Hobby-Programmierer und wenn ich "etwas" übersehen habe, dann ist es angenehmer, wenn der Container "kaputt" geht, anstatt "der ganze Server". (übertrieben gesagt)

Wenn ich daran denke, das ich für dieses Projekt `Docker` verwende, dann wüsste ich gar nicht wo anfangen. An sich würde ich so vorgehen und mir überlegen, was der Container benötigt, damit er funktioniert. Da wird es relativ schnell kritisch. Ich benötige Python ink Gunicorn. Und nun weiß ich nicht weiter, weil ich nicht weiß wie das "ganze System" dann arbeiten soll.
In diesem Beispiel, liefert Gunicorn auch die *.html Dateien aus? Dann brauche ich nicht mehr, weil ich dann mein oben beschriebenes ReverseProxy auf den Gunicorn-Port konfigurieren soll? Ist das so richtig?

Das wurde jetzt viel Text. Ich finde Container echt interessant und hatte mir eurer Hilfe auch einen lauffähigen Container erstellt bekommen, nur ist da 0 Routine und ich starte gefühlt wieder bei 0.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
sparrow
User
Beiträge: 4611
Registriert: Freitag 17. April 2009, 10:28

Die Frage, die ich mich immer stelle: Wo schneide ich Container. Und für mich gehört normalerweise der Webserver nicht mit hinein. Das ist der Punkt, an dem die HTTPS-Verbindung terminiert. Und wenn der im Container steckt, dann ist ein Loadbalancing schwierig, weil die Verbindung bestenfalls auf dem Loadbalancer ankommt.

Wenn du den Conainer so schneidest, dass du Django darin liegt, dann musst du deine statischen Dateien irgendwo anders lassen.
Bei Hyperscalern gibt es dafür eigene Services. Du hättest also einen Terminierungspunkt (HTTPS-Entrypoint, WAF, etc.) deinen Container und einen Service für die statischen Dateien.
Ich bin kein Freund davon, das so kompliziert zu machen. Man muss auch nicht aus allem ein Kubernetes-Projekt bauen.
Aber man kann ein bisschen im Hinterkopf behalten, wie es da funktioniert und das auf seine kleinen Lösungen projizieren.


Und daraus ergibt sich auch die Antwort darauf:
Dennis89 hat geschrieben: Sonntag 4. Januar 2026, 09:04 Dann muss ich allerdings beim laden der Seite, die Informationen, die `whitenoise` liefert, an den Browser senden oder mit Django rendern.
Irgendwie unterscheidest du gerade, was der Apache selbst ausliefern soll und was er an gunicorn durchreicht. Und das machst du einfach nicht, sondern gibst alles gunicorn. Und deine .html, .css und wasauchimmer liegt in den staticfiles. Ich weiß nicht, wie das bei vue ist: Kommt da am Ende auch eine einzige Datei heraus mit webpack?
Sirius3
User
Beiträge: 18340
Registriert: Sonntag 21. Oktober 2012, 17:20

vue ist da relativ felxibel. Ich habe es vor einigen Jahren ausgewählt, weil ich meinen selbstgeschriebene Engine durch was moderneres ersetzen wollte. Da konnte ich dann Stück für Stück migrieren. Da das eine SPA mit vielen einzelnen Modulen war, gab es auch eine strickte Kopplung von Django-Modulen für's Backend und entsprechende js- und css-Dateien im jeweiligen static-Unterordner. Das ganze per `collect` gesammelt und von nginx direkt ausgeliefert.

Bei einem neuen Projekt, würde ich glaube ich Backend und Frontend strikt trennen, so dass mein Django-Projekt gar keine static-Daten mehr hat, sondern nur eine REST-Schnittstelle.
Das gibt dann natürlich ein eigenständiges TypeScript oder Javascript-Projekt, in das Du Dich auch erst einarbeiten müßtest, ist aber insgesamt etwas sauberer und flexibler.

Bei Containern ist es wichtig, dass man schon vorher ein sauberes Deployment hat, dann baut man ein wheel und der ganze Containerbau besteht daraus, ein Python-Basis-Image zu nehmen und pip install aufzurufen.
Benutzeravatar
Dennis89
User
Beiträge: 1648
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für eure Antworten, bevor ich Nachfragen stelle, will ich sicher stellen, dass ich meinen Projektaufbau richtig dargestellt habe. Daher habe ich mal es mal kopiert und alles "Überflüssige" rausgeworfen.
Die Struktur:

Code: Alles auswählen

[dennis@dennis FürForum]$ tree -L 6
.
├── LICENSE
├── MANIFEST.in
├── README.md
├── requirements.txt
└── Website
    ├── evaluation
    │   ├── apps.py
    │   ├── __init__.py
    │   ├── static
    │   │   └── evaluation
    │   │       ├── images
    │   │       │   ├── background.png
    │   │       │   └── header.jpg
    │   │       └── style.css
    │   ├── templates
    │   │   └── evaluation
    │   │       ├── index.html
    │   │       └── script
    │   │           └── evaluation.js
    │   ├── urls.py
    │   └── views.py
    ├── __init__.py
    ├── manage.py
    └── ProjectName
        ├── asgi.py
        ├── __init__.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py

10 directories, 20 files
ProjectName/urls.py:

Code: Alles auswählen

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("evaluation/", include("evaluation.urls")),
    path('admin/', admin.site.urls),
]
evaluation/urls.py:

Code: Alles auswählen

from django.urls import path
from . import views

urlpatterns = [
    path("", views.get_operation_hours, name="evaluation")
]
apps.py:

Code: Alles auswählen

from django.apps import AppConfig


class EvaluationConfig(AppConfig):
    name = 'evaluation'
views.py:

Code: Alles auswählen

from datetime import datetime as dt
from json import loads

import influxalchemy
import influxdb
from django.http import JsonResponse


class SPS(influxalchemy.Measurement):
    __measurement__ = "Sensors"




def filter_operation_hours(date_range):
    with influxdb.DataFrameClient(database="Maschine") as client:
        ...
        return hours


def get_operation_hours(request):
    if request.method == "POST":
        user_data = dict(loads(request.body.decode("UTF-8")))
        date_range = (
            dt.fromisoformat(user_data["startDate"]),
            dt.fromisoformat(user_data["endDate"]),
        )
        if _is_date_valid(date_range):
            return JsonResponse(
                {"operation_hours": filter_operation_hours(date_range)}
            )
    return JsonResponse({"operation_hours": -99})


def _is_date_valid(date_range):
    return date_range[0] <= date_range[1]
index.html:

Code: Alles auswählen

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
    <meta http-equiv='x-ua-compatible' content='ie=edge'>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  </head>

  <link rel="stylesheet" type="text/css" href="../../static/evaluation/style.css">

  <body>
    <header class>
      <img src="../../static/evaluation/images/header.jpg" class="header" >
    </header>
    <div id='app'>
      <template>
        <div class="eval">
          <label>
            Start-Datum wählen:
            <input
              type="date"
              v-model="startDate"
              required />
              <span class="valid error" v-if="!isDaterangeValid">✖</span>
              <span class="valid" v-if="isDaterangeValid">✓</span>
          </label>
        </div>
        <div class="eval">
          <label>
            End-Datum wählen:
            <input
              type="date"
              v-model="endDate"
              required />
              <span class="valid error" v-if="!isDaterangeValid">✖</span>
              <span class="valid" v-if="isDaterangeValid">✓</span>
          </label>
        </div>
        <div class="eval">
          <button class="button" @click="getOperationHours">Stundenabfrage</button>
        </div>
        <div class="eval">
            Die Maschine lief <b style="color: red">{{ operationHours }} Stunden</b> unter den gegebenen Bedingungen.
        </div>
      </template>
    </div>

    <script src="script/evaluation.js"></script>
  </body>
</html>
evaluation.js:

Code: Alles auswählen

const { createApp, ref, computed } = Vue

  createApp({
    setup() {
      let startDate = ref()
      let endDate = ref()
      let operationHours = ref( 0 )

      async function getOperationHours() {
        await fetch( 'http://localhost:8000/evaluation/', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ "startDate": startDate.value, "endDate": endDate.value })
        })
        const response  = await fetch( 'http://localhost:8000/evaluation/' );
        const hours = await response.json();
        operationHours.value = hours[ "operation_hours" ];
      }


      return {
        startDate, endDate, operationHours,
        getOperationHours,
        isDaterangeValid
      }
    }
  }).mount('#app')
Für Apache evaluation.conf:

Code: Alles auswählen

Listen 190
<VirtualHost 192.168.0.175:190>
        DocumentRoot "/opt/FürForum/Website/evaluation/templates/evaluation"

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

<VirtualHost *:80>
        ProxyPreserveHost On

        ProxyPass /subdomain http://192.168.0.175:190/
        ProxyPassReverse /subdomain http://192.168.0.175:190/

</VirtualHost>
apache.conf (nur das hinzugefügte):

Code: Alles auswählen

<Directory /opt/FürForum/Website/evaluation/templates/evaluation/>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>
Meiner Meinung nach, habe ich eine klare Trennung von Front- und Backend. Ich kann den `static`- und `templates`- Ordner an beliebigen Stellen abspeichern, das müsste nach meinem Verständnis nach Django gar nicht interessieren. Bei mir ist nur wichtig dass die zwei erwähnten Ordner im gleichen Verzeichnis liegen.
Das ist auf jeden Fall mein Stand. Die index.html, evaluation.js und views.py habe ich bearbeitet bzw. Code rausgenommen, damit man auf einen Blick die Funktion und den Ablauf erkennt.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18340
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: es macht doch gar keinen Sinn, zwei `fetch`-Reqeusts abzusetzen. Mit dem Ergebnis des POST-Requests machst Du dann gar nichts, und das Ergebnis des GET-Requests ist laut Deinem Code immer -99.
Wenn eine Methode einen Fehler zurückliefern soll, dann keinen "gültigen" Wert, wie -99, sondern einen passenden HTTP-Code.
Auch die sonstige Fehlerbehandlung fehlt komplett.
Beim Template: auch vue kennt soetwas wie v-else falls die Bedingung nicht erfüllt ist.
Die Zeile "Die Maschine lief ..." sollte wohl nur sichtbar sein, wenn auch wirklich ein Wert abgefragt worden ist.
Benutzeravatar
Dennis89
User
Beiträge: 1648
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

danke für die Anmerkungen.
Das was mir an Fehler eingefallen ist, habe ich jetzt eingebaut. Mein übliches Problem dabei ist wieder, das ich doppelten Code habe. Ich kann allerdings eine Funktion, die Daten prüft nicht mal Daten und mal Wahrheitswerte zurück geben lassen, daher so. Und weil wir schon dabei sind, zeige ich die vollständige `views.py`. `filter_operation_hours` kann ich erst am Mittwoch testen, daher ist mir auch der Fehler mit zwei mal `fetch` nicht aufgefallen.

views.py:

Code: Alles auswählen

from datetime import datetime as dt
from json import loads

import influxalchemy
import influxdb
import pandas as pd
from django.http import JsonResponse, HttpResponse


class SPS(influxalchemy.Measurement):
    __measurement__ = "Sensors"


def filter_operation_hours(date_range, pressure_range):
    with influxdb.DataFrameClient(database="Maschine") as client:
        influx = influxalchemy.InfluxAlchemy(client)
        data = (
            influx.query(SPS.BP005)
            .filter(SPS.time.between(*date_range))
            .execute()["Sensors"]
        )
        data = data[data.index != "Time"]
        data["duration"] = data.index.diff()
        data = data.drop(data["BP005"][data["BP005"] < pressure_range["min"]].index)
        data = data.drop(data["BP005"][data["BP005"] > pressure_range["max"]].index)
        return pd.to_timedelta(data["duration"]).sum() / pd.Timedelta("1 hour")


def get_operation_hours(request):
    if request.method == "POST":
        user_data = dict(loads(request.body.decode("UTF-8")))
        if not _is_incoming_data_valid(user_data):
            return HttpResponse(reason="Eingabewerte sind nicht valide", status=400)
        date_range = (
            dt.fromisoformat(user_data["startDate"]),
            dt.fromisoformat(user_data["endDate"]),
        )
        pressure_range = {
            "min": float(user_data["minPressure"]),
            "max": float(user_data["maxPressure"]),
        }
        return JsonResponse(
            {"operation_hours": filter_operation_hours(date_range, pressure_range)}
        )
    return HttpResponse(reason="Anfrage konnte nicht verarbeitet werden", status=405)


def _is_incoming_data_valid(user_data):
    try:
        date_range = (
            dt.fromisoformat(user_data["startDate"]),
            dt.fromisoformat(user_data["endDate"]),
        )
        pressure_range = {
            "min": float(user_data["minPressure"]),
            "max": float(user_data["maxPressure"]),
        }
    except (KeyError, ValueError):
        return False
    if _is_date_valid(date_range) & _is_pressure_valid(pressure_range):
        return True
    return False


def _is_pressure_valid(pressure_range):
    return pressure_range["min"] <= pressure_range["max"]


def _is_date_valid(date_range):
    return date_range[0] <= date_range[1]
Edit: Mit dem `float` bin ich mir nicht sicher. Es ist unnötig, weil ich keine Strings schicke. Wenn aber durch ein Fehler andere Daten kommen, zum Beispiel ein String und eine Zahl, dann wird das im Größenvergleich nicht abgefangen. Ich weiß nie wie weit die Überprüfung gehen soll.

Die Änderung an `evaluation.js`:

Code: Alles auswählen

const { createApp, ref, computed } = Vue

  createApp({
    setup() {
      let startDate = ref()
      let endDate = ref()
      let operationHours = ref( 0 )

      async function getOperationHours() {
        const response = await fetch( 'http://localhost:8000/evaluation/', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ "startDate": startDate.value, "endDate": endDate.value })
        })
        if ( response.status === 400 ) {
          alert("Eingabewerte entsprechen nicht den Vorgaben!")
          } else {
            const hours = await response.json();
            operationHours.value = hours[ "operation_hours" ];
          }
      }

      const isDaterangeValid = computed(() => {
        return Date.parse( endDate.value ) >= Date.parse( startDate.value )
      })

      return {
        startDate, endDate, operationHours,
        getOperationHours,
        isDaterangeValid
      }
    }
  }).mount('#app')
Bei 405 wird auf der Seite nichts ausgegeben, da das meiner Meinung nach nicht vorkommen kann, wenn man auf den Button klickt, sondern wenn man bewusst unzulässige Anfragen schickt.


Wenn ich eure vorherigen Antworten durch lese, dann gibt es von der Trennung kein wirkliches richtig und falsch. Zum einen was das Thema angeht, was gehört in einen Container und was nicht, wie auch die Trennung von Back- und Frontend. Stand jetzt mag ich den Aufbau von Back- und Frontend so wie ich es habe und würde das beibehalten, es sei den es gibt aus eurer Erfahrung heraus Bedenken.
Was sich für mich sehr gut anhört:
Bei Containern ist es wichtig, dass man schon vorher ein sauberes Deployment hat, dann baut man ein wheel und der ganze Containerbau besteht daraus, ein Python-Basis-Image zu nehmen und pip install aufzurufen.
Dazu würde ich allerdings Hilfe benötigen, mal wieder. Wie ist denn ein "sauberes Deployment" definiert? Ich hole aktuell mit `git` den aktuellen Projektstand und je nach dem was geändert wurde, muss ich eventuell irgendwelche Konfigurationsdateien anpassen. Ich denke nicht, dass das "sauber" ist. Wie läuft das in der Praxis ab?


Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
sparrow
User
Beiträge: 4611
Registriert: Freitag 17. April 2009, 10:28

Ist das if mit '&' so gewollt oder sollte das ein 'and' sein?
Dennis89 hat geschrieben: Montag 5. Januar 2026, 10:31Mit dem `float` bin ich mir nicht sicher. Es ist unnötig, weil ich keine Strings schicke. Wenn aber durch ein Fehler andere Daten kommen, zum Beispiel ein String und eine Zahl, dann wird das im Größenvergleich nicht abgefangen. Ich weiß nie wie weit die Überprüfung gehen soll.
Das solltest du auf diese Art im besten Fall gar nicht prüfen - und das ein Framework für dich machen lassen. Zum Beispiel das hier bereits erwähnte "Django Rest Framework". Denn wenn du eine Schnittstelle definierst, definierst du auch, wie Daten dort ankommen. Und wenn die einkommenden Daten dem Schema nicht ensprechen (also dort, wo eine Zahl kommen sollte ein Text kommt) lehnt der Server das mit 4xx und möglicherweise einen Hinweis auf den Fehler ab.
Und damit man das nicht selbst alles per Hand prüfen musst, definiert man ein Schema. In DRF ist das in etwa so, als würde man ein Profil bauen.
Benutzeravatar
Dennis89
User
Beiträge: 1648
Registriert: Freitag 11. Dezember 2020, 15:13

Verstehe, vielen Dank.

Habe das jetzt folgendermaßen gelöst:

Code: Alles auswählen

from json import loads

import influxalchemy
import influxdb
import pandas as pd
from django.http import JsonResponse, HttpResponse
from rest_framework.serializers import (
    Serializer,
    DateField,
    FloatField,
    ValidationError,
)


class UserInput(Serializer):
    startDate = DateField()
    endDate = DateField()
    minPressure = FloatField(min_value=0)
    maxPressure = FloatField(min_value=0)

    def validate(self, data):
        if data["startDate"] > data["endDate"]:
            raise ValidationError("Start-Datum muss vor dem End-Datum liegen.")
        if data["minPressure"] > data["maxPressure"]:
            raise ValidationError(
                "Min-Druck darf nicht größer als der max. Druck sein."
            )
        return data


class SPS(influxalchemy.Measurement):
    __measurement__ = "Sensors"


def filter_operation_hours(date_range, pressure_range):
    with influxdb.DataFrameClient(database="Maschine") as client:
        influx = influxalchemy.InfluxAlchemy(client)
        data = (
            influx.query(SPS.BP005)
            .filter(SPS.time.between(*date_range))
            .execute()["Sensors"]
        )
        data = data[data.index != "Time"]
        data["duration"] = data.index.diff()
        data = data.drop(data["BP005"][data["BP005"] < pressure_range["min"]].index)
        data = data.drop(data["BP005"][data["BP005"] > pressure_range["max"]].index)
        return pd.to_timedelta(data["duration"]).sum() / pd.Timedelta("1 hour")


def get_operation_hours(request):
    if request.method == "POST":
        user_data = UserInput(data=dict(loads(request.body.decode("UTF-8"))))
        if not user_data.is_valid():
            return HttpResponse(reason="Eingabewerte sind nicht valide", status=400)
        user_data = user_data.validated_data
        date_range = (
            user_data["startDate"],
            user_data["endDate"],
        )
        pressure_range = {
            "min": user_data["minPressure"],
            "max": user_data["maxPressure"],
        }
        return JsonResponse(
            {"operation_hours": filter_operation_hours(date_range, pressure_range)}
        )
    return HttpResponse(reason="Anfrage konnte nicht verarbeitet werden", status=405)
Gefällt mir eigentlich sehr gut. Ich hatte erst Sorge, dass der Vergleich von `startDate` und `endDate` in `validate` nicht richtig funktioniert, weil ich dachte der nimmt die Rohdaten, aber es scheint so, als würde die Methode erst ausgeführt werden, wenn die Eingabewerte den Feldern entsprechen.

Falls das soweit passt, dann wäre jetzt das Deployment fällig?

Grüße und nochmals Danke euch.
Dennis

Edit: Ja natürlich, das hätte ein `and` sein sollen.
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1648
Registriert: Freitag 11. Dezember 2020, 15:13

Es gibt mehrere Arten von wheels. Ich arbeite an dem Projekt aktuell auf Fedora, werde aber auch auf Ubuntu daran arbeiten und auf dem Server läuft auch ein Ubuntu. Soll ich dann ein `many linux` - wheel bauen? So wie ich das verstanden habe, ist damit gewährleistet, dass das mit den "meisten" Linux-Distributionen kompatibel ist.
Um das zu bauen, habe ich folgendes gefunden:
https://github.com/pypa/python-manylinu ... ree/master

Bin ich richtig?

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
snafu
User
Beiträge: 6910
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Das doppelte drop() finde ich komisch. Lässt sich das nicht positiv ausdrücken als ``pressure_range["min"] <= data["BP005"] <= pressure_range["max"]``?
Sirius3
User
Beiträge: 18340
Registriert: Sonntag 21. Oktober 2012, 17:20

@snafu: das wird nicht funktionieren, da `a <= b <= c` zu `a <= b and b <= c` wird, und das `and` funktioniert nicht bei Vektoren.
Was dagegen funktioniert wäre:

Code: Alles auswählen

data = data[(pressure_range["min"] <= data["BP005"]) & (data["BP005"] <= pressure_range["max"])]
Damit ist dann das komische Konstrukt mit drop(...index) weg, das ich ja schon im anderen Thread angesprochen hatte.
Was auch komisch ist, ist das `data[data.index != "Time"]` wo es ja so aussieht, als ob eine Header-Zeile in den Daten gelandet wäre. Das Problem sollte also schon beim Speichern der Daten gelöst werden.
Antworten