Labeldruck und was draus folgt

Du hast eine Idee für ein Projekt?
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Reicht fuer Stabhochsprung jetzt Mikado zu spielen
Schön wärs.
Aber, ich nutze hier so ziemlich vieles aus unterschiedlichen Tutorials das ich mir zusammenkopieren und versuche zu verstehen. In den Tutorials wird das nun leider genau so gemacht. Da taucht dann ein Kürzel c auf für cursor. Kann ich nachvollziehen. In dem Moment wo ich mich gedanklich bei einem Ablauf befinde, der da grad erst mal beim Ergründen ist, was denn dieser cursor überhaupt macht. Erst dann kann ich dem ja einen Namen geben der ausdrückt was er ausdrücken soll.
Wenn ich mir die Dokumentation anschau, steht da z.B. das

Code: Alles auswählen

import csv
with open('some.csv', newline='', encoding='utf-8') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)
Ob ich dann, wie aus dem anderen Beispiel den Ausdruck creader übernehme oder nur reader schreibe, was ist da für ein Unterschied? Was ist »f«?
Wenn ich das dann rausgekriegt hab, kann ich das ändern.
(und, wie aussagekräftig ist z.B.sowas wie " import tkinter"? Versteht man da, was damit alles gemacht werden kann? )

Gut, ich sehe ein, dass dann die Hilfe die ich krieg, eher mager sein wird, also gelobe ich Besserung.

Zu den 8 Datensätzen.
Es soll keine Historie angelegt werden.
Das war eigentlich der Grund warum ich die Datei-Variante bevorzugt hätte.
8 Datensätze die aktuell benötigt werden. Wenn nicht mehr da sind, kann es keine Fehler geben die eine falsche Auswahl erzeugen würde.
Da die Fehler ja am ehesten aus der Quell-DB kommen werden, kann und will ich gar nicht alles abfangen, was die da falsch machen können.
Wenn sie meinen dass sie ihre DB so weiterpflegen wie bisher (sorry, aber da siehts aus wie Kraut und Rüben, das seh ich als Nichtexperte für Datenbanken) können sie auf der anderen Seite nicht erwarten dass das ein Kasten der dafür da ist 8 Label auszudrucken ausgleichen kann. Wenn der Datensatz zur Hälfte leer ist? Dann gibts halt auch ein halbleeres Label oder noch besser gar keins.

Noch eine Frage zur ID die ich mit dem csv-Reader einlese.
Die ID ist im CSV-File als Text markiert. Kann ich das irgendwie auflösen dass das eine Zahl wird? (Ähnliche Frage stellt sich ja beim Datum)
So wie ich den csv-reader verstanden hab, liest der Zeilenweise die Daten ein und so kommen sie in die DB.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wie aussagekraeftig ist denn "from tkinter import *"? Was versteht man denn da besser? Wenn man natuerlich "from tkinter import Button" macht, sieht man, dass da der Button drin ist. Aber damit man das so importieren konnte, musste man ja mal wissen, dass der da drin ist. Wenn man also nicht sinnlos 300 Namen aus tkinter alle einzeln importiert, muss man wohl eh zur Doku greifen.

Der csv-reader interpretiert von alleine nichts. Sowas musst du nachgelagert selbst machen, oder eine maechtigere Bibliothek wie pandas benutzen.
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

oder eine maechtigere Bibliothek wie pandas benutzen.
Okay, pandas sieht gut aus, allerdings "vermisse" ich das commit. Wenn ich die Daten hier mit replace austausche, was mir ja passt, habe ich keine Möglichkeit das zurückzurollen? Oder doch?
Edit:
Ich meine, wenn ich das so mache:

Code: Alles auswählen

    try:
        datensaetze = pan.read_csv(CSV_DATEI, quotechar='"', sep=';', encoding="utf-8", decimal=",", dtype={"ID":int, "E_St":int, "ISPT_Nr":str})        
        datensaetze.to_sql('knopfdaten', conn, if_exists='replace', index=False)
Dann sollte je eigentlich zweimal abgefangen werden. Einmal durch das try: und das zweite mal durch das if_exists, oder sehe ich das falsch?
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Ich glaube das mit der Datenbank gefällt mir.
Kann ich alles drin unterbringen was ich brauche.
Das extrahieren muss ich noch finden und dann kommt das Schwerste...
Fehlerbehandlung.
Da das meiste in der Datenbank passieren kann, hab ich da jetzt noch gar keinen Plan. Find zwar viel über pandas aber die haben alle kein Beispiel der Ausnahmebehandlung mit dabei gehabt.
Muss noch tiefer graben...
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Also...
Hab jetzt mal das Teil das mir vorher die Dateien hin und her geschubst hat umgebaut, dass es aus der csv-Datei in eine DB schreibt.
Hab dabei versucht den Fehler abzufangen, wenn die Datei nicht alle Spalten und zu wenige Datensätze hat.
Dazu habe ich die Datei zuerst in eine DB im Speicher erzeugt und alle Spalten die es braucht abgefragt (das muss ich noch zusehen, ob die von denen wirklich alle gebraucht werden...)
Nur wenn das keinen Fehler auswirft und es 8 Datensätze sind, schreibt es die neuen Daten in die Ziel-DB.
Diesmal habe ich versucht, die Namen so zu wählen, dass man durchblickt was jetzt was tut.
Mit dem Überprüfen bin ich jetzt noch nicht zufrieden. Das geht sicher einfacher, aber da hab ich bisher noch nichts gefunden...

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import time
from datetime import datetime as DateTime
from pathlib import Path
import pandas as pan
import pyudev
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError


PFAD = Path.home() / ".DruckData"
MEDIA_PFAD = Path("/media/earl/")
PFAD = Path.home() / ".DruckData"
DATABASE = PFAD / "sqlite/db/config8.db"
CONFIG_ON_STICK = Path("TB_Ausgabe_8iii.txt")
CSV_DATEI_TO_STICK = Path("gedruckte_nummern.csv")
SQL_OUT = "select ID, E_St, ISPT_Nr, Aktuell_Nr, Barcode from knopfdaten;"
SQL_CHECK =""" SELECT
               ID,
               E_ST ,
               ZEILE3 ,
               ZEILE5 ,
               ZEILE6 ,
               ZEILE7 ,
               ZEILEX ,
               "AUS _DATEI" ,
               ISPT_NR ,
               HALLENPOS ,
               LETZTERDRUCK ,
               AKTUELL_NR ,
               BARCODE ,
               LZ_FARBE ,
               BEMERKUNG ,
               USER ,
               LETZTEÄNDERUNG 
               FROM CHECKTABELLE;"""



def warte_auf_usb_stick(udev_context):
    monitor = pyudev.Monitor.from_netlink(udev_context)
    monitor.filter_by("block")
    for device in iter(monitor.poll, None):
        print(device.action)
        if "ID_FS_TYPE" in device and device.action == "add":
            name_of_stick = Path(device.get("ID_FS_LABEL"))
            print(device.action, name_of_stick)
            time.sleep(2)
            return name_of_stick

    raise AssertionError("unreachable code")


def anzahl_drucke_dokumentieren(conn, name_of_stick, dateiname_roh, timestamp):

    dateiname_ziel = MEDIA_PFAD \
                    /name_of_stick \
                    /dateiname_roh.with_name(\
                    f"{dateiname_roh.stem}_{timestamp:%Y-%m-%d_%H_%M}.csv"
                    )

    datfram = pan.read_sql(SQL_OUT, conn)
    datfram.to_csv(dateiname_ziel, sep=";", decimal=",", index=False)    
    


def config_arbeitsdb(temp, conn, name_of_stick):
    
    config_datei = MEDIA_PFAD / name_of_stick / CONFIG_ON_STICK
    datensaetze = pan.read_csv(
        config_datei,
        na_values= "x",
        quotechar='"',
        sep=';',
        encoding="utf-8",
        decimal=",",
        dtype={
            "ID":int,
            "E_St":int,
            "ISPT_Nr":str,
            "Barcode":str
            }
        )        
    datensaetze.to_sql('checktabelle', con=temp, if_exists='replace', index=False)
    
    result = temp.execute(SQL_CHECK).fetchall()
    print(result)
    print(len(result))
    if len(result) != 8:
        print("es waren keine 8 Datensätze")
    else:
        datensaetze.to_sql('knopfdaten', con=conn, if_exists='replace', index=False)
        


def auswerfen(name_of_stick):
    subprocess.run(
        ["umount", MEDIA_PFAD / name_of_stick],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )


def main():
    udev_context = pyudev.Context()
    
    while True:
        name_of_stick = warte_auf_usb_stick(udev_context)
        timestamp = DateTime.now()
        
        try:
            temp_engine = create_engine('sqlite://', echo=False, encoding='utf-8')
            ziel_conn = create_engine(f"sqlite:///{DATABASE}", echo=False,  encoding='utf-8')    
            config_arbeitsdb(temp_engine, ziel_conn, name_of_stick)
            anzahl_drucke_dokumentieren(ziel_conn, name_of_stick, CSV_DATEI_TO_STICK, timestamp)
            
            try:
                auswerfen(name_of_stick)
            except subprocess.CalledProcessError as error:
                print(
                    f"Fehler {error.returncode} beim Auswerfen: {error.stdout}"
                )
            except SQLAlchemyError as error:
                fehler = str(error.__dict__['orig'])
                print(fehler)    
    
            
        except OSError as error:
            print("Fehler beim kopieren:", error)
            
            try:
                auswerfen(name_of_stick)
            except subprocess.CalledProcessError as error:
                print(
                    f"Fehler {error.returncode} beim Auswerfen: {error.stdout}"
                )


if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@theoS: Pandas wird üblicherweise als `pd` abgekürzt beim importieren. Das finde ich für diese Aufgabe eine ziemlich heftige Abhängigkeit.

Den \ zum fortsetzen von Zeilen benutzt man sehr selten. Das löst man üblicherweise durch Klammersetzung:

Code: Alles auswählen

    dateiname_ziel = MEDIA_PFAD \
                    /name_of_stick \
                    /dateiname_roh.with_name(\
                    f"{dateiname_roh.stem}_{timestamp:%Y-%m-%d_%H_%M}.csv"
                    )
    # <=>
    
    dateiname_ziel = (
        MEDIA_PFAD
        / name_of_stick
        / dateiname_roh.with_name(
            f"{dateiname_roh.stem}_{timestamp:%Y-%m-%d_%H_%M}.csv"
        )
    )
Der \ im Aufruf von `with_name()` ist wegen der Klammern auch im Original schon überflüssig. Der \ ist unschön weil der wirklich als allerletztes in einer Zeile stehen muss. Wenn da ein Leerzeichen folgt, ist das schon ein Syntaxfehler. Einen Kommentar kann man da auch nicht mehr nach schreiben.

Die einzige Stelle wo ich noch \ einsetze sind Konstanten mit mehrzeiligen Zeichenkettenliteralen nach dem öffnenden """, damit die erste Zeile von so einer Konstanten sich nicht von den anderen absetzt oder gar übersehen wird:

Code: Alles auswählen

CONSTANT = """first line
second line
third line
"""

# <=>

CONSTANT = """\
first line
second line
third line
"""
Das mit der SQLite-DB im speicher verstehe ich nicht wirklich. Du speicherst da den Inhalt der CSV-Datei rein, um ihn dann gleich wieder auszulesen, und das nur um zu prüfen, dass es 8 Datensätze sind? Warum nicht einfach beim eingelesenen Dataframe schauen wie viele Zeilen der hat?

Ein `Engine`-Objekt ist etwas langlebiges das man in einem Programm normalerweise nur *einmal* erzeugt und nicht für jede Abfrage ein neues.

Das ``except SQLAlchemyError`` bezieht sich auf das ``try`` in dem der Stick ausgeworfen wird. Diese Funktion macht überhaupt nichts mit Datenbanken, da kann diese Ausnahme nicht kommen. Das gehört also zum anderen ``try``. In der Behandlung der Ausnahme steht dieser Ausdruck: ``error.__dict__["orig"]`` — warum greifst Du da auf das ”magische” und nicht garantierte `__dict__` zu, statt einfach auf das Attribut → ``error.orig``? Wobei das in beiden Fällen falsch ist weil allgemeine `SQLAlchemyError`-Objekte so ein Attribut gar nicht haben. Von den ganzen davon abgeleiteten Ausnahmen hat nur `StatementError` so ein Attribut.

Und egal zu welchem ``try`` man das schreibt, im Falle eines SQLAlchemyError wird der Stick nicht ausgeworfen. Ich vermute mal das ist ein Fehler.

Statt den Code zum Auswerfen jetzt auch noch in diesem ``except``-Zweig zu kopieren: Wenn der Stick grundsätzlich ausgeworfen werden soll, was er dann ja effectiv wird, dann gehört der Code dafür *einmal* hinter die ganzen anderen Aktionen in der Schleife.

Den Zeitstempel braucht man jetzt, wo es nur noch eine Datei gibt, nicht mehr im Hauptprogramm generieren. Das war vorher ja nur nötig, damit beide Dateien den genau gleichen Zeitstempel im Namen haben, damit man sieht welche Dateien zusammengehören.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import time
from datetime import datetime as DateTime
from pathlib import Path

import pandas as pd
import pyudev
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError

PFAD = Path.home() / ".DruckData"
MEDIA_PFAD = Path("/media/earl/")
PFAD = Path.home() / ".DruckData"
DATABASE = PFAD / "sqlite/db/config8.db"
CONFIG_ON_STICK = Path("TB_Ausgabe_8iii.txt")
CSV_DATEI_TO_STICK = Path("gedruckte_nummern.csv")
SQL_OUT = "SELECT ID, E_St, ISPT_Nr, Aktuell_Nr, Barcode FROM knopfdaten"


def warte_auf_usb_stick(udev_context):
    monitor = pyudev.Monitor.from_netlink(udev_context)
    monitor.filter_by("block")
    for device in iter(monitor.poll, None):
        print(device.action)
        if "ID_FS_TYPE" in device and device.action == "add":
            name_of_stick = Path(device.get("ID_FS_LABEL"))
            print(device.action, name_of_stick)
            time.sleep(2)
            return name_of_stick

    raise AssertionError("unreachable code")


def anzahl_drucke_dokumentieren(connection, name_of_stick, dateiname_roh):
    dateiname_ziel = (
        MEDIA_PFAD
        / name_of_stick
        / dateiname_roh.with_name(
            f"{dateiname_roh.stem}_{DateTime.now():%Y-%m-%d_%H_%M}.csv"
        )
    )
    pd.read_sql(SQL_OUT, connection).to_csv(
        dateiname_ziel, sep=";", decimal=",", index=False
    )


def config_arbeitsdb(connection, name_of_stick):
    config_datei = MEDIA_PFAD / name_of_stick / CONFIG_ON_STICK
    #
    # TODO Auf die Spalten einschränken die tatsächlich benötigt werden.
    #
    datensaetze = pd.read_csv(
        config_datei,
        na_values="x",
        quotechar='"',
        sep=";",
        encoding="utf-8",
        decimal=",",
        dtype={"ID": int, "E_St": int, "ISPT_Nr": str, "Barcode": str},
    )
    if len(datensaetze) != 8:
        print("es waren keine 8 Datensätze")
    else:
        datensaetze.to_sql(
            "knopfdaten", connection, if_exists="replace", index=False
        )


def auswerfen(name_of_stick):
    subprocess.run(
        ["umount", MEDIA_PFAD / name_of_stick],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )


def main():
    udev_context = pyudev.Context()
    db_engine = create_engine(f"sqlite:///{DATABASE}", encoding="utf-8")
    while True:
        name_of_stick = warte_auf_usb_stick(udev_context)
        try:
            config_arbeitsdb(db_engine, name_of_stick)
            anzahl_drucke_dokumentieren(
                db_engine, name_of_stick, CSV_DATEI_TO_STICK
            )
        except SQLAlchemyError as error:
            print(error)
        except OSError as error:
            print("Fehler beim kopieren:", error)

        try:
            auswerfen(name_of_stick)
        except subprocess.CalledProcessError as error:
            print(f"Fehler {error.returncode} beim Auswerfen: {error.stdout}")


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

@blackjack. Grad erst eine Schnellfrage vorneweg. Ich habe zwar dadurch dass ich die csv mit einem Excel Makro direkt aus der Access DB ziehe schon ein paar Möglichkeiten sie zu beeinflussen aber nicht alles.
Pandas macht ein Autocommit, d.h. wenn ich mit repace arbeite möchte ich zumindest sichergehen dass noch alle nötigen Spalten da sind. Das ist die Abfrage die ich auf die temp-Tabelle laufen lasse. Die überprüft ja nicht nur ob das 8 Datensätze sind.
Wenn ich das aufs Original mache ist's zu spät.
Weißt du da was besseres?
Mir kommt das nämlich schon selbst extrem umständlich vor.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@theoS: Steht im Grund schon in dem TODO-Kommentar: Man kann `read_csv()` angeben welche Spalten man aus der Datei lesen will. Das sollte dann eine Ausnahme auslösen wenn eine Spalte die man angibt, gar nicht in der Datei enthalten ist.

Aber auch wenn man einfach die gesamte Datei in einen Dataframe einliest, kann man ja einfach schauen ob da dann alle erwarteten Spaltennamen vorhanden sind.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Ok, das hat hingehauen mit den Spalten, musste dann noch ein except ValueError hinzufügen, dann bekomme ich auch da ein lesbare Meldung wenn eine Spalte die ich angegeben habe fehlt.
Danke!
Den \ zum fortsetzen von Zeilen benutzt man sehr selten. Das löst man üblicherweise durch Klammersetzung:
Das mit den Klammern hat bei mir aus irgendeinem Grund nicht geklappt. Vermute mal das war der Einrückungstrick. Hab dann im Internet davon gelesen dass man das mit dem \ macht. Ich übe das noch mal mit Klammern 8)
Und egal zu welchem ``try`` man das schreibt, im Falle eines SQLAlchemyError wird der Stick nicht ausgeworfen.
Kann ich so nicht bestätigen. Der Stick wurde rausgeworfen und es gab auch eine Fehlermeldung wenn ich die Abfrage laufen hab lassen die mir die Spaltenüberschriften ausgibt.
Das war, wie gesagt eine Verlegenheitslösung und zur Fehlerbehandlung von pandas hab ich nicht viel mehr gefunden als dieses merkwürdige Konstrukt. Es hat getan, aber deine Lösung ist deutlich eleganter.
Danke dafür noch mal extra!


Nächster Plan ist jetzt das und auch die DB mit dem "Knofpdrückteil" zu verbinden.
Da habe ich aber auch noch was vor, denn in der DB sind ja die Spalten für die letzte Nummer mit drin und der damit zusammengesetzte Barcode. Der muss immer die ISPT-Nummer die mal da ist, mal nicht, vorneweg haben. und - die Zahl soll dann bei 9999 aufhören und wieder von vorne anfangen...
(sonst hat der Barcode irgendwann mal keinen Platz mehr auf dem Label...)
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Nun, ich hab das jetzt mit sqlite3 gelöst und bin mir in 2 Sachen noch nicht so ganz sicher.
Das eine sqlite3, denn ich nutze ja im USB-Teil schon pandas, das geht mit dem sicher auch, zum anderen wie ich das Hochzählen löse. Ob in Python oder wie jetzt in SQL direkt in der DB.

Bei der ganzen Sache ist sind mir noch 2 Sachen aufgefallen, die ich auch noch in die ToDo-Liste aufnehmen muss: Ein Pop-Up für Fehlermeldungen und, was vielleicht nicht ganz unwichtig ist, eine Möglichkeit, den Raspi am Tagesende korrekt runterzufahren.
Dazu die konkrete Frage: Kann ich, da ich ja auf dem Pi einen Touchscreen habe, das mit den vorhandenen Buttons auch erreichen, dass man den Rechner runterfährt wenn man 2 bestimmte Knöpfe gleichzeitig drückt?

Hier mal der Code mit den Knöpfen.

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf8 -*-

import sqlite3
from sqlite3 import Error
from pathlib import Path
import csv
import tkinter as tk
from functools import partial


PFAD = Path.home() / ".DruckData"
BARCODE_DB_FILENAME = PFAD / "sqlite/db"
DATABASE = BARCODE_DB_FILENAME / "config8.db"
ORT = "PZ Dingsbums 14"


def save_row_as_file(count, row, first_line):
    
    text = "\n".join(
        [
            "^XA",
            "^FO15,90^GB780,0,8,^FS",
            "^FO15,250^GB780,0,8,^FS",
            "^FO15,700^GB780,0,8,^FS",
            "^FO0,0^GB600,200,2",
            "^FO15,20^GB780,785,4^FS",
            "^FO0,40^A0,50,50^FB800,1,0,C^FD",
            first_line,
            "^FS^FO0,110^A0,60,60^FB800,1,0,C^FD",
            str(row[1]),
            "^FS^FO0,190^A0,70,70^FB800,1,0,C^FD",
            str(row[2]),
            "^FS^FO0,80^BY3",
            "^BCN,170,Y,N,N",
            "^FO165,270^BY4^FD",
            str(count),
            "^FS^FO0,500^A0,60,50^FB800,,0,C^FD",
            str(row[3]),
            "^FS^FO0,580^A0,60,50^FB800,,0,C^FD",
            str(row[4]),
            "^FS^FO0,730^A0,60,60^FB800,1,0,C^FD",
            str(row[5]),
            "^FS",
            "^XZ",
            "",
        ]
    )
    PFAD.mkdir(exist_ok=True)
    (PFAD / f"testT_{row[3]}_{row[1]}.zpl").write_text(text, "utf-8")


def lade_daten(conn):
    curs2 = conn.cursor()
    config_sql = "select ID, E_St, Zeile3, Zeile5, Zeile6, Zeile7 from knopfdaten;"
    curs2.execute(config_sql)
    configdaten = curs2.fetchall()
    
    return  configdaten
 
def zaehl_ausdrucke(conn, ID):   
    print(ID)
    curs2 = conn.cursor()
    
    sql_text_hoch =f""" update knopfdaten set aktuell_nr = 
       (aktuell_nr) + 1,
       Barcode = ISPT_Nr || aktuell_nr +1
       where ID = {ID};"""    
    sql_text_barcode = f"select Barcode from knopfdaten where ID = {ID};"
    sql_text_aktnummer = f"select aktuell_nr from knopfdaten where ID = {ID};"
    
    curs2.execute(sql_text_hoch)  
    curs2.execute(sql_text_barcode) 
    barcode =  curs2.fetchone()[0]
    
    print(barcode)
    curs2.execute(sql_text_aktnummer)
    aktnummer = curs2.fetchone()[0]
    if aktnummer == 9999:
        curs2.execute(f"update knopfdaten set aktuell_nr = 0 where ID = {ID};")
        
    print(aktnummer)
    conn.commit()
    return barcode


def on_click(row, conn):
    count = zaehl_ausdrucke(conn, row[0])
    print(count)
    save_row_as_file(count, row, ORT)    

def main():
    
    BARCODE_DB_FILENAME.mkdir(parents=True, exist_ok=True)
    print(f"{BARCODE_DB_FILENAME}/config8.db")
            
    try:
        conn = sqlite3.connect(f"{DATABASE}")        
    except Error as e:
        print(e)
    configdaten = lade_daten(conn)  #lädt die Daten aus der csv nach

    root = tk.Tk()
    root.title("Auswahl der Label")
    root.config(background="#f2c618")
    button_frame = tk.Frame(root, width=1200, height=400)
    button_frame.grid(row=0, column=0, padx=10, pady=3)
    
    for index, entry in enumerate(configdaten):
        row_index, column_index = divmod(index, 4)
        tk.Button(
            button_frame,
            text="{}\n{}".format(entry[1], entry[2]),
            bg="#f2c618",
            width=15,
            height=10,
            command=partial(on_click, entry, conn),
            ).grid(row=row_index, column=column_index, padx=0, pady=0)

    root.mainloop()

        
if __name__ == '__main__':

    main()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Dass in `save_row_as_file` alles row heißt, macht die Funktion unlesbar. Niemand kann nachvollziehen, was row[3] oder row[1] ist.
Zeile 68: Dir wurde schon gesagt, dass man KEINE PARAMETER IN SQL-STATEMENTS HINEINFORMATIERT. Das gilt auch für Zeile 69, 70 und 80.
Zwei identische Abfragen für Barcode und aktuelle_nr ist unnötig, das gehört in eine Abfrage.

Zeile 65: was soll den dieser Update. Wie wird Barcode berechnet? Kannst Du das mal genau beschreiben? Ich glaube nicht, dass Dein Ausdruck das tut, was Du willst.

Wenn aber Barcode eh immer berechnet wird, braucht man den auch nicht in der Datenbank speichern.
Wenn Du dann nur 4 Ziffern haben willst, dann pass das schon beim Update an.
Warum heißt der Cursor curs2? Was soll die 2?

Die Funktion sieht vereinfacht so aus:

Code: Alles auswählen

from contextlib import closing

SQL_UPDATE_AKTUELLE_NR = """UPDATE knopfdaten
    SET aktuell_nr = (aktuell_nr + 1) % 10000 
    WHERE id = ?"""
SQL_SELECT_KNOPFDATEN = """SELECT ISPT_NR, aktuell_nr
    FROM knopfdaten WHERE id = ?"""

def zaehl_ausdrucke(connection, id):   
    with closing(connection.cursor()) as cursor:
        cursor.execute(SQL_UPDATE_AKTUELLE_NR, [id])
        cursor.execute(SQL_SELECT_KNOPFDATEN, [id])
        ispt_nr, aktuell_nr = cursor.fetchone()
        barcode = f"{ispt_nr}{aktuell_nr}"
        print(barcode, aktuell_nr)
        cursor.commit()
    return barcode
In `on_click` wird der Rückgabewert von `zaehl_ausdrucke` an count und nicht an barcode gebunden. Das ist verwirrend.

Zeile 95: ist zwar nur eine print-Ausgabe, aber Pfade setzt man trotzdem nicht per Stringformatierung zusammen.
Zeile 100: die „Fehlerbehandlung“ ist unsinnig, da Du im Fehlerfall so weitermachst, als ob nichts passiert wäre und Du in der nächsten Zeile in einen NameError läufst.
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Zeile 68: Dir wurde schon gesagt, dass man KEINE PARAMETER IN SQL-STATEMENTS HINEINFORMATIERT.
Mag sein, dass ich das zum einen falsch verstanden hab (dachte du kritisierst den Parameter [0] von Tabelle[0]) und zum anderen, du hast mir nicht gesagt, was ich stattdessen besser mache. bei meiner Google-Orgie bin ich auf die Methode gestoßen, das funktionierte. Das mit den Fragezeichen hab ich zwar bei der einen Abfrage die mir die DB erstellt so gemacht, aber auch nur halb verstanden.
Letztlich verstehe ich zwar, dass es "schöner" ist, aber das ist doch auch nur eine andere Art, einen Parameter zu übergeben?
Ist das schneller?
Wenn Du dann nur 4 Ziffern haben willst, dann pass das schon beim Update an.
Sorry, aber ich bin auch kein Datenbankprogrammierer und hab schreibenderweise nur recht wenig Ahnung von SQL. Und ich will mir der Abfrage zwei Sachen erreichen: die aktuelle Nummer soll in der DB gespeichert sein und dann im Barcode, der ja ein String ist. Das muss ein String sein, da evtl. führende Nullen da sind.
Wenn ich deine Funktion richtig verstehe, dann zählst du die Aktuelle_Nr hoch (was zur Hölle soll das %10000???) und baust dann den String wie er in die Datei soll zusammen. In der DB ist der Barcode dann aber immer der gleiche. (ich würde den ja nicht brauchen, aber jemand anders... Und, nein, es ist nicht möglich, dass der sich das selbst zusammensucht :o) )

Code: Alles auswählen

 ispt_nr, aktuell_nr = cursor.fetchone()
bei meinen Versuchen hat mir das Ding immer beide ausgegeben. dass man das so einfach vorneweg schreibt mit einem Komma dazwischen, ist mir bisher auch noch nicht untergekommen. Gut zu wissen.
:o

Hast du noch eine Idee, wie ich es bewerkstelligen könnte, dass was anderes passiert wenn ich zwei Knöpfe gleichzeitig drücke?
Danke.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Wie berechnet sich nun der Barcode? Und ich dachte, das ist ein internes System, wo sonst niemand auf die Datenbank zugreifen kann.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@theoS: Nein, das mit den ? ist nicht ”schöner”. Das ist *keine* Kosmetik! Das ist eine riesige, fette Sicherheitslücke. Und jetzt sag bitte nicht, dass das bei dem Programm kein Problem sei. Das ist *immer* ein Problem. Insbesondere weil der Fix dafür so verdammt, fucking einfach ist, gibt's da auch wirklich wenig Verständnis wenn man das trotzdem falsch macht.

Ich verstehe auch mal wieder dieses gegoogle nicht. Das `sqlite3`-Modul ist in der Standardbibliothek dokumentiert. Das ist die erste Anlaufstelle. Da sind viele Beispiele. Und auch eines das man keine Werte selbst in die SQL-Zeichenkette hineinformatiert und wie man es richtig macht. Zitat: „Never do this -- insecure!“ und „Do this instead“, jeweils mit Code.

``… % 10000`` rechnet Modulo 10.000, also den Rest einer Division durch 10.000. Oder mit anderen Worten eine Zahl bei der nur die letzten vier Ziffern übrig bleiben:

Code: Alles auswählen

In [474]: 12345678 % 10_000                                                     
Out[474]: 5678
Das mit der Mehrfachzuweisung wird oft (fälschlicherweise) unter dem Begriff „tuple unpacking“ geführt. Das funktioniert aber nicht nur mit Tupeln sondern mit allen iterierbaren Objekten. Und neben den einfachen Zuweisungen wo genau so viele Elemente in dem iterierbaren Objekt sein müssen, wie Namen links von der Zuweisung stehen, gibt es mittlerweile auch noch das was in PEP3132 beschrieben ist.

Das normale „unpacking“ wird ab und an im Tutorial in der Python-Dokumentation verwendet. Und das geht überall wo eine Zuweisung statt findet, also auch zwischen ``for`` und ``in``. Da gelten die gleichen Regeln wie bei der Zuweisung mit ``=``.

Da Du bei dem Import/Export-Programm bereits SQLAlchemy als Abhängigkeit hast, würde ich hier gar kein SQL selbst schreiben, sondern auch SQLAlchemy verwenden. Entweder als reinen SQL-Ersatz, aber wahrscheinlich sogar das ORM.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Wie berechnet sich nun der Barcode? Und ich dachte, das ist ein internes System, wo sonst niemand auf die Datenbank zugreifen kann
Der Barcode wird aus der ispt Nummer, das ist ein String u.u. mit führenden Nullen oder gar nicht da, und der aktuellen Nummer zusammengesetzt werden.
Dass das dann von irgendwem ausgewertet werden kann wird der Barcode und die aktuelle Nummer jeweils in einer eigenen Spalte gespeichert.
Brauche also den Barcode wieder hochgeladen und das so, dass unterwegs die führenden Nullen nicht verschüttet gehen. Oder Konflikte auftreten wegen nicht passenden Datentypen.

@__blackjack__ danke für die Erklärung.
Ich google deshalb, weil ich die Erklärung in der Dokumentation halt immer nur halb verstehe. Und wenn das was ist was öfter gefragt wird, finde ich dazu mehr Beispiele wo dann eines vielleicht dabei ist das ich verstehe.
Für mich ist das mit meinem eingerosteten Englischkenntnissen (sofern vorhanden) nicht so einfach damit dann auch noch zwischen Python, SQL und ZPL zu wechseln.
Bin ja kein Programmierer in meinem Brotberuf. Vermutlich wird dieses Projekt auch neben meinem ersten auch das letzte sein das ich mit Python mach.

Das mit den? hab ich jetzt verstanden. Betrachte also das andere als eine Übung für mich mit Parametern und Strings zu arbeiten.

Sicherheitsbedenken habe ich bei dem Programm tatsächlich keine, aber wenn man das so einfach von Anfang an vermeiden kann, ist das ja Mittel der Wahl.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du bestätigst damit nur das Problem, dass ich von Anfang an gesehen habe, zwei unabhängige Datenbanken synchron zu halten:
Von access wird ein Dings mit einer ispt-Nr und der Anzahl 5 auf den Raspi geladen und dort drei mal ein Barcode gedruckt (xxx6, xxx7 und xxx8). Der neue USB-Stick kommt, die Anzahl 8 wird geschrieben, aber da Access noch nicht weiss, dass gedruckt wurde, wird wieder 5 in Deine interne Datenbank geladen und munter nochmal xxx6 und xxx7 gedruckt.

Die Berechnung des Barcodes ist falsch. In deinem SQL-Ausdruck ist die +1 nicht geklammert. Außerdem wächst die Stellenanzahl des Barcodes mit der von Anzahl.
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Du bestätigst damit nur das Problem, dass ich von Anfang an gesehen habe, zwei unabhängige Datenbanken synchron zu halten:
Das war von Anfang an berechtigt zu fragen.
Wahrscheinlich wird sich dann der Barcode als Zierde herausstellen, weil ihn so keiner richtig auswerten kann.
Dazu kommt, dass Labels auch von Access selbst gedruckt werden, wenn viele auf einmal gebraucht werden. Das wird sicher spaßig, das auszuwerten. Aber nicht mein Problem.
Ich mach das Ding und der Rest ist deren Problem.
Die Berechnung des Barcodes ist falsch. In deinem SQL-Ausdruck ist die +1 nicht geklammert. Außerdem wächst die Stellenanzahl des Barcodes mit der von Anzahl.
Danke. Hatte die Klammer schon mal in einer vorigen Version drin, warum ich dir rausgemacht hab?
Weiß es nicht.
Kann mit deiner Version ja nicht passieren.
Muss den string halt dann noch einfach hochschreiben auf die DB.
Danke dafür.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

theoS hat geschrieben: Freitag 10. April 2020, 11:00 Aber nicht mein Problem.
Ich mach das Ding und der Rest ist deren Problem.
Dann bin ich hier raus.
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Naja, dass denen dann einfällt, dass das schön wäre wenn das synchronisiert läuft, dann bring ich denen auch die Netzlösung, die aber aus genannten Gründen nicht geht.
Wenn sie das dann auch über den Stick austauschen wollen, müssen sie erst mal in ihrer Datenbank aufräumen.
Solange das nicht ist, mache ich das so. Vielleicht setzte ich noch eine Konstante in die Mitte vom Barcode dass man sehen kann, welches System den erzeugt hat. Aber die letzte Nummer fängt dann bei einer Änderung von 1 an.
Im Hinterkopf kann ich ja das was auch schon mal vorgeschlagen wurde, die Daten nicht komplett auszutauschen sondern fortzuschreiben behalten. Aber mein Urlaub ist am Montag wieder rum, dann hab ich wieder weniger Zeit, also schau ich, dass das läuft was laufen muss.
Dann sollen die testen und entscheiden was sie noch haben wollen.
Danke für die bisherige Unterstützung.
theoS
User
Beiträge: 108
Registriert: Dienstag 5. November 2019, 21:44

Also, jetzt habe ich zwei Sachen gemacht.
Die eine, ich habe in meiner DB ein zweite Tabelle drin, in der das Hochzählen stattfindet und deren Wert dann (sofern ich jetzt nicht was übersehen hab) in der CSV gespeichert wird und deren ID's von der Konfigdatei übernommen werden also so, dass sie in Ruhe gelassen werden wenn sie schon da sind. Hier beginnt dann der Startwert beim ersten Mal bei 0, wenn dieser Datensatz dann noch mal kommt, wird dort weitergezählt.
Dann habe ich zumindest das Problem mal soweit von der Seite des Raspis gebannt, dass mir da die Zahlen überschrieben werden.
Dass man erkennt, wer gedruckt hat, habe ich im Barcode eine Nummer zwischenrein gemacht.

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import time
from datetime import datetime as DateTime
from pathlib import Path

import pandas as pd
import pyudev
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError

PFAD = Path.home() / ".DruckData"
MEDIA_PFAD = Path("/media/earl/")
PFAD = Path.home() / ".DruckData"
DATABASE = PFAD / "sqlite/db/config8.db"
CONFIG_ON_STICK = Path("TB_Ausgabe_8iii.txt")
CSV_DATEI_TO_STICK = Path("gedruckte_nummern.csv")
#SQL_OUT = "SELECT ID, E_St, ISPT_Nr, Aktuell_Nr, Barcode FROM knopfdaten"
SQL_OUT = """select distinct
             knopfdaten.ID,
             knopfdaten.E_St,
             knopfdaten.ISPT_Nr,
             numbers.Letzte_Nr
             from knopfdaten,
             numbers
             where
             numbers.ID = knopfdaten.ID"""

USED_COLS = [
        'ID',
        'E_St',
        'Zeile3',
        'Zeile5',
        'Zeile6',
        'Zeile7',
        'Zeilex',
        'ISPT_Nr',
        'HallenPos',
        'Aktuell_Nr',
        'Barcode'
        ]

def warte_auf_usb_stick(udev_context):
    monitor = pyudev.Monitor.from_netlink(udev_context)
    monitor.filter_by("block")
    for device in iter(monitor.poll, None):
        print(device.action)
        if "ID_FS_TYPE" in device and device.action == "add":
            name_of_stick = Path(device.get("ID_FS_LABEL"))
            print(device.action, name_of_stick)
            time.sleep(2)
            return name_of_stick

    raise AssertionError("unreachable code")


def anzahl_drucke_dokumentieren(connection, name_of_stick, dateiname_roh):
    dateiname_ziel = (
        MEDIA_PFAD
        / name_of_stick
        / dateiname_roh.with_name(
            f"{dateiname_roh.stem}_{DateTime.now():%Y-%m-%d_%H_%M}.csv"
        )
    )
    pd.read_sql(SQL_OUT, connection).to_csv(
        dateiname_ziel, sep=";", decimal=",", index=False
    )


def config_arbeitsdb(connection, name_of_stick):
    config_datei = MEDIA_PFAD / name_of_stick / CONFIG_ON_STICK
    #
    # TODO Auf die Spalten einschränken die tatsächlich benötigt werden.
    #
    datensaetze = pd.read_csv(        
        config_datei,
        usecols=USED_COLS,
        na_values="x",
        quotechar='"',
        sep=";",
        encoding="utf-8",
        decimal=",",
        dtype={"ID": int, "E_St": int, "ISPT_Nr": str, "Barcode": str},
    )
    if len(datensaetze) != 8:
        print("es waren keine 8 Datensätze")
    else:
        datensaetze.to_sql(
            "knopfdaten", connection, if_exists="replace", index=False
        )
        sql_ident = "insert or ignore into numbers(ID) select ID from knopfdaten;"
        connection.execute(sql_ident)



def auswerfen(name_of_stick):
    subprocess.run(
        ["umount", MEDIA_PFAD / name_of_stick],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )


def main():
    udev_context = pyudev.Context()
    db_engine = create_engine(f"sqlite:///{DATABASE}", encoding="utf-8")
    while True:
        name_of_stick = warte_auf_usb_stick(udev_context)
        try:
            config_arbeitsdb(db_engine, name_of_stick)
            anzahl_drucke_dokumentieren(
                db_engine, name_of_stick, CSV_DATEI_TO_STICK
            )
        except SQLAlchemyError as error:
            print(error)
        except ValueError as error:
            print(error)
        except OSError as error:
            print("Fehler beim kopieren:", error)

        try:
            auswerfen(name_of_stick)
        except subprocess.CalledProcessError as error:
            print(f"Fehler {error.returncode} beim Auswerfen: {error.stdout}")


if __name__ == "__main__":
    main()
Und im Teil mit den Knöpfen dann so:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf8 -*-

import sqlite3
from sqlite3 import Error
from pathlib import Path
import csv
import tkinter as tk
from functools import partial
from contextlib import closing


PFAD = Path.home() / ".DruckData"
BARCODE_DB_FILENAME = PFAD / "sqlite/db"
DATABASE = BARCODE_DB_FILENAME / "config8.db"
ORT = "PZ Dingsbums 14"
CHECKPOINT = "42"
SQL_UPDATE_AKTUELLE_NR = """UPDATE numbers
    SET letzte_nr = (letzte_nr + 1) % 10000 
    WHERE id = ?"""
SQL_SELECT_KNOPFDATEN = """SELECT knd.ISPT_Nr,
                           num.Letzte_Nr
                           FROM knopfdaten as knd,
                           numbers as num
                           WHERE knd.ID = ? 
	                   AND knd.ID = num.ID"""



def save_row_as_file(count, row, first_line):
    
    text = "\n".join(
        [
            "^XA",
            "^FO15,90^GB780,0,8,^FS",
            "^FO15,250^GB780,0,8,^FS",
            "^FO15,700^GB780,0,8,^FS",
            "^FO0,0^GB600,200,2",
            "^FO15,20^GB780,785,4^FS",
            "^FO0,40^A0,50,50^FB800,1,0,C^FD",
            first_line,
            "^FS^FO0,110^A0,60,60^FB800,1,0,C^FD",
            str(row[1]),
            "^FS^FO0,190^A0,70,70^FB800,1,0,C^FD",
            str(row[2]),
            "^FS^FO0,80^BY3",
            "^BCN,170,Y,N,N",
            "^FO165,270^BY4^FD",
            str(count),
            "^FS^FO0,500^A0,60,50^FB800,,0,C^FD",
            str(row[3]),
            "^FS^FO0,580^A0,60,50^FB800,,0,C^FD",
            str(row[4]),
            "^FS^FO0,730^A0,60,60^FB800,1,0,C^FD",
            str(row[5]),
            "^FS",
            "^XZ",
            "",
        ]
    )
    PFAD.mkdir(exist_ok=True)
    (PFAD / f"testT_{row[3]}_{row[1]}.zpl").write_text(text, "utf-8")


def lade_daten(conn):
    curs2 = conn.cursor()
    config_sql = "select ID, E_St, Zeile3, Zeile5, Zeile6, Zeile7 from knopfdaten;"
    curs2.execute(config_sql)
    configdaten = curs2.fetchall()
    
    return  configdaten

 
def zaehl_ausdrucke(connection, id):   
    with closing(connection.cursor()) as cursor:
        cursor.execute(SQL_UPDATE_AKTUELLE_NR, [id])
        cursor.execute(SQL_SELECT_KNOPFDATEN, [id])
        ispt_nr, letzte_nr = cursor.fetchone()
        barcode = f"{ispt_nr}{CHECKPOINT}{letzte_nr}"
        cursor.execute("update knopfdaten SET Barcode = ? where id = ?",  (barcode, id))
        print(barcode, letzte_nr)
        connection.commit()
    return barcode

def on_click(row, conn):
    count = zaehl_ausdrucke(conn, row[0])
    print(count)
    save_row_as_file(count, row, ORT)    

def main():
    
    BARCODE_DB_FILENAME.mkdir(parents=True, exist_ok=True)
   
            
    try:
        conn = sqlite3.connect(f"{DATABASE}")        
    except Error as e:
        print(e)
    configdaten = lade_daten(conn)  #lädt die Daten aus der csv nach

    root = tk.Tk()
    root.title("Auswahl der Label")
    root.config(background="#f2c618")
    button_frame = tk.Frame(root, width=1200, height=400)
    button_frame.grid(row=0, column=0, padx=10, pady=3)
    
    for index, entry in enumerate(configdaten):
        row_index, column_index = divmod(index, 4)
        tk.Button(
            button_frame,
            text="{}\n{}".format(entry[1], entry[2]),
            bg="#f2c618",
            width=15,
            height=10,
            command=partial(on_click, entry, conn),
            ).grid(row=row_index, column=column_index, padx=0, pady=0)

    root.mainloop()

        
if __name__ == '__main__':

    main()

ToDo ist das Ganze zusammenzubringen und vor allem ein Fehlermeldefenster und eine Möglichkeit den Raspi auszuschalten ohne einfach den Stecker zu ziehen.
Der hat ja nur einen Touchscreen.
Antworten