Commit 8e9dcfe4 authored by Marco Schmiedel's avatar Marco Schmiedel

Backup

parent 5fb2066a
......@@ -2,11 +2,11 @@
"fileId": "24784b38-54dc-4000-9d2a-f59082ebbc1c",
"originalPath": "work/models/base_base.py",
"currentPath": "work/models/base_base.py",
"hash": "a13647e2879a37dfcb76bf95c143fc42b0da2eac67a471884afd5c314dc46f8f",
"hash": "722215d7b7ca6bc285f74f96c5096fdae09a7d3ec477e319031640e3100ad5b4",
"docContent": "<p><br></p>",
"checkedStatus": "done",
"checkedStatus": "changed",
"comments": [],
"lastCheckedTimestamp": 1745314601207,
"lastFileModificationTimestamp": 1745313922231.5488,
"lastFileModificationTimestamp": 1746440397236.4856,
"flaggedForCopy": false
}
......@@ -2,10 +2,10 @@
"fileId": "36e791b4-e235-42f6-ac61-8560f1762892",
"originalPath": "work/workbench/Workbench.mwb",
"currentPath": "work/workbench/Workbench.mwb",
"hash": "f10bcc0690c342970656e4b4dcaa462b189bc5fa833b9f2bb04f88731782b1f0",
"hash": "d53db9e9d211116d4aafc32106a7e0c05a86c062af72f21a37420853a1c4eacc",
"docContent": "<p><br></p>",
"checkedStatus": "done",
"checkedStatus": "changed",
"comments": [],
"lastCheckedTimestamp": 1744805338829,
"lastFileModificationTimestamp": 1744805330071.942
"lastCheckedTimestamp": 1746433397717,
"lastFileModificationTimestamp": 1746440499172.53
}
......@@ -2,10 +2,10 @@
"fileId": "543b791f-9a45-4b53-906e-c49f79ac95d7",
"originalPath": "work/notebooks/ImportCsvToDatabase.ipynb",
"currentPath": "work/notebooks/ImportCsvToDatabase.ipynb",
"hash": "c6e6e4c70653fbfc4d43ae89e4f05fc1a8d2804eb2f515ad52c41637bd5b0e14",
"hash": "1897435119be0001a90340771be7cde4ca337b9d59653cf0faed8cd52514815c",
"docContent": "<p><br></p>",
"checkedStatus": "changed",
"comments": [],
"lastCheckedTimestamp": 1744806594117,
"lastFileModificationTimestamp": 1745313435812.8953
"lastFileModificationTimestamp": 1746447349386.795
}
......@@ -4,8 +4,8 @@
"currentPath": "work/workbench/Documentation.md",
"hash": "35d995fb3f8a904ed6f47adbd4da60916c93a29aad08af551b493a32714dcd9c",
"docContent": "<p><br></p>",
"checkedStatus": "done",
"checkedStatus": "todo",
"comments": [],
"lastCheckedTimestamp": 1744805208732,
"lastCheckedTimestamp": 1746428387938,
"lastFileModificationTimestamp": 1744805204185.4846
}
......@@ -2,10 +2,10 @@
"fileId": "f645f6dc-6831-4020-a6ae-8b5a572eed54",
"originalPath": "work/config/OpenAiConfig.py",
"currentPath": "work/config/OpenAiConfig.py",
"hash": "3da3805934b36b3ab8c21e10d8babe3cc8b6c9acb7b2b1446f782612b76cf2c4",
"hash": "50c0f7d96f9ea76aa069a0a24137e898dbd4fc3c4af867565c90468981bf6ff5",
"docContent": "<p><br></p>",
"checkedStatus": "todo",
"checkedStatus": "changed",
"comments": [],
"lastCheckedTimestamp": 1745314586062,
"lastFileModificationTimestamp": 1745313976355.6653
"lastFileModificationTimestamp": 1746437070245.503
}
# Wir verwenden Ubuntu als Betriebssystem.
FROM ubuntu:24.04
# Wir deaktivieren das interaktive Frontend.
ENV DEBIAN_FRONTEND=noninteractive
# Zuerst aktualisieren wir die Paketquellen und führen ein Upgrade durch.
RUN apt-get -y update
RUN apt-get -y upgrade
# Anschließend installieren wir systemweite Hilfspakete.
RUN apt-get install -y software-properties-common
# Wir installieren Python3 und pip.
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
# Wir fügen das PPA hinzu, um die deb-basierte Version von Chromium zu erhalten.
RUN add-apt-repository ppa:xtradeb/apps -y
# Wir aktualisieren erneut die Paketquellen (dadurch werden auch die PPA-Pakete verfügbar).
RUN apt-get -y update
# Wir installieren den Browser.
RUN apt-get install -y chromium-browser
RUN apt-get install -y chromium-driver
RUN apt-get install -y firefox-geckodriver
# Wir entfernen snapd, damit keine Snap-Version von Chromium verwendet wird.
RUN apt-get remove -y snapd
# Wir installieren Cron.
RUN apt-get install -y cron
# Wir installieren Vim.
RUN apt-get install -y vim
# Wir installhieren htop.
RUN apt-get install -y htop
# Wir installhieren ffmpeg.
RUN apt-get install -y ffmpeg
# Wir installhieren curl.
RUN apt-get install -y curl
# Wir installieren die Python-Abhängigkeiten via pip.
RUN pip3 install --break-system-packages selenium
RUN pip3 install --break-system-packages requests
RUN pip3 install --break-system-packages sqlalchemy
RUN pip3 install --break-system-packages pymysql
RUN pip3 install --break-system-packages pandas
RUN pip3 install --break-system-packages bs4
RUN pip3 install --break-system-packages feedparser
RUN pip3 install --break-system-packages demjson3
RUN pip3 install --break-system-packages --ignore-installed flask
RUN pip3 install --break-system-packages feedgen
RUN pip3 install --break-system-packages boto3
RUN pip3 install --break-system-packages pydub
RUN pip3 install --break-system-packages json5
RUN pip3 install --break-system-packages pyotp
RUN pip3 install --break-system-packages sshtunnel
RUN pip3 install --break-system-packages pypdf
# Wir kopieren die Cron-Datei in den Container.
COPY config/_CronConfig.txt /etc/cron.d/scrapeNewsCron
RUN chmod 0644 /etc/cron.d/scrapeNewsCron
RUN crontab /etc/cron.d/scrapeNewsCron
COPY cron.sh /maui/cron.sh
RUN chmod +x /maui/cron.sh
# Wir kopieren alle Systemdatein in den Container.
COPY config /maui/config
COPY manager /maui/manager
COPY commands /maui/commands
COPY models /maui/models
COPY boot.sh /maui/boot.sh
RUN chmod +x /maui/boot.sh
# Wir definieren das Startscript des Containers.
CMD ["/maui/boot.sh"]
\ No newline at end of file
#!/bin/bash
set -e
# Dieser Befehl startet den Cron-Service.
service cron start
# Dieser Befehl wechselt in das Arbeitsverzeichnis /obsidian/manager.
cd /maui/manager
# Dieser Befehl setzt die Umgebungsvariable PYTHONPATH auf /obsidian.
export PYTHONPATH=/maui
# Dieser Befehl startet den ApiManager Webserver.
python3 WebManager.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Hier wird der Suchpfad um das übergeordnete Verzeichnis erweitert, damit lokale Module gefunden werden.
import sys
sys.path.append("..")
#
# Hier werden Funktionen des Betriebssystems eingebunden.
import os
#
# Hier werden reguläre Ausdrücke zur Textbearbeitung eingebunden.
import re
#
# Hier wird das ast-Modul eingebunden, um Python-ähnliche Literale zu parsen.
import ast
#
# Hier wird das json-Modul eingebunden, um JSON-Daten zu verarbeiten.
import json
#
# Hier wird das datetime-Modul unter dem Alias _dt eingebunden, um Zeitstempel zu erzeugen.
import datetime as _dt
#
# Hier wird das traceback-Modul eingebunden, um Fehlermeldungen formatiert auszugeben.
import traceback
#
# Hier werden Typ-Alias-Definitionen aus dem typing-Modul eingebunden, um den Code klarer zu gestalten.
from typing import Any, Dict, List, Tuple
#
# Hier wird die pypdf-Bibliothek eingebunden, um Textinhalte aus PDF-Dateien zu extrahieren.
from pypdf import PdfReader
#
# Hier wird eine spezifische Exception aus pypdf eingebunden, um Leseprobleme differenziert zu behandeln.
from pypdf.errors import PdfReadError
#
# Hier wird der OpenAI-Manager eingebunden, um Chat-Nachrichten an GPT-Modelle zu senden.
from manager.OpenAiManager import OpenAiManager
#
# Hier wird der MySQL-Manager eingebunden, um Datenbank-Sessions zu erzeugen.
from manager.MysqlManager import MysqlManager
#
# Hier werden die SQLAlchemy-Basisklassen eingebunden, damit alle Models korrekt referenziert werden.
from models._system import Base
#
# Hier wird das Model BaseBase eingebunden, das die Haupttabelle für Tarife repräsentiert.
from models.base_base import BaseBase
#
# Hier wird das Model DealDeal eingebunden, das zugehörige Deal-Einträge verwaltet.
from models.deal_deal import DealDeal
#
# Hier wird das Model OptionOpti eingebunden, das optionale Tarif-Bausteine abbildet.
from models.option_opti import OptionOpti
#
# Hier wird der vollständige Prompt als mehrzeiliger String definiert, der alle Extraktionsregeln beinhaltet.
promptTemplate: str = (
"""
Du bist eine hochpräzise API zur Extraktion spezifischer Mobilfunktarif-Merkmale aus Dokumentenpaaren. Deine Eingabe besteht immer aus dem extrahierten Text von zwei PDF-Dateien: einem **Produktdetailblatt/Flyer** und einem **Produktinformationsblatt (PIB)**, die gemeinsam *einen* spezifischen Tarif beschreiben.
Deine Aufgabe ist es, beide Dokumente sorgfältig und vergleichend zu analysieren, um die unten definierten **relevanten Tarifbestandteile** mit höchstmöglicher Genauigkeit zu extrahieren. Das Ergebnis **muss ausschließlich** ein einzelnes JSON-Objekt sein, das exakt die vorgegebenen Schlüsselnamen und das flache Key-Value-Format verwendet. Ignoriere irrelevante Informationen wie Anbieteradressen, AGB-Verweise oder allgemeine Marketingtexte.
**Extraktionsanweisungen und Felddefinitionen:**
1. **`tariff_name` (String):** Extrahiere den vollständigen, exakten Namen des Tarifs.
2. **`marketing_start_date` (String oder null):** Extrahiere das Vermarktungsdatum (aus PIB). Formatiere als `JJJJ-MM-TT` oder `null`.
3. **`network_operator` (String oder null):** Identifiziere den Netzbetreiber (z.B. "Telekom", "Vodafone", "O2").
4. **`network_technology` (String oder null):** Identifiziere die primär beworbene/höchste Netztechnologie (z.B. "5G", "LTE").
5. **`is_data_only_tariff` (Boolean):** Erfasse, ob es ein reiner Datentarif ist. Kriterien: Telefonie nicht als Flat inkludiert oder explizit als "nicht möglich". Prüfe Inklusivleistungen und Preise sorgfältig.
6. **`inclusive_internet_flat` (Boolean oder null):** Prüfe auf explizite Nennung einer Internet-Flat.
7. **`inclusive_telephony_flat` (Boolean oder null):** Prüfe auf explizite Nennung einer Telefonie-Flat oder Preis pro Minute von 0,00 €.
8. **`telephony_price_per_minute_eur_brutto` (Number oder null):** Wenn `inclusive_telephony_flat` false und Telefonie möglich: extrahiere Brutto-Preis pro Minute. Sonst 0.0 (Flat) oder null.
9. **`telephony_price_per_minute_eur_netto` (Number oder null):** Bei positivem Brutto-Wert: Netto = Brutto / 1.19, gerundet auf 4 Nachkommastellen. Sonst 0.0 oder null.
10. **`inclusive_sms_flat` (Boolean oder null):** Prüfe auf SMS-Flat oder Preis pro SMS 0,00 €.
11. **`sms_price_per_unit_eur_brutto` (Number oder null):** Wenn keine SMS-Flat: extrahiere Brutto-Preis pro SMS.
12. **`sms_price_per_unit_eur_netto` (Number oder null):** Bei positivem Brutto-Wert: Netto = Brutto / 1.19, gerundet auf 4 Nachkommastellen.
13. **`inclusive_volte_wlan_call` (Boolean oder null):** Prüfe auf VoLTE/WLAN-Call. Setze false bei reinen Datentarifen.
14. **`data_volume_gb` (Number oder null):** Extrahiere das Datenvolumen in GB.
15. **`data_download_max_mbps` (Number oder null):** Maximaler Download in Mbit/s.
16. **`data_upload_max_mbps` (Number oder null):** Maximaler Upload in Mbit/s.
17. **`data_download_throttled_kbps` (Number oder null):** Gedrosselter Download in kbit/s.
18. **`data_upload_throttled_kbps` (Number oder null):** Gedrosselter Upload in kbit/s.
19. **`data_billing_increment_kb` (Number oder null):** Datentaktung in KB.
20. **`telephony_billing_increment_seconds` (String oder null):** Telefontaktung, z.B. "60/60".
21. **`pricing_connection_fee_eur_brutto` (Number oder null):** Einmaliger Anschlusspreis (brutto).
22. **`pricing_connection_fee_eur_netto` (Number oder null):** Netto-Anschlusspreis = Brutto / 1.19, gerundet auf 4 Nachkommastellen.
23. **`pricing_monthly_initial_eur_brutto` (Number oder null):** Monatlicher Bruttopreis in der Anfangsphase.
24. **`pricing_monthly_initial_eur_netto` (Number oder null):** Netto-Betrag derselben Phase; falls nicht angegeben selbst berechnen.
25. **`pricing_monthly_after_period_eur_brutto` (Number oder null):** Bruttopreis nach Aktionsphase.
26. **`pricing_monthly_after_period_eur_netto` (Number oder null):** Netto-Preis nach Aktionsphase = Brutto / 1.19.
27. **`contract_min_duration_months` (Number oder null):** Mindest-Vertragslaufzeit in Monaten.
28. **`contract_cancellation_notice_period_months` (Number oder null):** Kündigungsfrist in Monaten.
Werte, die nicht zuverlässig extrahiert werden können, auf `null` setzen.
Numerische Werte als Number belassen, Netto stets auf 4 Nachkommastellen runden.
**WICHTIG:** Antworte ausschließlich mit dem JSON-Objekt. Schreibe weder Markdown noch zusätzliche Erklärungen.
#########
"""
)
#
# Hier wird die Liste der Schlüssel definiert, die im GPT-Ergebnis zwingend vorhanden sein müssen.
expectedKeys: List[str] = [
"tariff_name",
"marketing_start_date",
"network_operator",
"network_technology",
"is_data_only_tariff",
"inclusive_internet_flat",
"inclusive_telephony_flat",
"telephony_price_per_minute_eur_brutto",
"telephony_price_per_minute_eur_netto",
"inclusive_sms_flat",
"sms_price_per_unit_eur_brutto",
"sms_price_per_unit_eur_netto",
"inclusive_volte_wlan_call",
"data_volume_gb",
"data_download_max_mbps",
"data_upload_max_mbps",
"data_download_throttled_kbps",
"data_upload_throttled_kbps",
"data_billing_increment_kb",
"telephony_billing_increment_seconds",
"pricing_connection_fee_eur_brutto",
"pricing_connection_fee_eur_netto",
"pricing_monthly_initial_eur_brutto",
"pricing_monthly_initial_eur_netto",
"pricing_monthly_after_period_eur_brutto",
"pricing_monthly_after_period_eur_netto",
"contract_min_duration_months",
"contract_cancellation_notice_period_months",
]
#
# Diese Funktion extrahiert den kompletten Text einer PDF-Datei und gibt ihn als String zurück.
def extractTextFromPdf(pdfPath: str) -> str | None:
if not os.path.exists(pdfPath):
print(f"INFO: Datei nicht gefunden: {os.path.basename(pdfPath)}")
return None
pageTexts: List[str] = []
try:
with open(pdfPath, "rb") as fileHandle:
reader = PdfReader(fileHandle)
for page in reader.pages:
txt = page.extract_text()
if txt:
pageTexts.append(txt)
if not pageTexts:
print(f"INFO: Kein Text in {os.path.basename(pdfPath)}")
return None
return "\n".join(pageTexts).strip()
except PdfReadError as exc:
print(f"WARNUNG: pypdf-Lesefehler bei '{os.path.basename(pdfPath)}': {exc}")
return None
except Exception:
print(f"FEHLER: Unerwarteter Fehler bei '{os.path.basename(pdfPath)}':")
traceback.print_exc(limit=1)
return None
#
# Diese Funktion entfernt eingehende Code-Fences, um reines JSON zu erhalten.
def stripCodeFence(raw: str) -> str:
if raw.strip().startswith("```"):
return re.sub(r"```[\w]*", "", raw).strip()
return raw
#
# Diese Funktion entfernt überflüssige Kommas vor schließenden Klammern aus JSON-Strings.
def removeTrailingCommas(js: str) -> str:
return re.sub(r",(\s*[}\]])", r"\1", js)
#
# Diese Funktion versucht, einen String in ein Dictionary umzuwandeln und nutzt mehrere Reparatur-Ansätze.
def loadJsonSafe(raw: str) -> Dict[str, Any] | None:
cleaned = stripCodeFence(raw).replace("\r", "")
for variant in (cleaned, removeTrailingCommas(cleaned)):
try:
return json.loads(variant)
except json.JSONDecodeError:
pass
try:
relaxed = cleaned.replace("null", "None").replace("true", "True").replace("false", "False")
return ast.literal_eval(relaxed)
except Exception:
return None
#
# Diese Funktion validiert die GPT-Antwort und prüft, ob alle Pflichtschlüssel vorhanden sind.
def validateResponse(raw: str) -> Tuple[bool, Dict[str, Any] | None]:
data = loadJsonSafe(raw)
if data is None or not isinstance(data, dict):
print("VALIDATION: Antwort ist kein gültiges JSON-Objekt.")
return False, None
missing = [k for k in expectedKeys if k not in data]
if missing:
print(f"VALIDATION: Fehlende Schlüssel: {', '.join(missing)}")
return False, None
return True, data
#
# Dieser Block wird ausgeführt, wenn das Skript direkt gestartet wird.
if __name__ == "__main__":
cacheDir = "../cache"
print(f"INFO: Suche nach PDF-Dateien in '{cacheDir}' …")
if not os.path.isdir(cacheDir):
print("FEHLER: Cache-Verzeichnis nicht gefunden.")
sys.exit(1)
pdfFiles = [f for f in os.listdir(cacheDir) if f.lower().endswith(".pdf")]
tariffIds: set[str] = set()
for f in pdfFiles:
stem = f[:-4].lower()
if stem.endswith("_flyer"):
tariffIds.add(stem[:-6])
elif stem.endswith("_pib"):
tariffIds.add(stem[:-4])
if not tariffIds:
print("INFO: Keine passenden PDF-Paare gefunden.")
sys.exit(0)
gptManager = OpenAiManager()
dbSession = MysqlManager().getSession()
for tariffId in sorted(tariffIds):
print(f"\n--- Verarbeitung ID: {tariffId} ---")
baseRecords = dbSession.query(BaseBase).filter_by(providercode_base=tariffId).all()
if not baseRecords:
print("WARNUNG: Kein BaseBase-Datensatz gefunden – übersprungen.")
continue
if all(br.details_base for br in baseRecords):
print("INFO: details_base bereits für alle Zeilen gefüllt – übersprungen.")
continue
flyerPath = os.path.join(cacheDir, f"{tariffId}_flyer.pdf")
pibPath = os.path.join(cacheDir, f"{tariffId}_pib.pdf")
flyerText = extractTextFromPdf(flyerPath)
pibText = extractTextFromPdf(pibPath)
if not flyerText or not pibText:
print("INFO: Fehlende Texte – übersprungen.")
continue
fullPrompt = promptTemplate + "# Flyer-Text:\n" + flyerText + "\n" + "# PIB-Text:\n" + pibText
validatedData: Dict[str, Any] | None = None
for attempt in range(1, 4):
print(f"INFO: GPT-Abfrage Versuch {attempt}/3 …")
try:
raw = gptManager.chat(fullPrompt, model="gpt-4.1")
except Exception as exc:
print(f"FEHLER: GPT-Abfrage fehlgeschlagen: {exc}")
raw = ""
ok, parsed = validateResponse(raw)
if ok:
validatedData = parsed
break
print("WARNUNG: Antwort ungültig – nächster Versuch …")
if not validatedData:
print("FEHLER: Drei ungültige Antworten – übersprungen.")
continue
for br in baseRecords:
if br.details_base is None:
br.details_base = validatedData
br.updated_base = _dt.datetime.now()
dbSession.commit()
print(f"INFO: JSON in {len([b for b in baseRecords if b.details_base])} Zeile(n) gespeichert.")
dbSession.close()
print("INFO: Verarbeitung abgeschlossen.")
import sys
# In diesem import wird der Pfad um eine Ebene nach oben erweitert.
sys.path.append("..")
# In diesem import werden Funktionen des Betriebssystems eingebunden.
import os
# In diesem import wird Funktionalität zum Lesen und Schreiben von CSV-Dateien eingebunden.
import csv
# In diesem import wird Funktionalität zum Arbeiten mit Datum und Uhrzeit eingebunden.
import datetime
# In diesem import wird Funktionalität zum Hinzufügen von Pausen im Code eingebunden.
import time
# In diesem import wird die Bibliothek für zeitbasierte Einmalpasswörter eingebunden.
import pyotp
# In diesem import wird Funktionalität für reguläre Ausdrücke eingebunden.
import re
# In diesem import wird Funktionalität für Base64-Codierung eingebunden.
import base64
# In diesem import wird eine Klasse für genaue Dezimalberechnungen eingebunden.
from decimal import Decimal
# In diesem import wird die Funktionalität zur Rückverfolgung von Fehlern eingebunden.
import traceback
# In diesem import wird Funktionalität zum Kopieren und Löschen von Dateien eingebunden.
import shutil
# In diesem import wird die Bibliothek zum Umgang mit Pandas-Datenstrukturen eingebunden.
import pandas as pd
# In diesem import wird Funktionalität für HTTP-Anfragen eingebunden.
import requests
# In diesem import wird die Bibliothek BeautifulSoup zum Parsen von HTML eingebunden.
from bs4 import BeautifulSoup
# In diesem import werden verschiedene Selektoren aus Selenium eingebunden.
from selenium.webdriver.common.by import By
# In diesem import werden Aktionen zum Simulieren von Mausbewegungen eingebunden.
from selenium.webdriver.common.action_chains import ActionChains
# In diesem import wird eine explizite Wartefunktion für Selenium geladen.
from selenium.webdriver.support.ui import WebDriverWait
# In diesem import wird eine Klasse zum Erstellen von Dropdown-Auswahlen eingebunden.
from selenium.webdriver.support.ui import Select
# In diesem import werden verschiedene Bedingungen für Selenium-Wartefunktionen eingebunden.
from selenium.webdriver.support import expected_conditions as EC
# In diesem import wird eine spezielle Ausnahme für Zeitüberschreitungen in Selenium geladen.
from selenium.webdriver.support.wait import TimeoutException
# In diesem import wird eine Ausnahme für nicht vorhandene Elemente in Selenium geladen.
from selenium.common.exceptions import NoSuchElementException
# In dieser import-Anweisung wird der SeleniumManager geladen.
from manager.SeleniumManager import SeleniumManager
# In dieser import-Anweisung werden Zugangsdaten aus der MauiConfig geladen.
from config.MauiConfig import MAUI_USERNAME, MAUI_PASSWORD, MAUI_AUTHCODE
# In dieser import-Anweisung wird ein MySQL-Manager zum Umgang mit Datenbanken geladen.
from manager.MysqlManager import MysqlManager
# In dieser import-Anweisung werden Modelle aus der System-Klasse geladen.
from models._system import Base
# In dieser import-Anweisung werden Basisklassen für Datenbankmodelle geladen.
from models.base_base import BaseBase
# In dieser import-Anweisung wird das Modell DealDeal eingebunden.
from models.deal_deal import DealDeal
# In dieser import-Anweisung wird das Modell OptionOpti eingebunden.
from models.option_opti import OptionOpti
# In dieser Variablen wird eine Menge gespeichert, um doppelte Kategorien zu vermeiden.
uniqueCategorySet = set()
# In dieser Funktion wird eine PDF-Datei aus dem Selenium-Kontext als Base64 heruntergeladen und abgespeichert.
def downloadPdfSelenium(seleniumDriver, pdfUrl, downloadFolder, fileName):
# In dieser Variablen wird ein Skript abgelegt, das als asynchroner Aufruf eine PDF-Datei anfordert und als Base64-String zurückliefert.
downloadScript = """
var callback = arguments[arguments.length - 1];
var xhr = new XMLHttpRequest();
xhr.open('GET', arguments[0], true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
var uInt8Array = new Uint8Array(xhr.response);
var binaryString = '';
for (var i = 0; i < uInt8Array.length; i++){
binaryString += String.fromCharCode(uInt8Array[i]);
}
var base64 = window.btoa(binaryString);
callback(base64);
};
xhr.onerror = function() { callback(null); };
xhr.send();
"""
# In dieser Variablen wird das Ergebnis des ausgeführten Skripts als Base64-String gespeichert.
pdfBase64String = seleniumDriver.execute_async_script(downloadScript, pdfUrl)
# In dieser if-Abzweigung wird geprüft, ob die Base64-Rückgabe korrekt ist.
if not pdfBase64String:
# In diesem Zweig wird eine Ausnahme ausgelöst, wenn kein gültiger Base64-Inhalt vorliegt.
raise Exception("Der PDF-Download per Selenium ist fehlgeschlagen.")
# In dieser Variablen wird der vollständige Pfad für die zu speichernde PDF-Datei ermittelt.
destinationPath = os.path.join(downloadFolder, fileName)
# In diesem with-Block wird die Zieldatei erstellt und mit dem dekodierten Inhalt befüllt.
with open(destinationPath, "wb") as pdfFile:
# An dieser Stelle wird der Base64-String dekodiert und in das PDF geschrieben.
pdfFile.write(base64.b64decode(pdfBase64String))
# In dieser Funktion wird der Login-Prozess über Selenium realisiert.
def login(seleniumManager, userName, userPassword, rawToken):
# In dieser Variablen wird der Selenium-Driver nach dem Request zur Login-Seite gespeichert.
seleniumDriver = seleniumManager.simpleRequest("https://maui.md.de")
# In dieser Variablen wird ein WebDriverWait-Objekt angelegt, um Elemente abwarten zu können.
wait = WebDriverWait(seleniumDriver, 10)
# In dieser Variablen wird das Eingabefeld für den Benutzernamen gespeichert, nachdem es präsent ist.
usernameField = wait.until(EC.presence_of_element_located((By.ID, "mat-input-0")))
# In dieser Zeile wird der Benutzername in das Feld eingetragen.
usernameField.send_keys(userName)
# In dieser Variablen wird das Eingabefeld für das Passwort gespeichert, nachdem es präsent ist.
userPasswordField = wait.until(EC.presence_of_element_located((By.ID, "mat-input-1")))
# In dieser Zeile wird das Passwort in das Feld eingetragen.
userPasswordField.send_keys(userPassword)
# In diesem Aufruf wird eine kurze Wartezeit eingefügt, um Stabilität zu gewährleisten.
time.sleep(1)
# In dieser Variablen wird der primäre Login-Button gespeichert, um ihn anschließend zu klicken.
loginButtonElement = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(),'Anmelden')]]")))
# In dieser Zeile wird der Login-Button betätigt.
loginButtonElement.click()
# In dieser Variablen wird ein TOTP-Objekt erstellt, das mittels Raw Token einen Code generiert.
totpGenerator = pyotp.TOTP(rawToken)
# In dieser Variablen wird der aktuelle 2FA-Code aus dem TOTP-Objekt geholt.
twoFactorCode = totpGenerator.now()
# In dieser Variablen wird das Eingabefeld für den 2FA-Code gespeichert, nachdem es präsent ist.
twoFactorField = wait.until(EC.presence_of_element_located((By.ID, "mat-input-2")))
# In dieser Zeile wird der generierte 2FA-Code in das Feld geschrieben.
twoFactorField.send_keys(twoFactorCode)
# In dieser Variablen wird der Anmelde-Button im Dialog gespeichert.
modalLoginButtonElement = wait.until(EC.element_to_be_clickable((By.XPATH, "//mat-dialog-actions//button[span[contains(text(),'Anmelden')]]")))
# In dieser Zeile wird der Button per Skript geklickt, da er manchmal verdeckt sein kann.
seleniumDriver.execute_script("arguments[0].click();", modalLoginButtonElement)
# In dieser Funktion wird nach erfolgreichem Login die Laufzeitvertrags-Seite aufgerufen.
def openLaufzeitvertrag(seleniumManager):
# In dieser Variablen wird ein WebDriverWait erzeugt, um gezielt Elemente abzufragen.
wait = WebDriverWait(seleniumManager.driver, 10)
# In dieser Variablen wird das Element mit dem Link zu Laufzeitvertrag gefunden.
laufzeitElement = wait.until(EC.presence_of_element_located((By.XPATH, "//a[contains(text(),'Laufzeitvertrag')]")))
# In dieser Variablen wird der Link ausgelesen und anschließend geladen.
url = laufzeitElement.get_attribute("href")
seleniumManager.driver.get(url)
# In dieser Funktion wird geprüft, ob das Dropdown-Menü bereit ist, indem störende Overlays ausgeblendet sind.
def waitForDropdownReady(seleniumDriver, wait, maxRetries=3, retryDelay=5):
# In dieser for-Schleife werden mehrere Versuche unternommen, um das Dropdown verfügbar zu machen.
for attemptIndex in range(maxRetries):
# In diesem try-Block wird gezielt geprüft, ob bestimmte Overlays verschwunden sind.
try:
# In dieser Ausgabe wird informiert, welcher Versuch gerade stattfindet.
print(f"DEBUG: Warte auf Dropdown-Bereitschaft (Versuch {attemptIndex + 1}/{maxRetries})...")
# In dieser Zeile wird bis zu 60 Sekunden gewartet, bis ein bestimmtes Iframe unsichtbar ist.
WebDriverWait(seleniumDriver, 60).until(
EC.invisibility_of_element_located((By.XPATH, "//iframe[contains(@src, 'wait.html')]"))
)
# In dieser Zeile wird bis zu 60 Sekunden gewartet, bis ein bestimmtes Overlay unsichtbar ist.
WebDriverWait(seleniumDriver, 60).until(
EC.invisibility_of_element_located((By.ID, "bg_layer"))
)
# In dieser Zeile wird bis zu 20 Sekunden gewartet, bis das Tarif-Dropdown im DOM auftaucht.
wait.until(EC.presence_of_element_located((By.NAME, "tarif_id")))
# In dieser Ausgabe wird mitgeteilt, dass das Dropdown bereit ist.
print(f"DEBUG: Dropdown ist bereit (Versuch {attemptIndex + 1}).")
return True
# In dieser except-Abzweigung wird geprüft, ob eine TimeoutException vorliegt.
except TimeoutException:
# Hier wird eine Warnung ausgegeben, dass der aktuelle Versuch erfolglos war.
print(f"DEBUG: Warnung: Timeout beim Warten auf Dropdown-Bereitschaft (Versuch {attemptIndex + 1}/{maxRetries}).")
# In dieser if-Abzweigung wird geprüft, ob noch ein weiterer Versuch erfolgen soll.
if attemptIndex < maxRetries - 1:
time.sleep(retryDelay)
else:
print(f"DEBUG: FEHLER: Konnte nach {maxRetries} Versuchen nicht auf Dropdown-Bereitschaft warten.")
break
# In dieser except-Abzweigung werden unvorhergesehene Fehler protokolliert.
except Exception as exception:
print(f"DEBUG: Unerwarteter Fehler beim Warten auf Dropdown (Versuch {attemptIndex + 1}/{maxRetries}): {exception}")
# In dieser if-Abzweigung wird geprüft, ob noch weitere Versuche unternommen werden.
if attemptIndex < maxRetries - 1:
time.sleep(retryDelay)
else:
print(f"DEBUG: FEHLER: Konnte nach {maxRetries} Versuchen wegen unerwartetem Fehler nicht auf Dropdown warten.")
break
# In dieser Zeile wird False zurückgegeben, falls alle Versuche scheitern.
return False
# In dieser Funktion wird der aktuell im Frontend angezeigte Tarifpreis abgerufen und der Nettopreis berechnet.
def parsePlanPrice(seleniumDriver):
# In dieser Variablen wird ein Initialwert für den Nettopreis gesetzt.
priceNet = 0.0
# In diesem try-Block wird versucht, den Wert aus dem Element preis_anzeige_tarif auszulesen.
try:
priceElement = seleniumDriver.find_element(By.ID, "preis_anzeige_tarif")
priceText = priceElement.text
priceMatch = re.search(r'([\d\.,]+)\s*EUR', priceText)
# In dieser if-Abzweigung wird geprüft, ob der reguläre Ausdruck einen Treffer hatte.
if priceMatch:
rawStr = priceMatch.group(1).replace(".", "").replace(",", ".")
grossPrice = float(rawStr)
priceNet = round(grossPrice / 1.19, 5)
# In dieser except-Abzweigung wird ein Hinweis ausgegeben, falls kein Preis ermittelt werden konnte.
except Exception as exception:
print(f"DEBUG: Warnung: Konnte Tarifpreis nicht extrahieren: {exception}")
# In dieser Zeile wird der ermittelte Nettopreis zurückgegeben.
return priceNet
# In dieser Funktion werden Kampagnen aus dem entsprechenden Dropdown geparst und als Liste zurückgegeben.
def parseCampaigns(seleniumDriver):
# In dieser Liste werden alle gefundenen Kampagnen-Tuples gespeichert.
campaignsList = []
# In diesem try-Block wird versucht, das Kampagnen-Select und dessen Optionen zu finden.
try:
campaignSelect = seleniumDriver.find_element(By.NAME, "am_aktion_select")
campaignOptions = campaignSelect.find_elements(By.TAG_NAME, "option")
# In dieser for-Schleife werden die Attribute jeder Option ausgelesen und gefiltert.
for copt in campaignOptions:
val = copt.get_attribute("value")
txt = copt.text.strip()
# In dieser if-Abzweigung werden ungültige oder leere Werte übersprungen.
if not val or val in [" |", "-1|", "|", "-1|", "0|"]:
continue
partsVal = val.split("|")
campaignId = partsVal[0].strip()
# In dieser if-Abzweigung wird geprüft, ob eine Kampagnen-ID extrahiert werden konnte.
if not campaignId:
continue
# In dieser if-Abzweigung wird geprüft, ob ein Trennstrich im Text enthalten ist.
if "-" in txt:
splitted = txt.split("-", 1)
campaignName = splitted[1].strip()
else:
campaignName = txt
campaignsList.append((campaignId, campaignName))
# In dieser except-Abzweigung wird ein Hinweis ausgegeben, falls das Element nicht gefunden werden konnte.
except Exception as exception:
print(f"DEBUG: Warnung: Konnte Kampagnen nicht extrahieren: {exception}")
# In dieser Zeile wird die Liste der gefundenen Kampagnen zurückgegeben.
return campaignsList
# In dieser Funktion werden die Hauptdaten gescraped und in verschiedene CSV-Dateien geschrieben.
def scrapeData(seleniumManager):
# In dieser Variablen wird der Selenium-Driver abgelegt.
seleniumDriver = seleniumManager.driver
# In dieser Variablen wird ein WebDriverWait mit Timeout 20 Sekunden abgelegt.
wait = WebDriverWait(seleniumDriver, 20)
# In dieser Variablen wird ein Pfad für das Cache-Verzeichnis definiert.
cacheDir = "../cache"
# In dieser if-Abzweigung wird geprüft, ob das Verzeichnis bereits existiert.
if os.path.exists(cacheDir):
# In diesem try-Block wird versucht, das bestehende Verzeichnis zu löschen.
try:
shutil.rmtree(cacheDir)
print(f"Info: Bestehendes Cache-Verzeichnis '{cacheDir}' wurde gelöscht.")
except OSError as exception:
print(f"Fehler beim Löschen von Verzeichnis {cacheDir}: {exception}")
# In diesem try-Block wird das Verzeichnis neu erstellt oder sichergestellt, dass es existiert.
try:
os.makedirs(cacheDir, exist_ok=True)
print(f"Info: Cache-Verzeichnis '{cacheDir}' sichergestellt/neu erstellt.")
except OSError as exception:
print(f"Fehler beim Erstellen von Verzeichnis {cacheDir}: {exception}")
# In diesen Variablen werden die Pfade zu den einzelnen CSV-Dateien definiert.
plansCsvFilePath = os.path.join(cacheDir, "plans.csv")
campaignsCsvFilePath = os.path.join(cacheDir, "campaigns.csv")
optionsCsvFilePath = os.path.join(cacheDir, "options.csv")
categorysCsvFilePath = os.path.join(cacheDir, "categorys.csv")
# In dieser Menge werden bereits geschriebene Tarife gespeichert, um Duplikate zu vermeiden.
writtenPlanIdSet = set()
# In dieser Liste werden die möglichen Rahmenvertragsnummern abgelegt.
frameworkList = ["", 980066161, 980008940, 981000541]
# In diesem with-Block werden alle CSV-Dateien geöffnet und die Writer initialisiert.
with open(plansCsvFilePath, mode="w", newline="", encoding="utf-8") as plansFile, \
open(campaignsCsvFilePath, mode="w", newline="", encoding="utf-8") as campaignsFile, \
open(optionsCsvFilePath, mode="w", newline="", encoding="utf-8") as optionsFile, \
open(categorysCsvFilePath, mode="w", newline="", encoding="utf-8") as categorysFile:
# In diesen Variablen werden die CSV-Writer für jede Datei angelegt.
plansWriter = csv.writer(plansFile, delimiter=";")
campaignsWriter = csv.writer(campaignsFile, delimiter=";")
optionsWriter = csv.writer(optionsFile, delimiter=";")
categorysWriter = csv.writer(categorysFile, delimiter=";")
# In dieser Zeile werden die Spaltenüberschriften für die plans.csv geschrieben.
plansWriter.writerow(["id", "provider", "network", "name", "price", "rahmen"])
# In dieser Zeile werden die Spaltenüberschriften für die campaigns.csv geschrieben.
campaignsWriter.writerow(["id", "plan", "name"])
# In dieser Zeile werden die Spaltenüberschriften für die options.csv geschrieben.
optionsWriter.writerow(["id", "category", "plan", "name", "price"])
# In dieser Zeile werden die Spaltenüberschriften für die categorys.csv geschrieben.
categorysWriter.writerow(["id", "name"])
# In dieser for-Schleife werden alle Rahmenvertragsnummern durchlaufen.
for currentFramework in frameworkList:
# In dieser if-Abzweigung wird geprüft, ob wir eine Nummer im Rahmenfeld setzen müssen.
if currentFramework != "":
# In diesem try-Block wird die Checkbox für Rahmenvertrag angeklickt.
try:
wait.until(EC.element_to_be_clickable((By.NAME, "rv_option")))
rvCheckbox = seleniumDriver.find_element(By.NAME, "rv_option")
if not rvCheckbox.is_selected():
seleniumDriver.execute_script("arguments[0].click();", rvCheckbox)
except Exception as exception:
print(f"DEBUG: Konnte Checkbox 'rv_option' nicht setzen: {exception}")
# In diesem try-Block wird das Eingabefeld für die Rahmenvertragsnummer gesetzt.
try:
wait.until(EC.presence_of_element_located((By.NAME, "rv_nr")))
rvNrField = seleniumDriver.find_element(By.NAME, "rv_nr")
rvNrField.clear()
rvNrField.send_keys(str(currentFramework))
except Exception as exception:
print(f"DEBUG: Konnte Feld 'rv_nr' nicht setzen: {exception}")
else:
print("emptyRahmen")
# In dieser Zeile wird eine Wartezeit von 5 Sekunden eingefügt.
time.sleep(5)
# In diesen Variablen werden die aktuellen Tarifwelten und Netze neu ausgelesen.
tarifWeltElements = seleniumDriver.find_elements(By.NAME, "tarif_welt")
tarifWelten = [elem.get_attribute("value") for elem in tarifWeltElements if elem.get_attribute("value")]
netzElements = seleniumDriver.find_elements(By.NAME, "netz")
netzList = [elem.get_attribute("value") for elem in netzElements if elem.get_attribute("value")]
# In diesem try-Block wird der Radio-Button für die Produktkategorie 'A' geklickt.
try:
productCategoryElement = wait.until(EC.element_to_be_clickable((By.XPATH, '//input[@name="sel_produkt_kategorie" and @value="A"]')))
seleniumDriver.execute_script("arguments[0].click();", productCategoryElement)
except Exception as exception:
print(f"DEBUG: Konnte die Produktkategorie 'A' nicht auswählen: {exception}")
traceback.print_exc()
continue
# In dieser for-Schleife werden alle gefundenen Tarifwelten durchlaufen.
for tarifWelt in tarifWelten:
# In dieser if-Abzweigung wird geprüft, ob das Dropdown bereit ist.
if not waitForDropdownReady(seleniumDriver, wait):
print(f"DEBUG: Überspringe Tarifwelt {tarifWelt}, da die Seite nicht rechtzeitig bereit war.")
continue
# In diesem try-Block wird der jeweilige Radio-Button für die Tarifwelt geklickt.
try:
tarifWeltRadio = wait.until(EC.element_to_be_clickable((By.XPATH, f'//input[@name="tarif_welt" and @value="{tarifWelt}"]')))
seleniumDriver.execute_script("arguments[0].click();", tarifWeltRadio)
except Exception as exception:
print(f"DEBUG: FEHLER beim Auswählen der Tarifwelt {tarifWelt}: {exception}. Überspringe...")
traceback.print_exc()
continue
# In dieser for-Schleife werden alle möglichen Netze durchlaufen.
for net in netzList:
# In dieser if-Abzweigung wird nochmals geprüft, ob das Dropdown bereit ist.
if not waitForDropdownReady(seleniumDriver, wait):
print(f"DEBUG: Überspringe Netz {net} in Tarifwelt {tarifWelt}, da die Seite nicht rechtzeitig bereit war.")
continue
# In diesem try-Block wird das jeweilige Netz geklickt.
try:
netRadio = wait.until(EC.element_to_be_clickable((By.XPATH, f'//input[@name="netz" and @value="{net}"]')))
seleniumDriver.execute_script("arguments[0].click();", netRadio)
except Exception as exception:
print(f"DEBUG: FEHLER beim Auswählen des Netzes {net} für Tarifwelt {tarifWelt}: {exception}. Überspringe...")
traceback.print_exc()
continue
# In dieser if-Abzweigung wird noch einmal die Verfügbarkeit des Tarif-Dropdowns geprüft.
if not waitForDropdownReady(seleniumDriver, wait):
print(f"DEBUG: Überspringe Netz {net} in Tarifwelt {tarifWelt}, da Tarif-Dropdown nicht bereit war.")
continue
# In diesem try-Block werden alle Tarifoptionen für das gegebene Netz ausgelesen.
try:
dropdown = wait.until(EC.presence_of_element_located((By.NAME, "tarif_id")))
selectObj = Select(dropdown)
optionsToProcess = [(opt.get_attribute("value"), opt.text.strip()) for opt in selectObj.options]
except Exception as exception:
print(f"DEBUG: FEHLER beim Sammeln der Tarifoptionen für Netz {net}, Tarifwelt {tarifWelt}: {exception}. Überspringe...")
traceback.print_exc()
continue
# In dieser for-Schleife werden alle Tarife aus dem Dropdown verarbeitet.
for tariffId, optText in optionsToProcess:
# In dieser if-Abzweigung werden Platzhalterwerte übersprungen.
if optText in ["Bitte wählen Sie aus...", ""] or not tariffId:
continue
# In dieser if-Abzweigung wird geprüft, ob der Tarif für diesen Rahmen schon erfasst wurde.
if (tariffId, currentFramework) in writtenPlanIdSet:
print(f"DEBUG: Tarif {tariffId} für Rahmen {currentFramework} bereits in CSV, überspringe.")
continue
print(f"DEBUG: Verarbeite: {tariffId} - {net} - {optText} (Rahmen {currentFramework})")
# In dieser if-Abzweigung wird geprüft, ob das Dropdown weiterhin verfügbar ist.
if not waitForDropdownReady(seleniumDriver, wait):
print(f"DEBUG: Überspringe Tarif {tariffId} ({optText}), da die Seite nicht rechtzeitig bereit war.")
continue
# In diesem try-Block wird der passende Tarif im Dropdown gewählt.
try:
currentDropdown = wait.until(EC.presence_of_element_located((By.NAME, "tarif_id")))
currentSelectObj = Select(currentDropdown)
currentOptionsValues = [o.get_attribute("value") for o in currentSelectObj.options]
if tariffId in currentOptionsValues:
currentSelectObj.select_by_value(tariffId)
else:
print(f"DEBUG: Warnung: Option mit Wert '{tariffId}' ({optText}) nicht mehr im Dropdown gefunden. Überspringe...")
continue
except Exception as exception:
print(f"DEBUG: Fehler beim Auswählen von Option '{optText}' für Tarif {tariffId}: {exception}")
traceback.print_exc()
continue
# In diesem try-Block wird gewartet, bis das Overlay verschwindet.
try:
WebDriverWait(seleniumDriver, timeout=60).until(EC.invisibility_of_element_located((By.ID, "bg_layer")))
except TimeoutException:
print(f"DEBUG: FEHLER: Timeout beim Warten auf bg_layer nach Auswahl von Tarif {tariffId}. Überspringe...")
continue
# In dieser Zeile wird kurz gewartet, um die Preisanzeige stabil zu laden.
time.sleep(1.5)
# In dieser Variablen wird der Nettopreis des aktuell ausgewählten Tarifs erfasst.
planPriceNet = parsePlanPrice(seleniumDriver)
# In dieser Variablen wird die Liste aller verfügbaren Kampagnen erfasst.
campaigns = parseCampaigns(seleniumDriver)
# In dieser Zeile wird der Tarif in die plans.csv geschrieben.
plansWriter.writerow([
tariffId,
tarifWelt,
net,
optText,
planPriceNet,
currentFramework
])
# In dieser Zeile wird der Tarif als bereits erfasst markiert.
writtenPlanIdSet.add((tariffId, currentFramework))
# In dieser for-Schleife werden alle Kampagnen in die campaigns.csv geschrieben.
for (campId, campName) in campaigns:
campaignsWriter.writerow([
campId,
tariffId,
campName
])
# In diesen Variablen werden die URLs für PDF-Dokumente abgeleitet.
flyerPdfUrl = f"https://maui.mobilcom.de/vertragserfassung/show_pib_flyer.php?variant_id={tariffId}"
pibPdfUrl = flyerPdfUrl + "&pib"
# In diesem try-Block werden die PDFs heruntergeladen.
try:
downloadPdfSelenium(seleniumDriver, flyerPdfUrl, cacheDir, f"{tariffId}_flyer.pdf")
downloadPdfSelenium(seleniumDriver, pibPdfUrl, cacheDir, f"{tariffId}_pib.pdf")
except Exception as exception:
print(f"DEBUG: Fehler beim PDF-Download für Tarif {tariffId}: {exception}")
# In dieser Variablen wird gespeichert, ob zur Optionsseite navigiert werden konnte.
navigationToOptionsSuccessful = False
# In diesem try-Block wird versucht, zur Optionsseite zu navigieren.
try:
print(f"DEBUG: Versuche zur Optionsseite zu navigieren für Tarif {tariffId}...")
wait.until(EC.presence_of_element_located((By.NAME, "mobildaten")))
seleniumDriver.execute_script("send_form(document.mobildaten, 'sim')")
navigationToOptionsSuccessful = True
print(f"DEBUG: Navigation zur Optionsseite vermutlich erfolgreich für Tarif {tariffId}.")
time.sleep(2)
except Exception as exception:
print(f"DEBUG: Fehler beim Navigieren zur Optionsseite für Tarif {tariffId}: {exception}")
traceback.print_exc()
continue
# In dieser if-Abzweigung wird geprüft, ob die Navigation erfolgreich war.
if navigationToOptionsSuccessful:
try:
print(f"DEBUG: Rufe scrapeOption für Tarif {tariffId} auf.")
scrapeOption(
seleniumManager,
tariffId,
optionsWriter,
categorysWriter
)
except Exception as exception:
print(f"DEBUG: Fehler während scrapeOption für Tarif {tariffId}: {exception}")
traceback.print_exc()
# In dieser Zeile werden die CSV-Dateien nach jedem Tarif zwischengespeichert.
print(f"DEBUG: Flushe CSV-Dateien nach Verarbeitung von Tarif {tariffId}.")
plansFile.flush()
campaignsFile.flush()
optionsFile.flush()
categorysFile.flush()
# In dieser Funktion werden die Optionsdaten eines Tarifs auf der Optionsseite extrahiert.
def scrapeOption(seleniumManager, tariffId, optionsWriter, categorysWriter):
# In dieser Debug-Ausgabe wird mitgeteilt, dass das Scraping der Option gestartet wurde.
print(f"DEBUG: scrapeOption gestartet für Tarif {tariffId}.")
# In dieser Variablen wird auf den im SeleniumManager gespeicherten Driver zugegriffen.
seleniumDriver = seleniumManager.driver
# In dieser Variablen wird ein WebDriverWait-Objekt mit 20 Sekunden Timeout angelegt.
wait = WebDriverWait(seleniumDriver, 20)
# In diesem try-Block findet das eigentliche Parsing der Optionsseite statt.
try:
# In dieser Zeile wird bis zu 60 Sekunden auf das Verschwinden eines Overlays gewartet.
print(f"DEBUG: Warte auf Unsichtbarkeit von bg_layer für Tarif {tariffId}.")
WebDriverWait(seleniumDriver, timeout=60).until(EC.invisibility_of_element_located((By.ID, "bg_layer")))
# In dieser Zeile wird darauf gewartet, dass das Formular 'tarifoptionen' im DOM vorhanden ist.
print(f"DEBUG: Warte auf Formular 'tarifoptionen' für Tarif {tariffId}.")
wait.until(EC.presence_of_element_located((By.NAME, "tarifoptionen")))
# In dieser Zeile wird geprüft, ob mindestens eine Tabelle mit Klasse 'tb_back' vorhanden ist.
print(f"DEBUG: Warte auf Klasse 'tb_back' für Tarif {tariffId}.")
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "tb_back")))
print(f"DEBUG: Optionsseite für Tarif {tariffId} scheint geladen zu sein.")
# In dieser Variablen wird der komplette HTML-Inhalt gespeichert.
htmlContent = seleniumDriver.page_source
# In dieser Variablen wird ein BeautifulSoup-Objekt erstellt, um den Inhalt zu parsen.
soupObj = BeautifulSoup(htmlContent, "html.parser")
print(f"DEBUG: Rufe parse_options für Tarif {tariffId} auf.")
optionsData, categoryData = parseOptions(soupObj)
print(f"DEBUG: parse_options fand {len(optionsData)} Optionen und {len(categoryData)} Kategorien für Tarif {tariffId}.")
# In dieser Variablen wird gezählt, wie viele neue Kategorien geschrieben werden.
catsWritten = 0
# In dieser for-Schleife werden alle Kategorien verarbeitet.
for catLine in categoryData:
parts = catLine.split(";", 1)
if len(parts) == 2:
catId = parts[0]
catName = parts[1]
global uniqueCategorySet
if catId not in uniqueCategorySet:
categorysWriter.writerow([catId, catName])
uniqueCategorySet.add(catId)
catsWritten += 1
print(f"DEBUG: {catsWritten} neue Kategorien in CSV geschrieben für Tarif {tariffId}.")
# In dieser Variablen wird gezählt, wie viele Optionen geschrieben werden.
optsWritten = 0
# In dieser for-Schleife werden die ermittelten Optionszeilen verarbeitet.
for line in optionsData:
parts = line.split(";", 3)
if len(parts) == 4:
categoryRefId = parts[0]
itemId = parts[1]
itemName = parts[2]
priceStr = parts[3]
# In diesem try-Block wird der Preis als float konvertiert.
try:
grossPrice = float(priceStr)
except ValueError:
grossPrice = 0.0
# In dieser Zeile wird der Nettopreis auf Basis von 19% MwSt. berechnet.
netPrice = round(grossPrice / 1.19, 5)
optionsWriter.writerow([
itemId,
categoryRefId,
tariffId,
itemName,
netPrice
])
optsWritten += 1
print(f"DEBUG: {optsWritten} Optionen in CSV geschrieben für Tarif {tariffId}.")
# In dieser except-Abzweigung werden Fehler während des Parsings protokolliert.
except Exception as exception:
print(f"FEHLER in scrapeOption (Parsing/Writing) für Tarif {tariffId}: {exception}")
traceback.print_exc()
# In diesem finally-Block wird versucht, auf die Hauptseite (Mobildaten) zurück zu navigieren.
finally:
# In diesen Variablen werden die Anzahl der Versuche und die Pausenzeit definiert.
maxRetriesNav = 2
retryDelayNav = 3
backNavSuccessful = False
# In dieser for-Schleife werden mehrere Versuche zum Rücksprung in die Hauptseite durchgeführt.
for attempt in range(maxRetriesNav):
try:
print(f"DEBUG: Versuche zurückzunavigieren von Optionsseite für Tarif {tariffId} (Versuch {attempt + 1}/{maxRetriesNav}).")
time.sleep(0.5)
WebDriverWait(seleniumDriver, 15).until(EC.presence_of_element_located((By.NAME, "tarifoptionen")))
seleniumDriver.execute_script("jump_2_container('Mobildaten')")
time.sleep(1.5)
WebDriverWait(seleniumDriver, 15).until(EC.presence_of_element_located((By.NAME, "tarif_id")))
backNavSuccessful = True
print(f"DEBUG: Rücknavigation von Optionsseite erfolgreich für Tarif {tariffId} (Versuch {attempt + 1}).")
break
except Exception as exception:
print(f"DEBUG: Warnung: Versuch {attempt + 1}/{maxRetriesNav} der Rücknavigation fehlgeschlagen für Tarif {tariffId}: {exception}")
if attempt < maxRetriesNav - 1:
time.sleep(retryDelayNav)
else:
print(f"DEBUG: Endgültige Warnung: Konnte nach {maxRetriesNav} Versuchen nicht von Tarif {tariffId} zurücknavigieren.")
# In dieser if-Abfrage wird protokolliert, falls die Rücknavigation nicht geklappt hat.
if not backNavSuccessful:
print(f"DEBUG: Rücknavigation von Tarif {tariffId} war nicht erfolgreich. Fortsetzung kann instabil sein.")
# In dieser Funktion werden die Optionen und Kategorien im HTML-Dokument geparst und aufbereitet.
def parseOptions(soupObj):
# In dieser Liste werden alle gefundenen Optionen gespeichert.
optionsResults = []
# In dieser Liste werden alle gefundenen Kategorien gespeichert.
categoryResults = []
# In diesem Set werden Kategorien gesammelt, die schon hinzugefügt wurden, um Duplikate zu vermeiden.
collectedCategoryIds = set()
# In dieser Variablen wird ein RegEx für die Prüfung von Gruppenfeldern definiert.
categoryCheckPattern = re.compile(r'service_code\[(G\d+)_check\]')
# In dieser Variablen wird ein RegEx für versteckte Gruppenfelder definiert.
categoryHiddenPattern = re.compile(r'service_code\[(G\d+)_check\]')
# In dieser Variablen wird ein RegEx für Radio-Buttons in Gruppenfeldern definiert.
categoryRadioPattern = re.compile(r'service_code\[(G\d+)\]')
# In dieser Variablen wird ein RegEx für Item-IDs definiert, die mit G oder O beginnen.
itemValuePattern = re.compile(r'^(G\d+|O\d+)$')
# In dieser Variablen wird ein RegEx für Preise definiert, um Beträge im Text zu erkennen.
pricePattern = re.compile(r'/\s*€\s*([\d.,]+)\s*monatlich', re.IGNORECASE)
# In dieser Variablen wird ein RegEx definiert, um Sub-Selects zu erkennen.
subSelectPattern = re.compile(r"service_code\[(G\d+)_S\d+\]")
# In dieser Variablen werden alle Haupttabellen mit Klasse 'tb_back' gesucht.
allPotentialMainTables = soupObj.find_all("table", class_="tb_back")
print(f"DEBUG: parse_options: {len(allPotentialMainTables)} potenzielle Haupttabellen (tb_back) gefunden.")
# In dieser for-Schleife wird jede gefundene Tabelle untersucht.
for tbl in allPotentialMainTables:
catNameEl = tbl.find("td", class_="tb_head")
if not catNameEl:
continue
catTextRaw = catNameEl.get_text(strip=True)
catText = re.sub(r'\s+', ' ', catTextRaw.replace('\xa0', ' ')).strip()
if not catText:
continue
categoryId = None
catInputCheck = tbl.find("input", attrs={"name": categoryCheckPattern})
catInputHidden = tbl.find("input", type="hidden", attrs={"name": categoryHiddenPattern})
catInputRadio = tbl.find("input", type="radio", attrs={"name": categoryRadioPattern})
# In dieser if-Abfolge wird geprüft, welche Kategorie-ID wir aus welcher Input-Variante ziehen können.
if catInputCheck:
matchCheck = categoryCheckPattern.search(catInputCheck.get("name", ""))
if matchCheck:
categoryId = matchCheck.group(1)
elif catInputHidden:
matchHidden = categoryHiddenPattern.search(catInputHidden.get("name", ""))
if matchHidden:
categoryId = matchHidden.group(1)
elif catInputRadio:
matchRadio = categoryRadioPattern.search(catInputRadio.get("name", ""))
if matchRadio:
categoryId = matchRadio.group(1)
# In dieser if-Abzweigung werden irrelevante oder unbekannte Gruppen ausgeschlossen.
if not categoryId or catText in ["Sonstige Angaben", "Pflicht-Angaben"]:
continue
print(f"DEBUG: Verarbeite Optionsgruppe: {categoryId} - {catText}")
# In dieser if-Abzweigung wird die Kategorie einmalig in die categoryResults aufgenommen.
if categoryId not in collectedCategoryIds:
categoryResults.append(f"{categoryId};{catText}")
collectedCategoryIds.add(categoryId)
# In dieser Variablen werden mögliche Untertabellen gesucht.
subTables = tbl.find_all("table", {"border": "0", "width": "520", "cellspacing": "0", "cellpadding": "4"})
if not subTables:
subTables = [tbl]
lastGId = None
# In dieser for-Schleife werden die Untertabellen untersucht.
for subTbl in subTables:
inp = subTbl.find("input", attrs={"value": itemValuePattern})
subSelect = subTbl.find("select", attrs={"name": subSelectPattern})
# In dieser if-Abzweigung wird geprüft, ob ein passendes Input-Feld gefunden wurde.
if inp:
itemId = inp.get("value", "").strip()
if not itemId:
continue
itemLabelTag = subTbl.find("a", attrs={"id": f"err_{itemId}"})
if not itemLabelTag:
itemLabelTag = subTbl.find("a", attrs={"name": f"err_{itemId}"})
itemName = "Unbekannt"
# In dieser if-Abzweigung wird der Text des Label-Tags als Name verwendet, falls vorhanden.
if itemLabelTag and itemLabelTag.text.strip():
itemName = re.sub(r'\s+', ' ', itemLabelTag.text.strip())
else:
divBlock = subTbl.find("div", {"name": f"{itemId}_block"})
if divBlock:
linkInDiv = divBlock.find("a")
if linkInDiv and linkInDiv.text.strip():
itemName = re.sub(r'\s+', ' ', linkInDiv.text.strip())
# In dieser if-Abzweigung wird fortgefahren, wenn kein Name ermittelt werden kann.
if itemName == "Unbekannt":
continue
combinedText = subTbl.get_text(" ", strip=True)
mPrice = pricePattern.search(combinedText)
priceStr = "0.0"
# In dieser if-Abzweigung wird ein gefundener Preis verarbeitet.
if mPrice:
rawPrice = mPrice.group(1)
normalized = rawPrice.replace(".", "").replace(",", ".")
try:
priceVal = float(normalized)
priceStr = f"{priceVal}"
except ValueError:
priceStr = "0.0"
# In dieser if-Abzweigung wird unterschieden, ob wir eine Gruppen-ID oder eine normale Option haben.
if itemId.startswith("G"):
optionsResults.append(f"{categoryId};{itemId};{itemName};{priceStr}")
lastGId = itemId
if itemId not in collectedCategoryIds:
categoryResults.append(f"{itemId};{itemName}")
collectedCategoryIds.add(itemId)
elif itemId.startswith("O"):
optionsResults.append(f"{categoryId};{itemId};{itemName};{priceStr}")
lastGId = None
# In dieser if-Abzweigung wird geprüft, ob wir ein Sub-Select haben und zuletzt eine Gruppen-ID gespeichert wurde.
if subSelect and lastGId:
optionTags = subSelect.find_all("option", attrs={"value": re.compile(r"^O\d+$")})
for optTag in optionTags:
optId = optTag.get("value", "").strip()
if not optId:
continue
optText = optTag.get_text(strip=True)
if not optText or optText == "Bitte wählen Sie aus...":
continue
optPriceMatch = pricePattern.search(optText)
optPriceStr = "0.0"
if optPriceMatch:
rawPrice = optPriceMatch.group(1)
normalized = rawPrice.replace(".", "").replace(",", ".")
try:
priceVal = float(normalized)
optPriceStr = f"{priceVal}"
except ValueError:
optPriceStr = "0.0"
optText = pricePattern.sub('', optText).strip()
optText = re.sub(r'\s+/\s*$', '', optText).strip()
optionsResults.append(f"{lastGId};{optId};{optText};{optPriceStr}")
lastGId = None
# In dieser Liste werden doppelte Einträge entfernt.
uniqueOptions = list(set(optionsResults))
uniqueCategoriesList = list(set(categoryResults))
print(f"DEBUG: parse_options: Gibt {len(uniqueOptions)} eindeutige Optionen und {len(uniqueCategoriesList)} eindeutige Kategorien zurück.")
return uniqueOptions, uniqueCategoriesList
# In dieser Funktion wird geprüft, ob eine bestimmte Gruppen-ID ein Sub-Select besitzt.
def hasSubSelectForId(gId, subSelects):
# In dieser for-Schleife werden alle Select-Elemente geprüft.
for s in subSelects:
sName = s.get("name", "")
if gId in sName:
return True
return False
# In dieser Variablen wird ein SeleniumManager mit sichtbarem Browserfenster und gegebenem Geckodriver-Pfad initialisiert.
seleniumManager = SeleniumManager()
# In dieser Zeile wird der Login mit den globalen Zugangsdaten durchgeführt.
login(seleniumManager, MAUI_USERNAME, MAUI_PASSWORD, MAUI_AUTHCODE)
# In dieser Zeile wird die Laufzeitvertrags-Seite geöffnet.
openLaufzeitvertrag(seleniumManager)
# In dieser Zeile wird eine kurze Wartezeit eingefügt.
time.sleep(5)
# In dieser Zeile werden alle Daten gescraped und in CSV-Dateien geschrieben.
scrapeData(seleniumManager)
# In dieser Zeile wird eine letzte Wartezeit eingebaut, bevor der Browser geschlossen wird.
time.sleep(10)
# In dieser Zeile wird der WebDriver schließlich geschlossen.
seleniumManager.closeDriver()
# In dieser Ausgabe wird signalisiert, dass das Scraping abgeschlossen ist.
print("Scraping abgeschlossen.")
import sys; sys.path.append("..")
import os
import csv
import datetime
import logging
from decimal import Decimal
from collections import defaultdict
from sqlalchemy.dialects.mysql import insert as mysql_insert
from manager.MysqlManager import MysqlManager
from models.base_base import BaseBase
from models.deal_deal import DealDeal
from models.option_opti import OptionOpti
#
# Hier wird die Log-Konfiguration festgelegt, damit während des Ablaufs aussagekräftige Zeit- und Fehlermeldungen ausgegeben werden.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s"
)
#
# Dieses Verzeichnis verweist auf den Zwischenspeicher, in dem alle CSV-Dateien abgelegt sind.
CSV_DIR = os.path.join("..", "cache")
#
# Hier werden die einzelnen CSV-Dateien innerhalb des Zwischenspeichers definiert.
csvFileCategories = os.path.join(CSV_DIR, "categorys.csv")
csvFilePlans = os.path.join(CSV_DIR, "plans.csv")
csvFileCampaigns = os.path.join(CSV_DIR, "campaigns.csv")
csvFileOptions = os.path.join(CSV_DIR, "options.csv")
#
# Diese Funktion liest eine CSV-Datei vollständig ein und liefert jede Zeile als Wörterbuch zurück.
def read_csv(path):
with open(path, newline="", encoding="utf-8") as f:
return list(csv.DictReader(f, delimiter=";"))
#
# Hier werden sämtliche CSV-Dateien in Listen von Wörterbüchern eingelesen.
cat_rows = read_csv(csvFileCategories)
plan_rows = read_csv(csvFilePlans)
camp_rows = read_csv(csvFileCampaigns)
opt_rows = read_csv(csvFileOptions)
#
# Dieses Wörterbuch ordnet jeder Kategorietabelle den zugehörigen Namen für späteres Nachschlagen zu.
category_name = {r["id"].strip(): r["name"].strip() for r in cat_rows}
#
# Diese Datenstruktur ordnet jeder Plan-ID alle zugehörigen Kampagnenzeilen zu, um schnellen Zugriff zu ermöglichen.
campaigns_by_plan = defaultdict(list)
for c in camp_rows:
campaigns_by_plan[c["plan"].strip()].append(c)
#
# Diese Datenstruktur ordnet jeder Plan-ID alle zugehörigen Optionszeilen zu, um schnellen Zugriff zu ermöglichen.
options_by_plan = defaultdict(list)
for o in opt_rows:
options_by_plan[o["plan"].strip()].append(o)
#
# Hier wird die Verbindung zur Datenbank geöffnet und eine neue Session erzeugt.
mysql = MysqlManager()
session = mysql.getSession()
#
# Dieses Wörterbuch enthält alle bestehenden Basiseinträge, damit später neue Einträge erkannt werden können.
base_db = {(b.provider_base, b.providercode_base): b
for b in session.query(BaseBase).all()}
#
# Diese verschachtelte Struktur hält alle vorhandenen Deals pro Base-ID, wodurch ein schneller Abgleich ermöglicht wird.
deals_db = defaultdict(dict)
for d in session.query(DealDeal).all():
deals_db[d.base_deal][d.providercode_deal] = d
#
# Diese verschachtelte Struktur hält alle vorhandenen Optionen pro Base-ID, um später Stop- und Reaktivierungslogik anzuwenden.
opts_db = defaultdict(dict)
for o in session.query(OptionOpti).all():
opts_db[o.base_opti][o.providercode_opti] = o
#
# Hier wird der aktuelle Zeitpunkt einmalig festgelegt, um ihn konsistent für alle neu erzeugten Datensätze zu verwenden.
now = datetime.datetime.now()
#
# Diese Liste sammelt alle neu anzulegenden Basiseinträge, damit sie in einem Schritt geschrieben werden können.
new_bases = []
for p in plan_rows:
prov_base = f"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}"
key = (prov_base, p["id"].strip())
if key not in base_db:
b = BaseBase(
provider_base = prov_base,
providercode_base = p["id"].strip(),
name_base = p["name"].strip(),
created_base = now,
updated_base = now
)
new_bases.append(b)
base_db[key] = b
#
# Hier werden alle neu erkannten Basiseinträge in einem einzigen Datenbankvorgang gespeichert.
if new_bases:
session.add_all(new_bases)
session.flush()
logging.info("Inserted %d new bases", len(new_bases))
#
# Diese verschachtelten Mengen erfassen für jede Base-ID die in diesem Lauf gewünschten Deals und Optionen.
desired_deals = defaultdict(set)
desired_opts = defaultdict(set)
#
# Diese Listen sammeln alle Datensätze, die per INSERT IGNORE neu geschrieben oder aktualisiert werden sollen.
deal_rows_insert = []
opt_rows_insert = []
for p in plan_rows:
prov_base = f"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}"
base_obj = base_db[(prov_base, p["id"].strip())]
b_id = base_obj.id_base
price = Decimal(p["price"].strip() or "0.00")
#
# Dieser Block fügt den obligatorischen Standard-Deal ohne Kampagnenkennung hinzu.
desired_deals[b_id].add("")
deal_rows_insert.append({
"provisiongroup_deal": 1,
"base_deal": b_id,
"providercode_deal": "",
"name_deal": "",
"price_deal": price,
"starts_deal": now,
"stops_deal": None,
"created_deal": now,
"updated_deal": now
})
#
# Dieser Block verarbeitet alle Kampagnen zum aktuellen Plan und fügt sie der Wunschliste hinzu.
for c in campaigns_by_plan[p["id"].strip()]:
code = f"A{c['id'].strip()}"
desired_deals[b_id].add(code)
deal_rows_insert.append({
"provisiongroup_deal": 1,
"base_deal": b_id,
"providercode_deal": code,
"name_deal": c["name"].strip(),
"price_deal": price,
"starts_deal": now,
"stops_deal": None,
"created_deal": now,
"updated_deal": now
})
#
# Dieser Block fügt alle Optionen zum aktuellen Plan der Wunschliste hinzu und bereitet die Insert-Zeilen vor.
for o in options_by_plan[p["id"].strip()]:
code_opt = o["id"].strip()
desired_opts[b_id].add(code_opt)
opt_rows_insert.append({
"provisiongroup_opti": 1,
"base_opti": b_id,
"providercode_opti": code_opt,
"providercategory_opti": o["category"].strip(),
"name_opti": f"{category_name.get(o['category'].strip(),'Unbekannt')} | {o['name'].strip()}",
"alias_opti": None,
"price_opti": Decimal(o["price"].strip() or "0.00"),
"starts_opti": now,
"stops_opti": None,
"provision1_opti": Decimal("0.00000"),
"provision2_opti": Decimal("0.00000"),
"provision3_opti": Decimal("0.00000"),
"provision4_opti": Decimal("0.00000"),
"created_opti": now,
"updated_opti": now
})
#
# In diesem Schritt werden doppelte Deal- und Optionszeilen anhand ihrer Schlüsselwerte entfernt.
deal_rows_insert = list({(r["base_deal"], r["providercode_deal"]): r for r in deal_rows_insert}.values())
opt_rows_insert = list({(r["base_opti"], r["providercode_opti"]): r for r in opt_rows_insert }.values())
#
# Diese Listen sammeln Datensätze, deren Status auf gestoppt oder reaktiviert gesetzt werden muss.
stop_deals, react_deals = [], []
stop_opts, react_opts = [], []
for (prov, _), b in base_db.items():
if not prov.startswith("Freenet"):
continue
b_id = b.id_base
wantD = desired_deals.get(b_id, set())
haveD = deals_db.get(b_id, {})
for code, obj in haveD.items():
if code in wantD and obj.stops_deal is not None:
react_deals.append({"id_deal": obj.id_deal,
"stops_deal": None,
"updated_deal": now})
if code not in wantD and obj.stops_deal is None:
stop_deals.append({"id_deal": obj.id_deal,
"stops_deal": now,
"updated_deal": now})
wantO = desired_opts.get(b_id, set())
haveO = opts_db.get(b_id, {})
for code, obj in haveO.items():
if code in wantO and obj.stops_opti is not None:
react_opts.append({"id_opti": obj.id_opti,
"stops_opti": None,
"updated_opti": now})
if code not in wantO and obj.stops_opti is None:
stop_opts.append({"id_opti": obj.id_opti,
"stops_opti": now,
"updated_opti": now})
#
# Dieser Block schreibt alle gewünschten Deals per INSERT IGNORE in die Datenbank.
session.execute(
mysql_insert(DealDeal.__table__).prefix_with("IGNORE"),
deal_rows_insert
)
logging.info("INSERT IGNORE'd %d deals", len(deal_rows_insert))
#
# Dieser Block schreibt alle gewünschten Optionen per INSERT IGNORE in die Datenbank.
session.execute(
mysql_insert(OptionOpti.__table__).prefix_with("IGNORE"),
opt_rows_insert
)
logging.info("INSERT IGNORE'd %d options", len(opt_rows_insert))
#
# Dieser Block aktualisiert alle Deals, die jetzt gestoppt werden müssen.
if stop_deals:
session.bulk_update_mappings(DealDeal, stop_deals)
logging.info("Stopped %d deals", len(stop_deals))
#
# Dieser Block aktualisiert alle Deals, die wieder reaktiviert werden müssen.
if react_deals:
session.bulk_update_mappings(DealDeal, react_deals)
logging.info("Reactivated %d deals", len(react_deals))
#
# Dieser Block aktualisiert alle Optionen, die jetzt gestoppt werden müssen.
if stop_opts:
session.bulk_update_mappings(OptionOpti, stop_opts)
logging.info("Stopped %d options", len(stop_opts))
#
# Dieser Block aktualisiert alle Optionen, die wieder reaktiviert werden müssen.
if react_opts:
session.bulk_update_mappings(OptionOpti, react_opts)
logging.info("Reactivated %d options", len(react_opts))
#
# Hier werden sämtliche Änderungen dauerhaft in der Datenbank gespeichert.
session.commit()
#
# Zum Abschluss wird die Session geschlossen, um Ressourcen freizugeben.
session.close()
logging.info("Import-Lauf abgeschlossen.")
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
sys.path.append("..")
import os
import datetime
from manager.S3Manager import S3Manager
from manager.MysqlManager import MysqlManager
from models.base_base import BaseBase
from models.deal_deal import DealDeal
from models.option_opti import OptionOpti
from models.provisiongroup_pgro import ProvisiongroupPgro # zwingend, um Abhängigkeits-Mapping zu initialisieren
#
# Dieses Verzeichnis enthält sämtliche PDF-Dateien für den Upload.
cacheDir = "../cache"
#
# Dieses Objekt übernimmt das Hochladen der Dateien in den S3-Bucket und liefert die endgültige URL.
s3Manager = S3Manager()
#
# Diese Datenbank-Session ermöglicht Abfragen und Aktualisierungen innerhalb der MySQL-Datenbank.
dbSession = MysqlManager().getSession()
#
# Diese Liste sammelt alle PDF-Dateinamen im Cache-Verzeichnis.
pdfFiles = [f for f in os.listdir(cacheDir) if f.lower().endswith(".pdf")]
#
# Diese Menge speichert alle eindeutigen Basis-IDs, die durch Suffix-Prüfung ermittelt wurden.
pdfIdSet = set()
for name in pdfFiles:
stem = name[:-4].lower()
if stem.endswith("_flyer"):
pdfIdSet.add(stem[:-6])
elif stem.endswith("_pib"):
pdfIdSet.add(stem[:-4])
#
# Dieser Block beendet das Skript, wenn keine geeigneten PDF-Dateien vorhanden sind.
if not pdfIdSet:
print(f"INFO: Keine PDF-Paare in '{cacheDir}' gefunden.")
dbSession.close()
sys.exit(0)
#
# Diese Schleife verarbeitet jede erkannte Basis-ID in sortierter Reihenfolge.
for currentId in sorted(pdfIdSet):
print(f"\n--- Verarbeitung ID: {currentId} ---")
#
# Diese Abfrage liefert alle BaseBase-Datensätze, um Mehrfachtreffer sicher zu unterstützen.
baseRecords = dbSession.query(BaseBase).filter_by(providercode_base=currentId).all()
if not baseRecords:
print(f"WARNUNG: Kein BaseBase-Eintrag für providercode_base='{currentId}'.")
continue
#
# Dieser Pfad verweist auf die potenzielle Flyer-Datei der aktuellen Basis-ID.
flyerPath = os.path.join(cacheDir, f"{currentId}_flyer.pdf")
#
# Dieser Pfad verweist auf die potenzielle PIB-Datei der aktuellen Basis-ID.
pibPath = os.path.join(cacheDir, f"{currentId}_pib.pdf")
#
# Diese Variable hält die hochgeladene Flyer-URL oder bleibt None, falls kein Upload erfolgte.
flyerUrl = None
if os.path.exists(flyerPath):
flyerKey = f"flyers/{currentId}_flyer.pdf"
flyerUrl = s3Manager.uploadFile(flyerPath, flyerKey)
if not flyerUrl:
print(f"FEHLER: Flyer-Upload fehlgeschlagen für ID {currentId}")
#
# Diese Variable hält die hochgeladene PIB-URL oder bleibt None, falls kein Upload erfolgte.
pibUrl = None
if os.path.exists(pibPath):
pibKey = f"pibs/{currentId}_pib.pdf"
pibUrl = s3Manager.uploadFile(pibPath, pibKey)
if not pibUrl:
print(f"FEHLER: PIB-Upload fehlgeschlagen für ID {currentId}")
#
# Diese Schleife aktualisiert jede gefundene Base-Zeile, um Flyer- und PIB-URLs konsistent zu setzen.
for base in baseRecords:
if base.flyerurl_base is None and flyerUrl:
base.flyerurl_base = flyerUrl
base.updated_base = datetime.datetime.now()
print(f"INFO: flyerurl_base gesetzt: {flyerUrl}")
if base.piburl_base is None and pibUrl:
base.piburl_base = pibUrl
base.updated_base = datetime.datetime.now()
print(f"INFO: piburl_base gesetzt: {pibUrl}")
#
# Dieser Aufruf speichert alle Änderungen für die aktuelle Basis-ID atomar in der Datenbank.
dbSession.commit()
#
# Hier wird die Session geschlossen, sobald alle Basis-IDs verarbeitet wurden.
dbSession.close()
#
# Diese Meldung bestätigt das erfolgreiche Ende des gesamten Upload-Vorgangs.
print("INFO: Upload-Vorgang abgeschlossen.")
secret = "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"
secret = "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"
0 4 * * * /maui/cron.sh downloadDataFromMaui.py
30 5 * * * /maui/cron.sh importCacheToDatabase.py
0 6 * * * /maui/cron.sh uploadCacheToAwsS3.py
0 6 * * * /maui/cron.sh calculateTarifDetailsWithGpt.py
#!/bin/bash
# Dieser Wrapper wechselt ins Verzeichnis /maui/commands und startet das
# gewünschte Python-Skript (mit python3), sofern nicht bereits eine Instanz
# dieses Skripts läuft. Gleichzeitig werden alle Ausgaben in zwei getrennten
# Logfiles im Verzeichnis /maui/logs abgelegt, wobei jedes Skript einen
# eigenen Unterordner erhält (benannt nach dem Skriptnamen ohne Erweiterung)
# und die Logfiles die Namen im Format
# - L_yyyymmdd-hhiiss.txt für die Standardausgabe,
# - E_yyyymmdd-hhiiss.err für die Fehlermeldung
# tragen. Logfiles, die älter als 24 Stunden (1440 Minuten) sind, werden
# automatisch gelöscht.
#--- Parameterprüfung ---
# In dieser Abfrage wird überprüft, ob mindestens ein Parameter übergeben wurde.
if [ "$#" -lt 1 ]; then
# Hier wird ein Hinweis ausgegeben, wie dieses Skript zu nutzen ist, wenn nicht
# genügend Parameter übergeben wurden.
echo "Usage: $0 <jobfilename> [arguments...]"
# Dieser Befehl beendet das Skript mit einem Fehlercode.
exit 1
fi
# Diese Variable speichert den ersten übergebenen Parameter als Namen des
# Python-Skripts.
jobname="$1"
# Dieser Befehl entfernt den ersten Parameter aus der Parameterliste, damit
# weitere Argumente optional weiterverarbeitet werden können.
shift
#--- Arbeitsverzeichnis und Log-Verzeichnis festlegen ---
# Diese Variable legt das Arbeitsverzeichnis fest, in dem sich die
# Python-Skripte befinden.
WORKDIR="/maui/commands"
# Diese Variable legt das Hauptverzeichnis für die Logdateien fest.
LOG_ROOT="/maui/logs"
# Diese Variable ermittelt aus dem übergebenen Skriptnamen
# (z. B. rawFromBloomberg.py) den Basisteil (rawFromBloomberg).
job_base=$(basename "$jobname" .py)
# Diese Variable bildet den Pfad für das individuellen Logverzeichnis, basierend
# auf dem Basisteil des Skriptnamens.
LOG_DIR="$LOG_ROOT/$job_base"
# Dieser Befehl stellt sicher, dass das Haupt-Logverzeichnis und das Verzeichnis
# für das aktuelle Skript existieren, und legt sie gegebenenfalls an.
mkdir -p "$LOG_DIR"
# Dieser Befehl findet und löscht alle Logdateien im spezifischen Verzeichnis,
# die älter als 24 Stunden (1440 Minuten) sind.
find "$LOG_DIR" -type f -mmin +1440 -delete
#--- Prozessüberprüfung ---
# Diese Variable speichert die Prozess-ID des aktuell ausgeführten Skripts,
# damit es sich nicht selbst erkennt.
current_pid=$$
# Diese Variable hält den Namen dieses Wrapperskripts (cron.sh), um ihn ebenfalls
# von der Prozessliste auszuschließen.
wrapper_name=$(basename "$0")
# Diese Variable legt fest, nach welchem exakten Aufrufmuster
# (python3 <jobname>) in der Prozessliste gesucht werden soll.
pattern="python3 $jobname"
# In dieser Variable werden alle zum Muster passenden Prozess-IDs gespeichert,
# wobei Zeilen des Wrappers und greps ausgeschlossen werden.
running=$(ps ax -o pid,cmd | grep "$pattern" | grep -v grep | grep -v "$wrapper_name" | awk '{print $1}')
# Diese Abfrage prüft, ob ein passender Prozess bereits läuft.
if [ -n "$running" ]; then
# Hier wird der Nutzer informiert, dass der entsprechende Job bereits ausgeführt
# wird, und ein erneuter Start verhindert.
echo "Job '$jobname' läuft bereits (PID(s): $running). Abbruch."
# Das Skript wird hier mit Exit-Code 0 (ohne Fehler) beendet, um keine neue
# Instanz zu starten.
exit 0
fi
#--- Logging vorbereiten und Job starten ---
# Diese Variable erzeugt einen Zeitstempel im Format yyyymmdd-hhiiss
# (z. B. 20250413-114530), um eindeutige Logdateien zu erstellen.
timestamp=$(date "+%Y%m%d-%H%M%S")
# Diese Variable bildet den vollständigen Pfad zur Logdatei für die Standardausgabe.
STDOUT_LOG="$LOG_DIR/L_${timestamp}.txt"
# Diese Variable bildet den vollständigen Pfad zur Logdatei für die Fehlermeldungen.
ERROR_LOG="$LOG_DIR/E_${timestamp}.err"
# Dieser Befehl wechselt in das festgelegte Arbeitsverzeichnis oder bricht mit
# Fehlermeldung ab, falls es nicht erreichbar ist.
cd "$WORKDIR" || { echo "Arbeitsverzeichnis $WORKDIR nicht erreichbar." >&2; exit 1; }
# Dieser Befehl führt das Python-Skript aus und leitet stdout in das L_-Logfile
# und stderr in das E_-Logfile um.
python3 "$jobname" "$@" > "$STDOUT_LOG" 2> "$ERROR_LOG"
# Fehler senden bei Inhalt: Jobname, Zeilenumbruch, Fehler
if [ -s "$ERROR_LOG" ]; then
payload="$jobname
$(<"$ERROR_LOG")"
curl -s -X POST https://ntfy.sh/itmaxDebug -d "$payload"
fi
\ No newline at end of file
import sys
sys.path.append("..")
# Leertaste vor dem Kommentar.
# Dieses Modul stellt Datums- und Zeitfunktionen bereit, die für diverse Berechnungen benötigt werden.
from datetime import datetime, date, timedelta, time, timezone
# Leertaste vor dem Kommentar.
# Dieses Modul stellt Funktionen für den Zugriff auf das Betriebssystem bereit.
import os
# Leertaste vor dem Kommentar.
# Dieses Modul dient zur Verarbeitung von XML-Daten.
import xml.etree.ElementTree as ET
# Leertaste vor dem Kommentar.
# Dieses Modul stellt Klassen und Funktionen für eine Flask-Webanwendung bereit.
from flask import Flask, Response, request, jsonify
# Leertaste vor dem Kommentar.
# Dieses Modul erzeugt RSS-Feeds aus Datenstrukturen.
from feedgen.feed import FeedGenerator
# Leertaste vor dem Kommentar.
# Diese Klasse stellt eine Verbindung zur MySQL-Datenbank her und verwaltet Sessions.
from manager.MysqlManager import MysqlManager
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert Rohdaten von News, die aus Obsidian stammen.
from models.rawnewsdata_rane import RawNewsDataRane
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert wirtschaftliche News, die ausgewertet werden.
from models.economicnews_ecne import EconomicNewsEcne
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert einen Wirtschaftsdatenkalender.
from models.economiccalendar_ecca import EconomicCalendarEcca
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert KI-generierte News-Daten.
from models.ainewsdata_aine import AinewsDataAine
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert einen Puffer zur Priorisierung von News.
from models.prioritybuffer_prbu import PrioritybufferPrbu
# Leertaste vor dem Kommentar.
# Diese Klasse stellt Reporting-Funktionen zur Verfügung, um HTML-Berichte zu erzeugen.
from manager.ReportingManager import ReportingManager
# Leertaste vor dem Kommentar.
# Diese Funktionen stellen Operatoren bereit, um komplexe SQL-Filter zu bauen.
from sqlalchemy import or_, and_
# Leertaste vor dem Kommentar.
# Diese Variable erzeugt eine neue Flask-Anwendung, die als Webserver agiert.
app = Flask(__name__)
# Leertaste vor dem Kommentar.
# Diese Abfrage startet den Flask-Webserver nur, wenn dieses Skript direkt ausgeführt wird.
if __name__ == '__main__':
# Leertaste vor dem Kommentar.
# Diese Zeile startet die Flask-Anwendung auf allen Schnittstellen und Port 80.
app.run(host='0.0.0.0', port=80)
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship
from models._system import Base
from models.basegroup_bgro import BasegroupBgro
from models.option_opti import OptionOpti
class BaseBase(Base):
__tablename__ = 'base_base'
id_base = Column(Integer, primary_key=True, autoincrement=True)
basegroup_base = Column(Integer, ForeignKey("basegroup_bgro.id_bgro"))
provider_base = Column(String(255), nullable=False)
providercode_base= Column(String(255))
name_base = Column(String(255), nullable=False)
alias_base = Column(String(255))
network_base = Column(Integer, nullable=False)
type_base = Column(Integer, nullable=False)
flyerurl_base = Column(String(255))
piburl_base = Column(String(255))
details_base = Column(JSON) # enthält das von GPT extrahierte Tarif‑JSON
created_base = Column(DateTime, nullable=False)
updated_base = Column(DateTime, nullable=False)
basegroup = relationship("BasegroupBgro", back_populates="bases")
deals = relationship("DealDeal", back_populates="base")
options = relationship("OptionOpti", back_populates="base")
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from models._system import Base
class BasegroupBgro(Base):
__tablename__ = 'basegroup_bgro'
id_bgro = Column(Integer, primary_key=True, autoincrement=True)
name_bgro = Column(String(255), nullable=False)
created_bgro = Column(DateTime, nullable=False)
updated_bgro = Column(DateTime, nullable=False)
bases = relationship("BaseBase", back_populates="basegroup")
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,640 +7,17 @@
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>id_base</th>\n",
" <th>provider_base</th>\n",
" <th>providercode_base</th>\n",
" <th>name_base</th>\n",
" <th>network_base</th>\n",
" <th>created_base</th>\n",
" <th>updated_base</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>133</td>\n",
" <td>Freenet | Cellway</td>\n",
" <td>3877325</td>\n",
" <td>Allnet Flat 20 GB Telekom (Okt 2024)</td>\n",
" <td>1</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>134</td>\n",
" <td>Freenet | Cellway</td>\n",
" <td>3877349</td>\n",
" <td>Allnet Flat 20 GB Telekom (Okt 2024) mit Smart...</td>\n",
" <td>1</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>135</td>\n",
" <td>Freenet | Cellway</td>\n",
" <td>3877337</td>\n",
" <td>Allnet Flat 20 GB Telekom (Okt 2024) mit Smart...</td>\n",
" <td>1</td>\n",
" <td>2025-04-22 11:44:42</td>\n",
" <td>2025-04-22 11:44:42</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>136</td>\n",
" <td>Freenet | Cellway</td>\n",
" <td>3878213</td>\n",
" <td>Allnet Flat 25 GB Telekom (Okt 2024)</td>\n",
" <td>1</td>\n",
" <td>2025-04-22 11:45:04</td>\n",
" <td>2025-04-22 11:45:04</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>137</td>\n",
" <td>Freenet | Cellway</td>\n",
" <td>3878237</td>\n",
" <td>Allnet Flat 25 GB Telekom (Okt 2024) mit Smart...</td>\n",
" <td>1</td>\n",
" <td>2025-04-22 11:45:24</td>\n",
" <td>2025-04-22 11:45:24</td>\n",
" </tr>\n",
" <tr>\n",
" <th>...</th>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>127</th>\n",
" <td>260</td>\n",
" <td>Freenet | Netzbetreiber</td>\n",
" <td>3782331</td>\n",
" <td>o2 Mobile Unlimited Smart (Jun 2024) mit Smart...</td>\n",
" <td>4</td>\n",
" <td>2025-04-22 12:17:20</td>\n",
" <td>2025-04-22 12:17:20</td>\n",
" </tr>\n",
" <tr>\n",
" <th>128</th>\n",
" <td>261</td>\n",
" <td>Freenet | Netzbetreiber</td>\n",
" <td>3782328</td>\n",
" <td>o2 Mobile Unlimited Smart (Jun 2024) mit Smart...</td>\n",
" <td>4</td>\n",
" <td>2025-04-22 12:17:38</td>\n",
" <td>2025-04-22 12:17:38</td>\n",
" </tr>\n",
" <tr>\n",
" <th>129</th>\n",
" <td>262</td>\n",
" <td>Freenet | Netzbetreiber</td>\n",
" <td>3973295</td>\n",
" <td>o2 Mobile XL (Nov 2024)</td>\n",
" <td>4</td>\n",
" <td>2025-04-22 12:17:55</td>\n",
" <td>2025-04-22 12:17:55</td>\n",
" </tr>\n",
" <tr>\n",
" <th>130</th>\n",
" <td>263</td>\n",
" <td>Freenet | Netzbetreiber</td>\n",
" <td>3973301</td>\n",
" <td>o2 Mobile XL (Nov 2024) mit Smartphone 10</td>\n",
" <td>4</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" </tr>\n",
" <tr>\n",
" <th>131</th>\n",
" <td>264</td>\n",
" <td>Freenet | Netzbetreiber</td>\n",
" <td>3973298</td>\n",
" <td>o2 Mobile XL (Nov 2024) mit Smartphone 5</td>\n",
" <td>4</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>132 rows × 7 columns</p>\n",
"</div>"
],
"text/plain": [
" id_base provider_base providercode_base \\\n",
"0 133 Freenet | Cellway 3877325 \n",
"1 134 Freenet | Cellway 3877349 \n",
"2 135 Freenet | Cellway 3877337 \n",
"3 136 Freenet | Cellway 3878213 \n",
"4 137 Freenet | Cellway 3878237 \n",
".. ... ... ... \n",
"127 260 Freenet | Netzbetreiber 3782331 \n",
"128 261 Freenet | Netzbetreiber 3782328 \n",
"129 262 Freenet | Netzbetreiber 3973295 \n",
"130 263 Freenet | Netzbetreiber 3973301 \n",
"131 264 Freenet | Netzbetreiber 3973298 \n",
"\n",
" name_base network_base \\\n",
"0 Allnet Flat 20 GB Telekom (Okt 2024) 1 \n",
"1 Allnet Flat 20 GB Telekom (Okt 2024) mit Smart... 1 \n",
"2 Allnet Flat 20 GB Telekom (Okt 2024) mit Smart... 1 \n",
"3 Allnet Flat 25 GB Telekom (Okt 2024) 1 \n",
"4 Allnet Flat 25 GB Telekom (Okt 2024) mit Smart... 1 \n",
".. ... ... \n",
"127 o2 Mobile Unlimited Smart (Jun 2024) mit Smart... 4 \n",
"128 o2 Mobile Unlimited Smart (Jun 2024) mit Smart... 4 \n",
"129 o2 Mobile XL (Nov 2024) 4 \n",
"130 o2 Mobile XL (Nov 2024) mit Smartphone 10 4 \n",
"131 o2 Mobile XL (Nov 2024) mit Smartphone 5 4 \n",
"\n",
" created_base updated_base \n",
"0 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"1 2025-04-22 11:44:22 2025-04-22 11:44:22 \n",
"2 2025-04-22 11:44:42 2025-04-22 11:44:42 \n",
"3 2025-04-22 11:45:04 2025-04-22 11:45:04 \n",
"4 2025-04-22 11:45:24 2025-04-22 11:45:24 \n",
".. ... ... \n",
"127 2025-04-22 12:17:20 2025-04-22 12:17:20 \n",
"128 2025-04-22 12:17:38 2025-04-22 12:17:38 \n",
"129 2025-04-22 12:17:55 2025-04-22 12:17:55 \n",
"130 2025-04-22 12:18:13 2025-04-22 12:18:13 \n",
"131 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"\n",
"[132 rows x 7 columns]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>id_deal</th>\n",
" <th>base_deal</th>\n",
" <th>providercode_deal</th>\n",
" <th>name_deal</th>\n",
" <th>price_deal</th>\n",
" <th>starts_deal</th>\n",
" <th>stops_deal</th>\n",
" <th>created_deal</th>\n",
" <th>updated_deal</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>281</td>\n",
" <td>133</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td>25.20168</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>282</td>\n",
" <td>133</td>\n",
" <td>A3908081</td>\n",
" <td>170 EUR Sonderbonus</td>\n",
" <td>25.20168</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>283</td>\n",
" <td>133</td>\n",
" <td>A3908084</td>\n",
" <td>24 x 10 EUR Rabatt auf MGP, 0 EUR Vergütungsve...</td>\n",
" <td>25.20168</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>284</td>\n",
" <td>134</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td>33.60504</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>285</td>\n",
" <td>134</td>\n",
" <td>A3908081</td>\n",
" <td>170 EUR Sonderbonus</td>\n",
" <td>33.60504</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" <td>2025-04-22 11:44:22</td>\n",
" </tr>\n",
" <tr>\n",
" <th>...</th>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>275</th>\n",
" <td>556</td>\n",
" <td>262</td>\n",
" <td>A3990011</td>\n",
" <td>210 EUR Sonderbonus</td>\n",
" <td>42.00840</td>\n",
" <td>2025-04-22 12:17:55</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:17:55</td>\n",
" <td>2025-04-22 12:17:55</td>\n",
" </tr>\n",
" <tr>\n",
" <th>276</th>\n",
" <td>557</td>\n",
" <td>263</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td>50.41176</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" </tr>\n",
" <tr>\n",
" <th>277</th>\n",
" <td>558</td>\n",
" <td>263</td>\n",
" <td>A3990011</td>\n",
" <td>210 EUR Sonderbonus</td>\n",
" <td>50.41176</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" <td>2025-04-22 12:18:13</td>\n",
" </tr>\n",
" <tr>\n",
" <th>278</th>\n",
" <td>559</td>\n",
" <td>264</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td>46.21008</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" <tr>\n",
" <th>279</th>\n",
" <td>560</td>\n",
" <td>264</td>\n",
" <td>A3990011</td>\n",
" <td>210 EUR Sonderbonus</td>\n",
" <td>46.21008</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>280 rows × 9 columns</p>\n",
"</div>"
],
"text/plain": [
" id_deal base_deal providercode_deal \\\n",
"0 281 133 \n",
"1 282 133 A3908081 \n",
"2 283 133 A3908084 \n",
"3 284 134 \n",
"4 285 134 A3908081 \n",
".. ... ... ... \n",
"275 556 262 A3990011 \n",
"276 557 263 \n",
"277 558 263 A3990011 \n",
"278 559 264 \n",
"279 560 264 A3990011 \n",
"\n",
" name_deal price_deal \\\n",
"0 25.20168 \n",
"1 170 EUR Sonderbonus 25.20168 \n",
"2 24 x 10 EUR Rabatt auf MGP, 0 EUR Vergütungsve... 25.20168 \n",
"3 33.60504 \n",
"4 170 EUR Sonderbonus 33.60504 \n",
".. ... ... \n",
"275 210 EUR Sonderbonus 42.00840 \n",
"276 50.41176 \n",
"277 210 EUR Sonderbonus 50.41176 \n",
"278 46.21008 \n",
"279 210 EUR Sonderbonus 46.21008 \n",
"\n",
" starts_deal stops_deal created_deal updated_deal \n",
"0 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"1 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"2 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"3 2025-04-22 11:44:22 None 2025-04-22 11:44:22 2025-04-22 11:44:22 \n",
"4 2025-04-22 11:44:22 None 2025-04-22 11:44:22 2025-04-22 11:44:22 \n",
".. ... ... ... ... \n",
"275 2025-04-22 12:17:55 None 2025-04-22 12:17:55 2025-04-22 12:17:55 \n",
"276 2025-04-22 12:18:13 None 2025-04-22 12:18:13 2025-04-22 12:18:13 \n",
"277 2025-04-22 12:18:13 None 2025-04-22 12:18:13 2025-04-22 12:18:13 \n",
"278 2025-04-22 12:18:31 None 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"279 2025-04-22 12:18:31 None 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"\n",
"[280 rows x 9 columns]"
"name": "stderr",
"output_type": "stream",
"text": [
"2025-05-05 14:13:55,040 INFO: Connected (version 2.0, client OpenSSH_7.6p1)\n",
"2025-05-05 14:13:55,245 INFO: Authentication (publickey) failed.\n",
"2025-05-05 14:13:55,302 INFO: Connected (version 2.0, client OpenSSH_7.6p1)\n",
"2025-05-05 14:13:55,512 INFO: Authentication (password) successful!\n",
"2025-05-05 14:13:57,286 INFO: INSERT IGNORE'd 769 deals\n",
"2025-05-05 14:14:05,398 INFO: INSERT IGNORE'd 10386 options\n",
"2025-05-05 14:14:05,489 INFO: Import-Lauf abgeschlossen.\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>id_opti</th>\n",
" <th>base_opti</th>\n",
" <th>providercode_opti</th>\n",
" <th>providercategory_opti</th>\n",
" <th>name_opti</th>\n",
" <th>price_opti</th>\n",
" <th>starts_opti</th>\n",
" <th>stops_opti</th>\n",
" <th>created_opti</th>\n",
" <th>updated_opti</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>4627</td>\n",
" <td>133</td>\n",
" <td>O3132980</td>\n",
" <td>G3120771</td>\n",
" <td>Aktionsguthaben VP | 192 EUR Aktionsguthaben V...</td>\n",
" <td>0.00000</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>4628</td>\n",
" <td>133</td>\n",
" <td>O1173</td>\n",
" <td>G19</td>\n",
" <td>Standard EVN | EVN gekürzt</td>\n",
" <td>0.00000</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>4629</td>\n",
" <td>133</td>\n",
" <td>O15695</td>\n",
" <td>G209</td>\n",
" <td>Sperren | Sperre anonyme Anrufe - auf Kundenwu...</td>\n",
" <td>0.00000</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>4630</td>\n",
" <td>133</td>\n",
" <td>O18978</td>\n",
" <td>G218</td>\n",
" <td>Mobile Sicherheit | freenet Handy Helfer 24 Mo...</td>\n",
" <td>4.19328</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>4631</td>\n",
" <td>133</td>\n",
" <td>O17691</td>\n",
" <td>G236</td>\n",
" <td>freenet Cloud (Cloud) | Digitales Service Pake...</td>\n",
" <td>2.51261</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" <td>2025-04-22 11:43:59</td>\n",
" </tr>\n",
" <tr>\n",
" <th>...</th>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5680</th>\n",
" <td>10307</td>\n",
" <td>264</td>\n",
" <td>O3501308</td>\n",
" <td>G3559799</td>\n",
" <td>Cashback Fachhandel | VP: 100 Euro Cashback</td>\n",
" <td>0.00000</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5681</th>\n",
" <td>10308</td>\n",
" <td>264</td>\n",
" <td>O3708580</td>\n",
" <td>G3711730</td>\n",
" <td>Rabatt auf Monatsgrundpreis | VP: 24 x 7 EUR R...</td>\n",
" <td>0.00000</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5682</th>\n",
" <td>10309</td>\n",
" <td>264</td>\n",
" <td>G19</td>\n",
" <td>G4</td>\n",
" <td>Einzelverbindungsnachweis | Standard EVN</td>\n",
" <td>0.00000</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5683</th>\n",
" <td>10310</td>\n",
" <td>264</td>\n",
" <td>O2805</td>\n",
" <td>G131</td>\n",
" <td>Multicard | Multicard 3</td>\n",
" <td>4.19328</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5684</th>\n",
" <td>10311</td>\n",
" <td>264</td>\n",
" <td>O202</td>\n",
" <td>G289</td>\n",
" <td>Deutschlandweite Festnetznummer kostenlos | St...</td>\n",
" <td>0.00000</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>None</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" <td>2025-04-22 12:18:31</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>5685 rows × 10 columns</p>\n",
"</div>"
],
"text/plain": [
" id_opti base_opti providercode_opti providercategory_opti \\\n",
"0 4627 133 O3132980 G3120771 \n",
"1 4628 133 O1173 G19 \n",
"2 4629 133 O15695 G209 \n",
"3 4630 133 O18978 G218 \n",
"4 4631 133 O17691 G236 \n",
"... ... ... ... ... \n",
"5680 10307 264 O3501308 G3559799 \n",
"5681 10308 264 O3708580 G3711730 \n",
"5682 10309 264 G19 G4 \n",
"5683 10310 264 O2805 G131 \n",
"5684 10311 264 O202 G289 \n",
"\n",
" name_opti price_opti \\\n",
"0 Aktionsguthaben VP | 192 EUR Aktionsguthaben V... 0.00000 \n",
"1 Standard EVN | EVN gekürzt 0.00000 \n",
"2 Sperren | Sperre anonyme Anrufe - auf Kundenwu... 0.00000 \n",
"3 Mobile Sicherheit | freenet Handy Helfer 24 Mo... 4.19328 \n",
"4 freenet Cloud (Cloud) | Digitales Service Pake... 2.51261 \n",
"... ... ... \n",
"5680 Cashback Fachhandel | VP: 100 Euro Cashback 0.00000 \n",
"5681 Rabatt auf Monatsgrundpreis | VP: 24 x 7 EUR R... 0.00000 \n",
"5682 Einzelverbindungsnachweis | Standard EVN 0.00000 \n",
"5683 Multicard | Multicard 3 4.19328 \n",
"5684 Deutschlandweite Festnetznummer kostenlos | St... 0.00000 \n",
"\n",
" starts_opti stops_opti created_opti updated_opti \n",
"0 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"1 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"2 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"3 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"4 2025-04-22 11:43:59 None 2025-04-22 11:43:59 2025-04-22 11:43:59 \n",
"... ... ... ... ... \n",
"5680 2025-04-22 12:18:31 None 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"5681 2025-04-22 12:18:31 None 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"5682 2025-04-22 12:18:31 None 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"5683 2025-04-22 12:18:31 None 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"5684 2025-04-22 12:18:31 None 2025-04-22 12:18:31 2025-04-22 12:18:31 \n",
"\n",
"[5685 rows x 10 columns]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
......@@ -648,383 +25,275 @@
"import os\n",
"import csv\n",
"import datetime\n",
"import pandas as pd\n",
"import logging\n",
"from decimal import Decimal\n",
"from collections import defaultdict\n",
"from sqlalchemy.dialects.mysql import insert as mysql_insert\n",
"from manager.MysqlManager import MysqlManager\n",
"from models._system import Base\n",
"from models.base_base import BaseBase\n",
"from models.deal_deal import DealDeal\n",
"from models.option_opti import OptionOpti\n",
"\n",
"# Hier wird der Pfad zur CSV-Datei mit den Tarifplandaten festgelegt.\n",
"csvFilePath = os.path.join('..', 'cache', 'plans.csv')\n",
"\n",
"# Hier wird der Pfad zur CSV-Datei mit den Kampagnendaten festgelegt.\n",
"csvFileCampaigns = os.path.join('..', 'cache', 'campaigns.csv')\n",
"\n",
"# Hier wird der Pfad zur CSV-Datei mit den Optionsdaten festgelegt.\n",
"csvFileOptions = os.path.join('..', 'cache', 'options.csv')\n",
"\n",
"# Hier wird der Pfad zur CSV-Datei mit den Kategoriedaten festgelegt.\n",
"csvFileCategories = os.path.join('..', 'cache', 'categorys.csv')\n",
"\n",
"# Hier wird ein Objekt zur Verwaltung der MySQL-Verbindung erzeugt.\n",
"mysqlManager = MysqlManager()\n",
"\n",
"# Hier wird eine neue Datenbank-Session geöffnet, um Abfragen durchzuführen.\n",
"dbSession = mysqlManager.getSession()\n",
"\n",
"# Hier wird eine Liste gespeichert, welche die IDs neu erstellter Base-Einträge aufnimmt.\n",
"newBaseIds = []\n",
"\n",
"# Hier wird eine Liste gespeichert, welche die IDs neu erstellter Deal-Einträge aufnimmt.\n",
"newDealIds = []\n",
"\n",
"# Hier wird eine Liste gespeichert, welche die IDs neu erstellter Options-Einträge aufnimmt.\n",
"newOptionIds = []\n",
"\n",
"# Hier wird ein Wörterbuch verwaltet, das Kategorie-IDs auf deren Namen abbildet.\n",
"categoryMap = {}\n",
"\n",
"# Hier wird die CSV-Datei mit Kategoriedaten geöffnet, um diese in das Wörterbuch zu laden.\n",
"with open(csvFileCategories, newline='', encoding='utf-8') as csvCat:\n",
"\n",
" # Hier wird ein CSV-DictReader erzeugt, der den Inhalt zeilenweise als Wörterbuch interpretiert.\n",
" csvReaderCat = csv.DictReader(csvCat, delimiter=';')\n",
"\n",
" # Hier wird jede Zeile durchlaufen, um die Kategorie-ID und den Namen zu übernehmen.\n",
" for catRow in csvReaderCat:\n",
"\n",
" # Hier wird die Kategorie-ID geholt und Leerzeichen werden entfernt.\n",
" catId = catRow.get('id', '').strip()\n",
"\n",
" # Hier wird der Kategoriename geholt und Leerzeichen werden entfernt.\n",
" catName = catRow.get('name', '').strip()\n",
"\n",
" # Hier wird das Mapping in das Wörterbuch geschrieben, wobei die ID der Schlüssel ist.\n",
" categoryMap[catId] = catName\n",
"\n",
"# Hier wird die CSV-Datei mit den Tarifplandaten geöffnet, um die Daten in die Datenbank zu übertragen.\n",
"with open(csvFilePath, newline='', encoding='utf-8') as csvFile:\n",
"\n",
" # Hier wird ein CSV-DictReader erzeugt, um die Tarifplandaten auszulesen.\n",
" csvReader = csv.DictReader(csvFile, delimiter=';')\n",
"\n",
" # Hier werden die Tarifplandaten zeilenweise durchlaufen.\n",
" for row in csvReader:\n",
"\n",
" # Hier wird die Plan-ID aus der Zeile übernommen und Leerzeichen entfernt.\n",
" csvId = row.get('id', '').strip()\n",
"\n",
" # Hier wird der Providername übernommen und Leerzeichen entfernt.\n",
" providerName = row.get('provider', '').strip()\n",
"\n",
" # Hier wird das Netzwerk übernommen und Leerzeichen entfernt.\n",
" networkName = row.get('network', '').strip()\n",
"\n",
" # Hier wird der Tarifname übernommen und Leerzeichen entfernt.\n",
" planName = row.get('name', '').strip()\n",
"\n",
" # Hier wird der Tarifpreis als String übernommen und Leerzeichen entfernt.\n",
" planPriceStr = row.get('price', '').strip()\n",
"\n",
" # Hier wird ein zusammengesetzter Providerwert erstellt.\n",
" providerBaseVal = \"Freenet | \" + providerName\n",
"\n",
" # Hier wird in der Tabelle 'base_base' nach einem bereits vorhandenen Datensatz gesucht, der passt.\n",
" existingBase = dbSession.query(BaseBase).filter_by(\n",
" provider_base=providerBaseVal,\n",
" providercode_base=csvId\n",
" ).first()\n",
"\n",
" # Hier wird ein Netzwerk-Integerwert anhand des Netzwerknamens bestimmt.\n",
" if networkName == \"D1\":\n",
" networkVal = 1\n",
" elif networkName == \"D2\":\n",
" networkVal = 2\n",
" elif networkName == \"O2\":\n",
" networkVal = 4\n",
" else:\n",
" networkVal = 0\n",
"\n",
" # Hier wird die aktuelle Uhrzeit bestimmt.\n",
" currentTime = datetime.datetime.now()\n",
"\n",
" # Hier wird geprüft, ob bereits ein passender Eintrag existiert.\n",
" if existingBase:\n",
"\n",
" # Hier wird der vorhandene Eintrag genutzt, falls er existiert.\n",
" baseRecord = existingBase\n",
"\n",
" else:\n",
"\n",
" # Hier wird ein neuer Eintrag in 'base_base' erzeugt, da kein passender vorhanden war.\n",
" baseRecord = BaseBase(\n",
" provider_base=providerBaseVal,\n",
" providercode_base=csvId,\n",
" basegroup_base=1,\n",
" name_base=planName,\n",
" network_base=networkVal,\n",
" type_base=0,\n",
" created_base=currentTime,\n",
" updated_base=currentTime\n",
"# \n",
"# Hier wird die Log-Konfiguration festgelegt, damit während des Ablaufs aussagekräftige Zeit- und Fehlermeldungen ausgegeben werden.\n",
"logging.basicConfig(\n",
" level=logging.INFO,\n",
" format=\"%(asctime)s %(levelname)s: %(message)s\"\n",
")\n",
"\n",
"# \n",
"# Dieses Verzeichnis verweist auf den Zwischenspeicher, in dem alle CSV-Dateien abgelegt sind.\n",
"CSV_DIR = os.path.join(\"..\", \"cache\")\n",
"\n",
"# \n",
"# Hier werden die einzelnen CSV-Dateien innerhalb des Zwischenspeichers definiert.\n",
"csvFileCategories = os.path.join(CSV_DIR, \"categorys.csv\")\n",
"csvFilePlans = os.path.join(CSV_DIR, \"plans.csv\")\n",
"csvFileCampaigns = os.path.join(CSV_DIR, \"campaigns.csv\")\n",
"csvFileOptions = os.path.join(CSV_DIR, \"options.csv\")\n",
"\n",
"# \n",
"# Diese Funktion liest eine CSV-Datei vollständig ein und liefert jede Zeile als Wörterbuch zurück.\n",
"def read_csv(path):\n",
" with open(path, newline=\"\", encoding=\"utf-8\") as f:\n",
" return list(csv.DictReader(f, delimiter=\";\"))\n",
"\n",
"# \n",
"# Hier werden sämtliche CSV-Dateien in Listen von Wörterbüchern eingelesen.\n",
"cat_rows = read_csv(csvFileCategories)\n",
"plan_rows = read_csv(csvFilePlans)\n",
"camp_rows = read_csv(csvFileCampaigns)\n",
"opt_rows = read_csv(csvFileOptions)\n",
"\n",
"# \n",
"# Dieses Wörterbuch ordnet jeder Kategorietabelle den zugehörigen Namen für späteres Nachschlagen zu.\n",
"category_name = {r[\"id\"].strip(): r[\"name\"].strip() for r in cat_rows}\n",
"\n",
"# \n",
"# Diese Datenstruktur ordnet jeder Plan-ID alle zugehörigen Kampagnenzeilen zu, um schnellen Zugriff zu ermöglichen.\n",
"campaigns_by_plan = defaultdict(list)\n",
"for c in camp_rows:\n",
" campaigns_by_plan[c[\"plan\"].strip()].append(c)\n",
"\n",
"# \n",
"# Diese Datenstruktur ordnet jeder Plan-ID alle zugehörigen Optionszeilen zu, um schnellen Zugriff zu ermöglichen.\n",
"options_by_plan = defaultdict(list)\n",
"for o in opt_rows:\n",
" options_by_plan[o[\"plan\"].strip()].append(o)\n",
"\n",
"# \n",
"# Hier wird die Verbindung zur Datenbank geöffnet und eine neue Session erzeugt.\n",
"mysql = MysqlManager()\n",
"session = mysql.getSession()\n",
"\n",
"# \n",
"# Dieses Wörterbuch enthält alle bestehenden Basiseinträge, damit später neue Einträge erkannt werden können.\n",
"base_db = {(b.provider_base, b.providercode_base): b\n",
" for b in session.query(BaseBase).all()}\n",
"\n",
"# \n",
"# Diese verschachtelte Struktur hält alle vorhandenen Deals pro Base-ID, wodurch ein schneller Abgleich ermöglicht wird.\n",
"deals_db = defaultdict(dict)\n",
"for d in session.query(DealDeal).all():\n",
" deals_db[d.base_deal][d.providercode_deal] = d\n",
"\n",
"# \n",
"# Diese verschachtelte Struktur hält alle vorhandenen Optionen pro Base-ID, um später Stop- und Reaktivierungslogik anzuwenden.\n",
"opts_db = defaultdict(dict)\n",
"for o in session.query(OptionOpti).all():\n",
" opts_db[o.base_opti][o.providercode_opti] = o\n",
"\n",
"# \n",
"# Hier wird der aktuelle Zeitpunkt einmalig festgelegt, um ihn konsistent für alle neu erzeugten Datensätze zu verwenden.\n",
"now = datetime.datetime.now()\n",
"\n",
"# \n",
"# Diese Liste sammelt alle neu anzulegenden Basiseinträge, damit sie in einem Schritt geschrieben werden können.\n",
"new_bases = []\n",
"for p in plan_rows:\n",
" prov_base = f\"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}\"\n",
" key = (prov_base, p[\"id\"].strip())\n",
" if key not in base_db:\n",
" b = BaseBase(\n",
" provider_base = prov_base,\n",
" providercode_base = p[\"id\"].strip(),\n",
" name_base = p[\"name\"].strip(),\n",
" created_base = now,\n",
" updated_base = now\n",
" )\n",
"\n",
" # Hier wird das neue Objekt der Session hinzugefügt.\n",
" dbSession.add(baseRecord)\n",
"\n",
" # Hier wird das Objekt in der Datenbank gespeichert.\n",
" dbSession.commit()\n",
"\n",
" # Hier wird die ID des neuen Eintrags gemerkt.\n",
" newBaseIds.append(baseRecord.id_base)\n",
"\n",
" # Hier wird eine Funktion definiert, um Deals zu erstellen oder zu überprüfen.\n",
" def upsertDeal(providerCodeDeal, dealName=\"\"):\n",
"\n",
" # Hier wird geprüft, ob bereits ein Deal mit dem angegebenen Providercode vorhanden ist.\n",
" existingDeal = dbSession.query(DealDeal).filter_by(\n",
" base_deal=baseRecord.id_base,\n",
" providercode_deal=providerCodeDeal\n",
" ).first()\n",
"\n",
" # Hier wird unterschieden, ob ein Deal gefunden wurde.\n",
" if existingDeal:\n",
"\n",
" # Hier ist nichts weiter zu tun, wenn er bereits existiert.\n",
" pass\n",
"\n",
" else:\n",
"\n",
" # Hier wird ein neuer Deal erstellt, wenn er noch nicht vorhanden ist.\n",
" newDeal = DealDeal(\n",
" provisiongroup_deal=1,\n",
" base_deal=baseRecord.id_base,\n",
" providercode_deal=providerCodeDeal,\n",
" name_deal=dealName,\n",
" price_deal=Decimal(planPriceStr) if planPriceStr else Decimal(\"0.00\"),\n",
" starts_deal=currentTime,\n",
" stops_deal=None,\n",
" created_deal=currentTime,\n",
" updated_deal=currentTime\n",
" )\n",
"\n",
" # Hier wird das neue Deal-Objekt der Session hinzugefügt.\n",
" dbSession.add(newDeal)\n",
"\n",
" # Hier wird das Objekt in der Datenbank gespeichert.\n",
" dbSession.commit()\n",
"\n",
" # Hier wird die ID des neuen Deals gemerkt.\n",
" newDealIds.append(newDeal.id_deal)\n",
"\n",
" # Hier wird ein Standard-Deal ohne spezifischen Providercode angelegt.\n",
" upsertDeal(\"\")\n",
"\n",
" # Hier wird die CSV-Datei mit den Kampagnendaten geöffnet, um passende Deals zu erstellen.\n",
" with open(csvFileCampaigns, newline='', encoding='utf-8') as csvFile2:\n",
"\n",
" # Hier wird ein CSV-DictReader erzeugt, um die Kampagnen zeilenweise auszulesen.\n",
" csvReader2 = csv.DictReader(csvFile2, delimiter=';')\n",
"\n",
" # Hier werden alle Kampagnen durchlaufen.\n",
" for row2 in csvReader2:\n",
"\n",
" # Hier wird die Kampagnen-ID ermittelt und Leerzeichen entfernt.\n",
" campaignId = row2.get('id', '').strip()\n",
"\n",
" # Hier wird die zugehörige Tarif-ID ermittelt und Leerzeichen entfernt.\n",
" campaignPlan = row2.get('plan', '').strip()\n",
"\n",
" # Hier wird der Kampagnenname ermittelt und Leerzeichen entfernt.\n",
" campaignName = row2.get('name', '').strip()\n",
"\n",
" # Hier wird geprüft, ob die Kampagne zum aktuellen Tarif gehört.\n",
" if campaignPlan == csvId:\n",
"\n",
" # Hier wird ein spezifischer Deal für die Kampagne erzeugt.\n",
" upsertDeal(\"A\" + campaignId, campaignName)\n",
"\n",
" # Hier wird eine Funktion definiert, um Optionen zu erstellen oder zu überprüfen.\n",
" def upsertOption(providerCodeOpti, categoryOpti, optionName, optionPriceStr):\n",
"\n",
" # Hier wird geprüft, ob bereits eine Option mit dem angegebenen Providercode vorhanden ist.\n",
" existingOpti = dbSession.query(OptionOpti).filter_by(\n",
" base_opti=baseRecord.id_base,\n",
" providercode_opti=providerCodeOpti\n",
" ).first()\n",
"\n",
" # Hier wird unterschieden, ob eine Option gefunden wurde.\n",
" if existingOpti:\n",
"\n",
" # Hier ist nichts weiter zu tun, wenn sie bereits existiert.\n",
" pass\n",
"\n",
" else:\n",
"\n",
" # Hier wird der Kategoriename aus dem Wörterbuch ermittelt und ggf. ein Standard genommen.\n",
" categoryFullName = categoryMap.get(categoryOpti, \"Unbekannt\")\n",
"\n",
" # Hier wird ein vollständiger Name aus Kategorie und Optionsname generiert.\n",
" fullName = categoryFullName + \" | \" + optionName\n",
"\n",
" # Hier wird eine neue Option erzeugt.\n",
" newOpti = OptionOpti(\n",
" provisiongroup_opti=1,\n",
" base_opti=baseRecord.id_base,\n",
" providercode_opti=providerCodeOpti,\n",
" providercategory_opti=categoryOpti,\n",
" name_opti=fullName,\n",
" alias_opti=None,\n",
" price_opti=Decimal(optionPriceStr) if optionPriceStr else Decimal(\"0.00\"),\n",
" starts_opti=currentTime,\n",
" stops_opti=None,\n",
" provision1_opti=Decimal(\"0.00000\"),\n",
" provision2_opti=Decimal(\"0.00000\"),\n",
" provision3_opti=Decimal(\"0.00000\"),\n",
" provision4_opti=Decimal(\"0.00000\"),\n",
" created_opti=currentTime,\n",
" updated_opti=currentTime\n",
" )\n",
"\n",
" # Hier wird das neue Option-Objekt der Session hinzugefügt.\n",
" dbSession.add(newOpti)\n",
"\n",
" # Hier wird das Objekt in der Datenbank gespeichert.\n",
" dbSession.commit()\n",
"\n",
" # Hier wird die ID der neuen Option gemerkt.\n",
" newOptionIds.append(newOpti.id_opti)\n",
"\n",
" # Hier wird die CSV-Datei mit den Optionsdaten geöffnet, um passende Optionen zu erstellen.\n",
" with open(csvFileOptions, newline='', encoding='utf-8') as csvFileOpt:\n",
"\n",
" # Hier wird ein CSV-DictReader erzeugt, um die Optionsdaten zeilenweise auszulesen.\n",
" csvReaderOpt = csv.DictReader(csvFileOpt, delimiter=';')\n",
"\n",
" # Hier werden alle Optionsdaten durchlaufen.\n",
" for rowOpt in csvReaderOpt:\n",
"\n",
" # Hier wird die Options-ID ermittelt und Leerzeichen entfernt.\n",
" optId = rowOpt.get('id', '').strip()\n",
"\n",
" # Hier wird die zugehörige Plan-ID ermittelt und Leerzeichen entfernt.\n",
" optPlan = rowOpt.get('plan', '').strip()\n",
"\n",
" # Hier wird die Kategorie-ID ermittelt und Leerzeichen entfernt.\n",
" optCategory = rowOpt.get('category', '').strip()\n",
"\n",
" # Hier wird der Optionsname ermittelt und Leerzeichen entfernt.\n",
" optName = rowOpt.get('name', '').strip()\n",
"\n",
" # Hier wird der Optionspreis als String ermittelt und Leerzeichen entfernt.\n",
" optPriceStr = rowOpt.get('price', '').strip()\n",
"\n",
" # Hier wird geprüft, ob die Option zum aktuellen Tarif gehört.\n",
" if optPlan == csvId:\n",
"\n",
" # Hier wird eine neue Option in der Datenbank angelegt oder überprüft.\n",
" upsertOption(optId, optCategory, optName, optPriceStr)\n",
"\n",
"# Hier wird eine leere Liste für neu angelegte Base-Datensätze definiert.\n",
"newBases = []\n",
"\n",
"# Hier wird geprüft, ob neue Base-Einträge angelegt wurden.\n",
"if newBaseIds:\n",
"\n",
" # Hier werden alle neu angelegten Base-Einträge aus der Datenbank gelesen.\n",
" newBases = dbSession.query(BaseBase).filter(BaseBase.id_base.in_(newBaseIds)).all()\n",
"\n",
"# Hier wird eine leere Liste für neu angelegte Deal-Datensätze definiert.\n",
"newDeals = []\n",
"\n",
"# Hier wird geprüft, ob neue Deal-Einträge angelegt wurden.\n",
"if newDealIds:\n",
"\n",
" # Hier werden alle neu angelegten Deals aus der Datenbank gelesen.\n",
" newDeals = dbSession.query(DealDeal).filter(DealDeal.id_deal.in_(newDealIds)).all()\n",
"\n",
"# Hier wird eine leere Liste für neu angelegte Options-Datensätze definiert.\n",
"newOptions = []\n",
"\n",
"# Hier wird geprüft, ob neue Options-Einträge angelegt wurden.\n",
"if newOptionIds:\n",
"\n",
" # Hier werden alle neu angelegten Options-Datensätze aus der Datenbank gelesen.\n",
" newOptions = dbSession.query(OptionOpti).filter(OptionOpti.id_opti.in_(newOptionIds)).all()\n",
"\n",
"# Hier wird eine Liste von Wörterbüchern erstellt, um sie in ein Pandas DataFrame für Base zu überführen.\n",
"baseData = [{\n",
" 'id_base': b.id_base,\n",
" 'provider_base': b.provider_base,\n",
" 'providercode_base': b.providercode_base,\n",
" 'name_base': b.name_base,\n",
" 'network_base': b.network_base,\n",
" 'created_base': b.created_base,\n",
" 'updated_base': b.updated_base\n",
"} for b in newBases]\n",
"\n",
"# Hier wird eine Liste von Wörterbüchern erstellt, um sie in ein Pandas DataFrame für Deals zu überführen.\n",
"dealData = [{\n",
" 'id_deal': d.id_deal,\n",
" 'base_deal': d.base_deal,\n",
" 'providercode_deal': d.providercode_deal,\n",
" 'name_deal': d.name_deal,\n",
" 'price_deal': d.price_deal,\n",
" 'starts_deal': d.starts_deal,\n",
" 'stops_deal': d.stops_deal,\n",
" 'created_deal': d.created_deal,\n",
" 'updated_deal': d.updated_deal\n",
"} for d in newDeals]\n",
"\n",
"# Hier wird eine Liste von Wörterbüchern erstellt, um sie in ein Pandas DataFrame für Optionen zu überführen.\n",
"optiData = [{\n",
" 'id_opti': o.id_opti,\n",
" 'base_opti': o.base_opti,\n",
" 'providercode_opti': o.providercode_opti,\n",
" 'providercategory_opti': o.providercategory_opti,\n",
" 'name_opti': o.name_opti,\n",
" 'price_opti': o.price_opti,\n",
" 'starts_opti': o.starts_opti,\n",
" 'stops_opti': o.stops_opti,\n",
" 'created_opti': o.created_opti,\n",
" 'updated_opti': o.updated_opti\n",
"} for o in newOptions]\n",
"\n",
"# Hier wird ein DataFrame für die neu angelegten Base-Einträge erzeugt.\n",
"dfBase = pd.DataFrame(baseData)\n",
"\n",
"# Hier wird ein DataFrame für die neu angelegten Deal-Einträge erzeugt.\n",
"dfDeal = pd.DataFrame(dealData)\n",
"\n",
"# Hier wird ein DataFrame für die neu angelegten Options-Einträge erzeugt.\n",
"dfOpti = pd.DataFrame(optiData)\n",
"\n",
"# Hier wird das DataFrame der Base-Einträge angezeigt.\n",
"display(dfBase)\n",
"\n",
"# Hier wird das DataFrame der Deal-Einträge angezeigt.\n",
"display(dfDeal)\n",
"\n",
"# Hier wird das DataFrame der Options-Einträge angezeigt.\n",
"display(dfOpti)\n",
"\n",
"# Hier wird die Datenbank-Session geschlossen, um Ressourcen freizugeben.\n",
"dbSession.close()\n"
" new_bases.append(b)\n",
" base_db[key] = b\n",
"\n",
"# \n",
"# Hier werden alle neu erkannten Basiseinträge in einem einzigen Datenbankvorgang gespeichert.\n",
"if new_bases:\n",
" session.add_all(new_bases)\n",
" session.flush()\n",
" logging.info(\"Inserted %d new bases\", len(new_bases))\n",
"\n",
"# \n",
"# Diese verschachtelten Mengen erfassen für jede Base-ID die in diesem Lauf gewünschten Deals und Optionen.\n",
"desired_deals = defaultdict(set)\n",
"desired_opts = defaultdict(set)\n",
"\n",
"# \n",
"# Diese Listen sammeln alle Datensätze, die per INSERT IGNORE neu geschrieben oder aktualisiert werden sollen.\n",
"deal_rows_insert = []\n",
"opt_rows_insert = []\n",
"\n",
"for p in plan_rows:\n",
" prov_base = f\"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}\"\n",
" base_obj = base_db[(prov_base, p[\"id\"].strip())]\n",
" b_id = base_obj.id_base\n",
" price = Decimal(p[\"price\"].strip() or \"0.00\")\n",
"\n",
" # \n",
" # Dieser Block fügt den obligatorischen Standard-Deal ohne Kampagnenkennung hinzu.\n",
" desired_deals[b_id].add(\"\")\n",
" deal_rows_insert.append({\n",
" \"provisiongroup_deal\": 1,\n",
" \"base_deal\": b_id,\n",
" \"providercode_deal\": \"\",\n",
" \"name_deal\": \"\",\n",
" \"price_deal\": price,\n",
" \"starts_deal\": now,\n",
" \"stops_deal\": None,\n",
" \"created_deal\": now,\n",
" \"updated_deal\": now\n",
" })\n",
"\n",
" # \n",
" # Dieser Block verarbeitet alle Kampagnen zum aktuellen Plan und fügt sie der Wunschliste hinzu.\n",
" for c in campaigns_by_plan[p[\"id\"].strip()]:\n",
" code = f\"A{c['id'].strip()}\"\n",
" desired_deals[b_id].add(code)\n",
" deal_rows_insert.append({\n",
" \"provisiongroup_deal\": 1,\n",
" \"base_deal\": b_id,\n",
" \"providercode_deal\": code,\n",
" \"name_deal\": c[\"name\"].strip(),\n",
" \"price_deal\": price,\n",
" \"starts_deal\": now,\n",
" \"stops_deal\": None,\n",
" \"created_deal\": now,\n",
" \"updated_deal\": now\n",
" })\n",
"\n",
" # \n",
" # Dieser Block fügt alle Optionen zum aktuellen Plan der Wunschliste hinzu und bereitet die Insert-Zeilen vor.\n",
" for o in options_by_plan[p[\"id\"].strip()]:\n",
" code_opt = o[\"id\"].strip()\n",
" desired_opts[b_id].add(code_opt)\n",
" opt_rows_insert.append({\n",
" \"provisiongroup_opti\": 1,\n",
" \"base_opti\": b_id,\n",
" \"providercode_opti\": code_opt,\n",
" \"providercategory_opti\": o[\"category\"].strip(),\n",
" \"name_opti\": f\"{category_name.get(o['category'].strip(),'Unbekannt')} | {o['name'].strip()}\",\n",
" \"alias_opti\": None,\n",
" \"price_opti\": Decimal(o[\"price\"].strip() or \"0.00\"),\n",
" \"starts_opti\": now,\n",
" \"stops_opti\": None,\n",
" \"provision1_opti\": Decimal(\"0.00000\"),\n",
" \"provision2_opti\": Decimal(\"0.00000\"),\n",
" \"provision3_opti\": Decimal(\"0.00000\"),\n",
" \"provision4_opti\": Decimal(\"0.00000\"),\n",
" \"created_opti\": now,\n",
" \"updated_opti\": now\n",
" })\n",
"\n",
"# \n",
"# In diesem Schritt werden doppelte Deal- und Optionszeilen anhand ihrer Schlüsselwerte entfernt.\n",
"deal_rows_insert = list({(r[\"base_deal\"], r[\"providercode_deal\"]): r for r in deal_rows_insert}.values())\n",
"opt_rows_insert = list({(r[\"base_opti\"], r[\"providercode_opti\"]): r for r in opt_rows_insert }.values())\n",
"\n",
"# \n",
"# Diese Listen sammeln Datensätze, deren Status auf gestoppt oder reaktiviert gesetzt werden muss.\n",
"stop_deals, react_deals = [], []\n",
"stop_opts, react_opts = [], []\n",
"\n",
"for (prov, _), b in base_db.items():\n",
" if not prov.startswith(\"Freenet\"):\n",
" continue\n",
" b_id = b.id_base\n",
" wantD = desired_deals.get(b_id, set())\n",
" haveD = deals_db.get(b_id, {})\n",
" for code, obj in haveD.items():\n",
" if code in wantD and obj.stops_deal is not None:\n",
" react_deals.append({\"id_deal\": obj.id_deal,\n",
" \"stops_deal\": None,\n",
" \"updated_deal\": now})\n",
" if code not in wantD and obj.stops_deal is None:\n",
" stop_deals.append({\"id_deal\": obj.id_deal,\n",
" \"stops_deal\": now,\n",
" \"updated_deal\": now})\n",
"\n",
" wantO = desired_opts.get(b_id, set())\n",
" haveO = opts_db.get(b_id, {})\n",
" for code, obj in haveO.items():\n",
" if code in wantO and obj.stops_opti is not None:\n",
" react_opts.append({\"id_opti\": obj.id_opti,\n",
" \"stops_opti\": None,\n",
" \"updated_opti\": now})\n",
" if code not in wantO and obj.stops_opti is None:\n",
" stop_opts.append({\"id_opti\": obj.id_opti,\n",
" \"stops_opti\": now,\n",
" \"updated_opti\": now})\n",
"\n",
"# \n",
"# Dieser Block schreibt alle gewünschten Deals per INSERT IGNORE in die Datenbank.\n",
"session.execute(\n",
" mysql_insert(DealDeal.__table__).prefix_with(\"IGNORE\"),\n",
" deal_rows_insert\n",
")\n",
"logging.info(\"INSERT IGNORE'd %d deals\", len(deal_rows_insert))\n",
"\n",
"# \n",
"# Dieser Block schreibt alle gewünschten Optionen per INSERT IGNORE in die Datenbank.\n",
"session.execute(\n",
" mysql_insert(OptionOpti.__table__).prefix_with(\"IGNORE\"),\n",
" opt_rows_insert\n",
")\n",
"logging.info(\"INSERT IGNORE'd %d options\", len(opt_rows_insert))\n",
"\n",
"# \n",
"# Dieser Block aktualisiert alle Deals, die jetzt gestoppt werden müssen.\n",
"if stop_deals:\n",
" session.bulk_update_mappings(DealDeal, stop_deals)\n",
" logging.info(\"Stopped %d deals\", len(stop_deals))\n",
"\n",
"# \n",
"# Dieser Block aktualisiert alle Deals, die wieder reaktiviert werden müssen.\n",
"if react_deals:\n",
" session.bulk_update_mappings(DealDeal, react_deals)\n",
" logging.info(\"Reactivated %d deals\", len(react_deals))\n",
"\n",
"# \n",
"# Dieser Block aktualisiert alle Optionen, die jetzt gestoppt werden müssen.\n",
"if stop_opts:\n",
" session.bulk_update_mappings(OptionOpti, stop_opts)\n",
" logging.info(\"Stopped %d options\", len(stop_opts))\n",
"\n",
"# \n",
"# Dieser Block aktualisiert alle Optionen, die wieder reaktiviert werden müssen.\n",
"if react_opts:\n",
" session.bulk_update_mappings(OptionOpti, react_opts)\n",
" logging.info(\"Reactivated %d options\", len(react_opts))\n",
"\n",
"# \n",
"# Hier werden sämtliche Änderungen dauerhaft in der Datenbank gespeichert.\n",
"session.commit()\n",
"\n",
"# \n",
"# Zum Abschluss wird die Session geschlossen, um Ressourcen freizugeben.\n",
"session.close()\n",
"logging.info(\"Import-Lauf abgeschlossen.\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d6387255-71e0-40b4-8579-674320efc4db",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "001e79bd-8dff-4753-8754-6ec3101784dd",
"id": "01c87648-ca16-4f15-9c0d-17576bc3947d",
"metadata": {},
"outputs": [],
"source": []
......
......@@ -14,7 +14,7 @@
"text": [
"\n",
"--- Verarbeitung ID: 3243715 ---\n",
"INFO: flyerurl_base bereits vorhanden: https://freenetflyer.s3.amazonaws.com/flyers/3243715_flyer.pdf\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3243715_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3243715_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3332926 ---\n",
......@@ -25,10 +25,26 @@
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3337091_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3337091_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3337092 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3337092_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3337092_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3337093 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3337093_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3337093_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3337095 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3337095_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3337095_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3337096 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3337096_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3337096_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3337097 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3337097_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3337097_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3380690 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3380690_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3380690_pib.pdf\n",
......@@ -57,18 +73,50 @@
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398803_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398803_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398804 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398804_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398804_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398805 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398805_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398805_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398807 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398807_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398807_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398808 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398808_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398808_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398809 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398809_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398809_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398811 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398811_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398811_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398812 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398812_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398812_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398813 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398813_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398813_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398815 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398815_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398815_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398816 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398816_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398816_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3398817 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3398817_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3398817_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3415206 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3415206_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3415206_pib.pdf\n",
......@@ -77,46 +125,150 @@
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3430251_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3430251_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3430252 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3430252_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3430252_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3430253 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3430253_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3430253_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435067 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435067_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435067_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435068 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435068_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435068_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435069 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435069_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435069_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435071 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435071_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435071_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435072 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435072_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435072_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435073 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435073_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435073_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435075 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435075_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435075_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435076 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435076_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435076_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435077 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435077_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435077_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435079 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435079_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435079_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435080 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435080_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435080_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3435081 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3435081_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3435081_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3446028 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3446028_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3446028_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3446029 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3446029_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3446029_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3446030 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3446030_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3446030_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3448928 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3448928_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3448928_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3448929 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3448929_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3448929_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3448930 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3448930_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3448930_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3453573 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3453573_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3453573_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3453574 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3453574_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3453574_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461557 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461557_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461557_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461558 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461558_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461558_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461559 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461559_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461559_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461561 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461561_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461561_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461562 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461562_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461562_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461563 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461563_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461563_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461565 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461565_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461565_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461566 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461566_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461566_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461567 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461567_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461567_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461569 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461569_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461569_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461570 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461570_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461570_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3461571 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3461571_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3461571_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3470273 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3470273_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3470273_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3470276 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3470276_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3470276_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3473445 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3473445_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3473445_pib.pdf\n",
......@@ -129,22 +281,62 @@
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3596218_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3596218_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3596221 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3596221_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3596221_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3596224 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3596224_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3596224_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3649995 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3649995_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3649995_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3649998 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3649998_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3649998_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650001 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650001_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650001_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650007 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650007_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650007_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650010 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650010_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650010_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650013 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650013_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650013_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650019 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650019_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650019_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650022 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650022_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650022_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650025 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650025_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650025_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650031 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650031_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650031_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650034 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650034_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650034_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3650037 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3650037_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3650037_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3697996 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3697996_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3697996_pib.pdf\n",
......@@ -165,382 +357,85 @@
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3743301_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3743301_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3745454 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3745454_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3745454_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750083 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750083_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750083_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750086 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750086_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750086_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750089 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750089_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750089_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750710 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750710_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750710_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750713 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750713_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750713_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750716 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750716_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750716_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750758 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750758_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750758_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750761 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750761_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750761_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750764 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750764_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750764_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750806 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750806_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750806_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750809 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750809_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750809_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3750812 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3750812_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3750812_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3765006 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3765006_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3765006_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3782151 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3782151_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3782151_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3782322 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3782322_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3782322_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3782325 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3782325_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3782325_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3782328 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3782328_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3782328_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3782331 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3782331_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3782331_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833020 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833020_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833020_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833026 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833026_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833026_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833773 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833773_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833773_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833776 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833776_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833776_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833953 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833953_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833953_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833956 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833956_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833956_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833959 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833959_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833959_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833962 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833962_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833962_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833965 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833965_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833965_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833968 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833968_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833968_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833971 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833971_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833971_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833974 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833974_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833974_pib.pdf\n",
"--- Verarbeitung ID: 3765009 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3765009_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3765009_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833977 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833977_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833977_pib.pdf\n",
"--- Verarbeitung ID: 3765012 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3765012_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3765012_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833983 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833983_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833983_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833986 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833986_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833986_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833989 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833989_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833989_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833995 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833995_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833995_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3833998 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3833998_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3833998_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3834001 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3834001_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3834001_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3861200 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3861200_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3861200_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3861206 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3861206_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3861206_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877175 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877175_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877175_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877193 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877193_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877193_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877208 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877208_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877208_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877211 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877211_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877211_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877289 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877289_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877289_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877301 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877301_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877301_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877313 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877313_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877313_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877325 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877325_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877325_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877337 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877337_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877337_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3877349 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3877349_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3877349_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878165 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878165_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878165_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878177 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878177_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878177_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878189 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878189_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878189_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878213 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878213_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878213_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878225 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878225_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878225_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878237 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878237_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878237_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878261 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878261_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878261_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878273 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878273_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878273_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3878285 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3878285_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3878285_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3905336 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3905336_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3905336_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3905348 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3905348_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3905348_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3905360 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3905360_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3905360_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3907523 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3907523_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3907523_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3907559 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3907559_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3907559_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3907595 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3907595_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3907595_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3907631 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3907631_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3907631_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3935061 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3935061_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3935061_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3936264 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3936264_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3936264_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3936300 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3936300_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3936300_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3936336 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3936336_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3936336_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3936372 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3936372_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3936372_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3936408 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3936408_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3936408_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973250 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973250_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973250_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973253 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973253_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973253_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973256 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973256_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973256_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973259 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973259_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973259_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973262 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973262_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973262_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973265 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973265_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973265_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973277 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973277_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973277_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973280 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973280_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973280_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973283 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973283_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973283_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973286 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973286_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973286_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973289 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973289_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973289_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973292 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973292_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973292_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973295 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973295_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973295_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973298 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973298_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973298_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3973301 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3973301_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3973301_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3974111 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3974111_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3974111_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975845 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975845_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975845_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975848 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975848_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975848_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975851 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975851_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975851_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975854 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975854_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975854_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975860 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975860_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975860_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975866 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975866_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975866_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975869 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975869_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975869_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975872 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975872_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975872_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975875 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975875_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975875_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 3975881 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/3975881_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/3975881_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 4069662 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/4069662_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/4069662_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 4069698 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/4069698_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/4069698_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 4069734 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/4069734_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/4069734_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 4069770 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/4069770_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/4069770_pib.pdf\n",
"\n",
"--- Verarbeitung ID: 4069806 ---\n",
"INFO: flyerurl_base gesetzt: https://freenetflyer.s3.amazonaws.com/flyers/4069806_flyer.pdf\n",
"INFO: piburl_base gesetzt: https://freenetflyer.s3.amazonaws.com/pibs/4069806_pib.pdf\n",
"INFO: Upload-Vorgang abgeschlossen.\n"
"--- Verarbeitung ID: 3782151 ---\n"
]
},
{
"ename": "MultipleResultsFound",
"evalue": "Multiple rows were found when one or none was required",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mMultipleResultsFound\u001b[0m Traceback (most recent call last)",
"Cell \u001b[0;32mIn[1], line 62\u001b[0m\n\u001b[1;32m 59\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m--- Verarbeitung ID: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcurrentId\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m ---\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 61\u001b[0m \u001b[38;5;66;03m# In dieser Variablen wird versucht, einen passenden BaseBase-Eintrag aus der Datenbank zu erhalten.\u001b[39;00m\n\u001b[0;32m---> 62\u001b[0m baseRecord \u001b[38;5;241m=\u001b[39m \u001b[43mdbSession\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mquery\u001b[49m\u001b[43m(\u001b[49m\u001b[43mBaseBase\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfilter_by\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprovidercode_base\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcurrentId\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mone_or_none\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 64\u001b[0m \u001b[38;5;66;03m# In dieser Abzweigung wird geprüft, ob überhaupt ein passender Datensatz vorhanden ist.\u001b[39;00m\n\u001b[1;32m 65\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m baseRecord \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 66\u001b[0m \n\u001b[1;32m 67\u001b[0m \u001b[38;5;66;03m# Hier wird eine Warnung ausgegeben, wenn kein BaseBase-Eintrag existiert.\u001b[39;00m\n",
"File \u001b[0;32m~/anaconda3/lib/python3.10/site-packages/sqlalchemy/orm/query.py:2802\u001b[0m, in \u001b[0;36mQuery.one_or_none\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 2774\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mone_or_none\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Optional[_T]:\n\u001b[1;32m 2775\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Return at most one result or raise an exception.\u001b[39;00m\n\u001b[1;32m 2776\u001b[0m \n\u001b[1;32m 2777\u001b[0m \u001b[38;5;124;03m Returns ``None`` if the query selects\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 2800\u001b[0m \n\u001b[1;32m 2801\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 2802\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_iter\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mone_or_none\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
"File \u001b[0;32m~/anaconda3/lib/python3.10/site-packages/sqlalchemy/engine/result.py:1803\u001b[0m, in \u001b[0;36mScalarResult.one_or_none\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1795\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mone_or_none\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Optional[_R]:\n\u001b[1;32m 1796\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Return at most one object or raise an exception.\u001b[39;00m\n\u001b[1;32m 1797\u001b[0m \n\u001b[1;32m 1798\u001b[0m \u001b[38;5;124;03m Equivalent to :meth:`_engine.Result.one_or_none` except that\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1801\u001b[0m \n\u001b[1;32m 1802\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1803\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_only_one_row\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1804\u001b[0m \u001b[43m \u001b[49m\u001b[43mraise_for_second_row\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mraise_for_none\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mscalar\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\n\u001b[1;32m 1805\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n",
"File \u001b[0;32m~/anaconda3/lib/python3.10/site-packages/sqlalchemy/engine/result.py:789\u001b[0m, in \u001b[0;36mResultInternal._only_one_row\u001b[0;34m(self, raise_for_second_row, raise_for_none, scalar)\u001b[0m\n\u001b[1;32m 787\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m next_row \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m _NO_ROW:\n\u001b[1;32m 788\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_soft_close(hard\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[0;32m--> 789\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m exc\u001b[38;5;241m.\u001b[39mMultipleResultsFound(\n\u001b[1;32m 790\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mMultiple rows were found when exactly one was required\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 791\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m raise_for_none\n\u001b[1;32m 792\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mMultiple rows were found when one or none \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 793\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mwas required\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 794\u001b[0m )\n\u001b[1;32m 795\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 796\u001b[0m next_row \u001b[38;5;241m=\u001b[39m _NO_ROW\n",
"\u001b[0;31mMultipleResultsFound\u001b[0m: Multiple rows were found when one or none was required"
]
}
],
......
......@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 1,
"id": "531d8b07-8f2f-4ef6-b92a-d4bf3364e166",
"metadata": {},
"outputs": [
......@@ -10,12 +10,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"eyJraWQiOiJpZUFmc2p0UDJLdDhVM2F2VHlGVEkiLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3R5cGUiOiJhcHBsaWNhdGlvbiIsIm9yZyI6Ik1EIiwiY2xpZW50X2lkIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJjdXN0b21fZGF0YSI6eyJyZXNwb25zaWJsZVRlYW0iOiJCQ1MgTW9iaWxlIn0sImF6cCI6IjhWeWpiUVp5VFZkeDJUMlVPNm1BM1pURWVpb2RIY3AtIiwic2NvcGUiOiJhZ3JlZW1lbnRUZXJtU2hlZXRzOndyaXRlIG1hdWkudnZpOnJlYWQgY3VzdG9tZXJQcm9kdWN0OnJlYWQgcHJvZHVjdE9mZmVyaW5nOnJlYWQgemFwLmNvbnRyYWN0OnJlYWQgemFwLmNvbnRyYWN0OndyaXRlIHN0cyIsImlhdCI6MTc0NTk5OTY0MCwic3ViIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJpc3MiOiJodHRwczovL3N0cy5tZC5kZS92MS9vaWRjLyIsImp0aSI6ImE4bnR5OGlZZEZpRDdfZzEwZnVaTCIsImV4cCI6MTc0NjA4NjA0MH0.z-LLmQHq1Pb2PNFMjLhmxMMhS9PXZoKgfBSWV-brTdIAZs4uYoUHqVTdf2UcQnFWTFuUyguoQG7r3oKANOiGQKGXiMQZrljZvi0Lb_M-ExVjtBxoVXfyea-aSl9I7qAhyq_Ir7x62RyEJzX0A47y3ZxYT8bGsLd6ApE4e1LnvF8f5QEGRNRqhHBdvlGHYAY2NbJxj2TQrzvwdPPsJONiqJWgt7Ab7OErB39qDJY91ZgQYyUQNErAEhqYlJCZu5HgC_AsX-QgH9u4FQbee6LZL-mJ_dHwWbq5c55dLET6cxxBq4h2Ha3E8fOw2tV3wInaTeHOwGmypGGVNog3el7Tgw\n",
"eyJraWQiOiJpZUFmc2p0UDJLdDhVM2F2VHlGVEkiLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3R5cGUiOiJhcHBsaWNhdGlvbiIsIm9yZyI6Ik1EIiwiY2xpZW50X2lkIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJjdXN0b21fZGF0YSI6eyJyZXNwb25zaWJsZVRlYW0iOiJCQ1MgTW9iaWxlIn0sImF6cCI6IjhWeWpiUVp5VFZkeDJUMlVPNm1BM1pURWVpb2RIY3AtIiwic2NvcGUiOiJhZ3JlZW1lbnRUZXJtU2hlZXRzOndyaXRlIG1hdWkudnZpOnJlYWQgY3VzdG9tZXJQcm9kdWN0OnJlYWQgcHJvZHVjdE9mZmVyaW5nOnJlYWQgemFwLmNvbnRyYWN0OnJlYWQgemFwLmNvbnRyYWN0OndyaXRlIHN0cyIsImlhdCI6MTc0NjAxOTcwOCwic3ViIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJpc3MiOiJodHRwczovL3N0cy5tZC5kZS92MS9vaWRjLyIsImp0aSI6InJ1OF9GaUYtZi1GUS00eFdIV2JuSyIsImV4cCI6MTc0NjEwNjEwOH0.BiISXTvunrU_v6KR98pzQ4GZLpEl2f_fSNfaIYDLWq1BsKTeNS7f4hUsBLK91yLGIvyY1vfOuTOeLJeYodX_5CFpQOvx97nKlg46_4g9uqiHYiyd6ehGyAnEp4W-DT_mwJ8PgIqprFeN9Tw5_a5WYMmCZKfMwPP940werC90r0iabClML_J-56_DT8NWN_bm_EUPNrpyWRUb65WCphmbutqbFMc8wBCF4xtwI5VD9v2Nd9sXbFs8TevvX-Weg8WdIVQeQimiigvP1uEEiVimybcK8_Lav3PcDkVcQJ5YbtAx8INIGImAQckeCR7Bvd8mdwdrqc9YWa_oEPg_X89uoA\n",
"---\n",
"{'error': [], 'pciId': 'YLXM-34JW-L1Q4', 'pciPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragsinformationen.pdf', 'pcsPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragszusammenfassung.pdf'}\n",
"{'error': [], 'pciId': '0DRR-IY89-V1MT', 'pciPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragsinformationen.pdf', 'pcsPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragszusammenfassung.pdf'}\n",
"---\n",
"https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragsinformationen.pdf\n",
"https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragszusammenfassung.pdf\n"
"https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragsinformationen.pdf\n",
"https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragszusammenfassung.pdf\n"
]
}
],
......@@ -132,7 +132,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 2,
"id": "4d796b32-df5d-4ba5-a062-4d96b12db2b3",
"metadata": {},
"outputs": [
......@@ -141,7 +141,7 @@
"output_type": "stream",
"text": [
"---\n",
"PCS PDF erfolgreich heruntergeladen und gespeichert unter: ../cache/YLXM-34JW-L1Q4_3877325_pcs.pdf\n",
"PCS PDF erfolgreich heruntergeladen und gespeichert unter: ../cache/0DRR-IY89-V1MT_3877325_pcs.pdf\n",
"---\n"
]
}
......
......@@ -2,13 +2,13 @@
jupyter lab
docker build --platform linux/amd64 -t obsidian:latest .
docker build --platform linux/amd64 -t maui:latest .
# dev
docker run -it -v ./commands:/obsidian/commands -v ./cache:/obsidian/cache -v ./config:/obsidian/config -v ./manager:/obsidian/manager -v ./models:/obsidian/models -v ./files:/obsidian/files -p 80:80 obsidian:latest /bin/bash
docker run -it -v ./commands:/maui/commands -v ./cache:/maui/cache -v ./config:/maui/config -v ./manager:/maui/manager -v ./models:/maui/models -p 80:80 maui:latest /bin/bash
# detached
docker run -it -d obsidian:latest
# ecr
aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin 181802255479.dkr.ecr.eu-central-1.amazonaws.com
......
# Documentation
## Base (base_base)
In dieser Tabelle sind die grundlegenden Tarife gespeichert, die als Ausgangsbasis für das gesamte Datenbankschema dienen. Die hinterlegten Tarife bilden das Fundament, auf dem alle weiteren Strukturen aufbauen.
Beispielhafte Tarife:
```
Green Allnet Flat
Green Allnet Flat mit Handy 5
Green Allnet Flat mit Handy 10
```
### id_base
Dieser Primärschlüssel identifiziert jeden Tarif eindeutig in der Tabelle.
### basegroup_base
Dieses optionale Feld dient als Fremdschlüssel und verweist auf die Tabelle **Basegroup (basegroup_bgro)**, wodurch eine thematische Gruppierung der Tarife ermöglicht wird.
### provider_base
Hier wird der Name des Providers gespeichert, beispielsweise "Freenet" oder "Klarmobil".
### providercode_base
Ein optionales Feld, in dem ein zusätzlicher Provider-Code (Fremdschlüssel) hinterlegt werden kann.
### name_base
Dieses Feld enthält den offiziellen Tarifnamen, wie er vom Provider vorgegeben wird.
### alias_base
Hier kann ein alternativer Name definiert werden, der den offiziellen Tarifnamen ersetzt.
### network_base
In diesem Feld wird das verwendete Funknetz definiert.
**Mögliche Werte:**
- **1**: D1/Telekom
- **2**: D2/Vodafone
- **4**: O2/Telefonica
### type_base
Dieses Feld legt den Tariftyp fest.
**Mögliche Werte:**
- **0**: Unbekannt
- **1**: Voice
- **2**: Daten
### flyerurl_base
Dies ist die URL zum Tarifflyer.
### piburl_base
Dies ist die URL zum Produktinformationsblatt.
### details_base
Dies sind alle Tarifdetails die vom Provider vorgegeben werden (als JSON).
### created_base
Der Zeitstempel, der angibt, wann der Datensatz erstellt wurde.
### updated_base
Der Zeitstempel, an dem der Datensatz zuletzt aktualisiert wurde.
---
## Basegroup (basegroup_bgro)
Diese Tabelle fasst Tarife in Gruppen zusammen, um eine übersichtliche Struktur zu ermöglichen. Die Gruppierung hilft dabei, ähnliche Tarife gemeinsam zu verwalten.
Beispielhafte Tarifgruppen:
```
Red Allnet Flat Tarifgruppe
Green Allnet Flat Tarifgruppe
Blue Allnet Flat Tarifgruppe
```
### id_bgro
Der Primärschlüssel der Tabelle, der jede Tarifgruppe eindeutig identifiziert.
### name_bgro
Hier wird der Name der Tarifgruppe festgelegt.
### created_bgro
Der Zeitpunkt, zu dem die Tarifgruppe erstellt wurde.
### updated_bgro
Der Zeitpunkt, zu dem die Tarifgruppe zuletzt aktualisiert wurde.
---
## Deal (deal_deal)
In dieser Tabelle werden zeitlich befristete Aktionen erfasst. Jede Aktion, jeder Deal verknüpft einen Basis-Tarif aus der Tabelle **Base (base_base)** mit einer Provisiongruppe und definiert spezielle Konditionen.
### id_deal
Der Primärschlüssel der Tabelle, der jede Aktion eindeutig identifiziert.
### base_deal
Dieses Feld ist ein Fremdschlüssel, der auf einen Tarif in der Tabelle **Base (base_base)** verweist.
### provisiongroup_deal
Dieses Feld definiert, unter welcher Provisionsgruppe dieser Tarif verwaltet wird.
### providercode_deal
Ein optionales Feld, das einen zusätzlichen Provider-Code (Fremdschlüssel) speichern kann.
### alias_deal
Ein optionaler, alternativer Name des Deals.
### price_deal
In diesem Feld wird der Preis des Deals als Dezimalwert festgehalten (Netto).
### starts_deal
Das Datum und die Uhrzeit, ab wann der Deal aktiv wird.
### stops_deal
Ein optionales Datum und eine Uhrzeit, bis zu denen der Deal gültig ist.
### provision1_deal bis provision4_deal
Diese Felder enthalten verschiedene Provisionswerte für für die Aktion.
Alle Provisionen zusammengerechnet ergeben die Gesamtprovision.
### created_deal
Der Zeitpunkt, zu dem der Deal-Datensatz erstellt wurde.
### updated_deal
Der Zeitpunkt, zu dem der Deal zuletzt aktualisiert wurde.
---
## Option (option_opti)
Diese Tabelle erfasst spezifische Optionen zu Tarifen, die zusätzliche Leistungen oder Konfigurationen darstellen. Optionen ergänzen die Basistarife und ermöglichen erweiterte Angebotsvarianten.
### id_opti
Der Primärschlüssel der Tabelle, der jede Option eindeutig identifiziert.
### base_opti
Dieses Feld fungiert als Fremdschlüssel und verweist auf den zugehörigen Tarif in der Tabelle **Base (base_base)**.
### provisiongroup_opti
Dieses Feld definiert, unter welcher Provisionsgruppe dieser Tarif verwaltet wird.
### providercode_opti
Ein optionales Feld zur Speicherung eines zusätzlichen Provider-Codes.
### providercategory_opti
Dieses optionale Feld dient der Kategorisierung der Option (Providerkategorie).
### name_opti
Das Feld enthält den offiziellen Namen der Option, wie er vom Provider vorgegeben wird.
### alias_opti
Ein optionaler Alias, der den offiziellen Namen ergänzen oder ersetzen kann.
### price_opti
In diesem Feld wird der Preis der Option als Dezimalwert festgehalten (Netto).
### starts_opti
Das Datum und die Uhrzeit, ab wann die Option aktiv wird.
### stops_opti
Ein optionales Datum und eine Uhrzeit, bis zu denen die Option gültig ist.
### provision1_opti bis provision4_opti
Diese Felder enthalten verschiedene Provisionswerte für für die Option.
Alle Provisionen zusammengerechnet ergeben die Gesamtprovision.
### created_opti
Der Zeitpunkt, zu dem der Options-Datensatz erstellt wurde.
### updated_opti
Der Zeitpunkt, zu dem der Options-Datensatz zuletzt aktualisiert wurde.
---
## Provisiongroup (provisiongroup_pgro)
In dieser Tabelle werden Provisiongruppen verwaltet, die sowohl in Deals als auch in Optionen Anwendung finden. Eine Provisiongruppe legt fest, wie viel Provision prozentual für den Vertrieb freigegeben wird.
### id_pgro
Der Primärschlüssel der Tabelle, der jede Provisiongruppe eindeutig identifiziert.
### name_pgro
Hier wird der Name der Provisiongruppe gespeichert.
### percent_pgro
Dieser Dezimalwert legt den Provisionsprozentsatz fest. Standardmäßig beträgt dieser 0.00.
### created_pgro
Der Zeitpunkt, zu dem der Datensatz der Provisiongruppe erstellt wurde.
### updated_pgro
Ein optionales Feld, das den Zeitpunkt der letzten Aktualisierung der Provisiongruppe festhält.
No preview for this file type
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment