Das MPI-Zope3-Kochbuch
Zope und Python bringen eine riesige Klassen/Komponenten-Bibliothek mit. Leider muss man diese Bibliothek komplett kennen, um zu wissen, was man eigentlich machen muss, wenn man ein gegebenes Problem hat. In diesem Wiki-Topic sollen nun Hinweise und Lösungen aufgezeigt werden, ausgehend von speziellen Problemen.
Zugriff auf bestimmte Objekte
Oft hat man in Zope3 das Problem
habe Objekt vom Typ X, brauche Objekt vom Typ Y . Hier ist eine Sammlung solcher Probleme mit entsprechenden Lösungen.
Der root-Folder ist das Grundverzeichnis der Daten in der
ZodB.
Es gibt mehrere Möglichkeiten, um auf diesen zuzugreifen.
Den
RootFolder kann man in
Python und auch in
Tal (also in
PageTemplates) ermitteln.
Ausserdem kann man den
RootFolder auch über eine manuell gestartete zusätzliche
TransAction erhalten - siehe
RootFolder#NewTransaction .
Eine bestimmte VieW eines Objektes
... In Python
Eine
VieW läßt sich erzeugen, wenn man 3 Dinge hat: Ein Objekt
context , einen
ReQuest request , der evtl. schon zu einer anderen
VieW gehört, und den Namen
name der gesuchten
VieW . Im Kochbuch ist auch beschrieben, wie man z.B. den passenden
name der default-
VieW ermittelt.
from zope.app import zapi
myview=zapi.queryMultiAdapter((context,request),name=name)
In
TaLes kann man mit der
@@-Notation in
Path-Expressions leicht
VieWs bilden.
Beispiel:
<div tal:content="context/@@name_der_view" />
Das geht auch z.B. mit Unterobjekten eines Containers:
<div tal:repeat="subobject python: context.values()">
<a tal:attributes="href subobject/@@absolute_url" tal:content="subobject/__name__" />
</div>
Der HTTP-Header
Der HTTP-Header hängt am
ReQuest-Objekt. Das geht aus allen
BrowserViews heraus - auch innerhalb von
PageTemplates - z.B. so:
<div tal:content="request/PATH_INFO"
Welche Felder und Informationen genau zur Verfügung stehen, findet man leicht herraus, indem man sich das
ReQuest -Objekt mit
dir(request) oder
print request genauer ansieht.
HTTP-Formular-Daten
Siehe
HttpForms.
Das Request-Objekt
Das
ReQuest-Objekt steht in
PageTemplates (siehe
ReQuest#GetTal ) und in
VieWs (siehe
ReQuest#GetFromView ) zur Verfügung.
Wie man das
ReQuest-Objekt
aus dem Nichts ermitteln kann, ist unter
ReQuest#GetMagic beschrieben.
Ein bestimmtes UtilitY
Siehe
UtilitY#GetOne .
Namen herrausfinden
... aller Utilities, die ein bestimmtes Interface erfüllen
Siehe
UtilitY#UtilityList .
... eines Objektes als UrL
Für alle
ContentObjects, die im Content-Baum der
ZodB liegen, kann die eindeutige
UrL ermittelt werden. Verantwortlich dafür ist, dass diese Objekte sog.
LocationNs? sind. Das selbe gilt auch für alle
BrowserViews.
Es existiert eine
BrowserView, für alle
ContentObjects:
absolute_url . Diese kann man z.B. im Browser selbst aufrufen (was nicht viel Sinn macht):
http://server/meinobjekt/absolute_url
Da sich alle
BrowserViews in
PageTemplate mit der
AtAt -Notation einbinden lassen, kann man auch solche Konstruktionen wagen:
<div tal:content="context/@@absolute_url" />
<a tal:attributes="href string:${context/@@absolute_url}/edit.html">Dieses Objekt bearbeiten</a>
<a tal:repeat="subobject context"
tal:attributes="href string:${subobject/@@absolute_url}"
tal:content="string:Objekt ${subobject/__name__} bearbeiten" />
In Python
from zope.app import zapi
url=zapi.absoluteURL(mein_objekt,request)
Hinweise:
- Eine UrL kann nur dann gebildet werden, wenn ein ReQuest -Objekt vorhanden ist - es geht definitv nicht ohne.
In Python ohne passendes ReQuest-Objekt
Ohne
ReQuest kann man keine absolute URL berechnen, allerdings ist folgende Lösung recht brauchbar und liefert eine
UrL relativ zum
RootFolder (z.B.
/calendar/event.html ):
from zope.app import zapi
def getUrl(obj):
return zapi.getPath(obj)
Ermitteln der bereitgestellten Interfaces
Siehe
InterFace#ObjectsInterfaces.
Ermitteln aller bereitgestellten SchemaFields
Dazu kombiniert man die Möglichkeiten unter
InterFace#ObjectsInterfaces und
InterFace#GetFields .
Achtung: Objekte implementieren teils sehr viele
InterFaces, wovon etliche keinen Content beschreiben (z.B.
ILocation von
LocatioN-Objekten ).
Objekte mit komplexen Datenstrukturen
Zope ist in der Lage, Content in einzelnen Objekten zu speichern, der komplexer als eine reine Menge von Feldern ist. z.B. können Felder auch aus Listen bestehen, deren Einträge jeweils wieder Mengen von Feldern sind. Weiteres dazu gibt's unter
KomplexerContent.
Achtung: Wird der Content zu komplex, sollte man ihn Form von Containern und enthaltenen Objekten abbilden.
Objekte mit grossen Datenmengen
Das
z3c.extfile -Paket speichert grosse Datenmengen im Filesystem, wobei nur eine Referenz in der
ZodB abgelegt wird:
http://svn.zope.org/z3c.extfile/trunk/src/z3c/extfile/ .
Einfache benutzer-editierbare Webseiten
Wenn man einfach nur statische Seiten braucht - vielleicht sogar unter Benutzerkontrolle, kann man entweder ZPT-Objekte benutzen. Man kann sich aber auch der verschiedenen Renderer von Zope bedienen. Diese gestatten - ähnlich wie in einem Wiki - ohne HTML-Kenntnisse - Seiten (auch mit Stil-Elementen) zu erstellen. Solche Funktionalität läßt sich auch leicht in eigene
ContentObjects einbauen.
Eine Beispielimplementation gibt's unter
MpgSiteSimplepage .
Unter
FbI18n ist eine spezielle Datenstruktur beschrieben, mit der man verschiedene Sprachversionen von wahlfreien Datenfeldern (z.B.
TextLine oder
SourceText) verwalten kann. Damit ist es auch möglich mehrsprachige Daten und sprachunabhängige Daten (z.B. Datumsangaben) in einem Objekt zu kombinieren.
Versionskontrolle an Objekten
Zope bringt dafür das Paket
zope.app.versioncontrol mit.
Die Wikiseite
WorkFlow läßt sich darüber aus.
Container
Automatisch Unterobjekte in einen neuen Container legen
Automatisch Unterobjekte in einem
ConTainer anlegen, der gerade erst erzeugt wurde, ist etwas tricky. Unter
ConTainer#AutoSubobjects ist die Problematik sammt lösung beschrieben.
Anlegen von Objekten durch nicht authentifizierte Benutzer
Siehe
AddingView#AddingPermission
Löschen von mehreren (oder allen) Unterobjekten
Dabei gibt es einiges zu beachten. Informationen gibt's unter
ConTainer#MassDeletion .
Umbenennen von Objekten im ConTainer
Dafür gibt's ein spezielles
InterFace, das wie folgt benutzt wird:
from zope.copypastemove.interfaces import IContainerItemRenamer
def rename_object(container,oldname,newname):
icir=IContainerItemRenamer(container)
renameItem(oldname,newname)
Wenn der neue Name schon bzw. der alte nicht vorhanden ist, gibt's eine
ExCeption .
Fein granulierte Berechtigungen (z.B. Anlegen, aber nicht Löschen...)
Oft ist es sinnvoll, die Berechtigungen zum Anlegen und zum Löschen von Objekten in einem
ConTainer unterschiedlich zu behandeln, so dass z.B. anonyme nutzer Objekt anlegen aber nicht wieder Löschen oder auch nicht wieder lesen dürfen (ähnlichen einem FTP-Incomming-Verzeichnis).
Unter
ConTainer#GranularRights ist beschrieben, wie das geht.
Anlegen eines Objektes in einem ConTainer mit automatischer Namenswahl
Unter
ConTainer#NameChooser ist beschrieben, wie man Zope die Namenswahl über neue Unterobjekte überläßt.
Anlegen eines Objektes im Browser ohne automatische Namenswahl
Üblicherweise legt man im
AddForm fest, welchen Namen ein neues Objekt erhalten soll. Läßt man das Namensfeld frei, wird ein Name automatisch vergeben (per
NameChooser). In bestimmten Situationen hätte man gerne mehr Kontrolle über den vergebenen Namen - wie das geht, ist unter
AddForm#NameSelection beschrieben.
Im ZmI können in einem Container keine Unterobjekte angelegt werden. Was kann man tun?
Vielleicht wurde vergessen, die
<browser:containerViews> -Direktive im
ZcmL zu definieren?. Ohne diese hat das
ZmI keinen Zugriff auf die
ConTainer .
Wie bekommt man eine Liste der möglichen Unterobjekt-Typen?
Das ist unter
ConTainer#ValidSubobjects beschrieben.
Rendern von Text
Unter
Rendern von Text versteht man das Umrechnen von einer Textform (z.B. Twiki-Quelle,
StructuredText,
RestructuredText oder auch reiner ASCII-Text) in etwas anderes. Das bedeutet üblicherweise, dass man den Text aus der Speicherform (Quelle) in die Transportform (z.B. XHTML) überführen will.
Zope3 bringt ein Konzept mit, das folgendes Umfasst:
- Source-Text wird in ein spezielles Objekt umgewandelt, das ein vone
zope.app.renderer.interfaces.ISource abgeleitetes Source-Format-spezifisches InterFace (z.B. zope.app.renderer.rest.IReStructuredTextSource erfüllt.
- Ein VieW, die sich auf dieses InterFace und auf den aktuellen ReQuest versteht (das ist meistens HTML) macht aus dem Source-Objekt einen UniCode -String - den gerenderten Text.
- Diesen String gibt man in der aktuellen VieW zurück.
Und so wird's gemacht:
from zope.security.proxy import removeSecurityProxy
class MyView(BrowserView):
def __call__(self):
rest=zapi.createObject(None,'zope.source.rest',self.context.my_restrucuted_textstring)
view=getMultiAdapter((removeSecurityProxy(rest),request),IBrowserView,'')
html=view()
return html
Einen eigenen Renderer schreiben
Wenn man ihn wie ein Plugin in das Renderer-Framework integrieren will, muss man lediglich eine
FactorY, die das
zope.app.renderer.interfaces.ISource -Objekt erzeugt, und die Renderer-VieW schreiben. Die Renderer-VieW ist dabei das eigentlich Arbeitstier - sie geht den Quelltext Zeile für Zeile durch und baut daraus z.B. HTML. Man kann aber z.B. auch einen externen Renderer aufrufen (z.B. ein Shellkommando).
MpgSite bringt einen Renderer mit, der Twiki-Sourcen in valides XHTML verwandelt (
FbTwiki ). Dieser Renderer nimmt auch zusätzliche Informationen wie das aktuelle
ContentObject entgegen, um z.B. Bilder einzubinden.
Eine Umleitung schicken
Siehe
BrowserView#HttpRedirect
Zurückliefern statische Objekte
Oft werden statische Objekte benötigt - z.B. Icons oder CSS-files. Für diese will man nicht eigene
VieWs schreiben. Das muss man auch nicht, denn es gibt
Browserresourcen.
Zurückliefern bestimmter Inhaltetypen (z.B. Bilder)
Eine
VieW kann beliebige Daten zurückliefern - also auch z.B. ein in Echtzeit errechnetes Bild. Dazu muss man manuell den Content-Typ setzen, wobei man den MIME-Type der Daten kennen muss.
Ein Beispiel:
views.py
from zope.app.publisher.browser import BrowserView
class PictureView(BrowserView):
def __call__(self):
self.request.response.setHeader('Content-Type','image/jpeg')
return self.content.picture_jpeg
Hinweise:
- Das Datenfeld
picture_jpeg muss auf jeden Fall ein ganz normaler str -String sein - also z.B. nach dem BytesLine -Schema (siehe SchemaInterFaces) - kein UniCode-String.
- Kennt man den MIME-Type nicht, kann man die libmagic benutzen - wie z.B. in FbFile .
Zurückliefern grosser Datenmengen
Zope unterstützt den Transfer grosser Datenmengen währende eines
ReQuests zum Benutzer, die nicht komplett im Speicher liegen müssen (
BloBs). Dazu muss die
VieW lediglich anstelle von
UniCode ein File-Objekt zurückliefern.
filmview.py
from zope.app.publisher.browser import BrowserView
MOVIE_FOLDER="/DATA/movies"
class FilmDownload(BrowserView):
def __call__(self):
fileobj=open(MOVIE_FOLDER + "/" + self.request['moviename'] + ".avi",'rb')
self.request.response.setheader('Content-Type','video/x-msviewo')
return fileobj
Hinweise:
- Es versteht sich von selbst, dass der open-Aufruf besser abgesichert werden muss. Bei diesem Beispiel sind z.B. Angriffe durch
.. -Komponenten in der per ReQuest übergebenen Variable sehr leicht machbar.
Downloadbare VieWs
Manchmal will man explizit, dass Daten eines
VieWs heruntergeladen und nicht im Browser angezeigt werden. Das kann z.B. ein grosses Bild sein, dessen Anzeige einen Browser überfordern würde.
Mit dem
Content-Disposition -HTTP-Header ist es möglich, den Browser nicht nur anzuweisen, den Link downzuloaden (und nicht anzuzeigen) - man kann auch noch einen Dateiennamen vorschlagen.
Ein Beispiel:
views.py
from zope.app.publisher.browser import BrowserView
class BigPictureView(BrowserView):
def __call__(self):
self.request.response.setHeader('Content-Type','image/jpeg')
self.request.response.setHeader('Content-disposition','attachment; filename=bigimage.jpg')
return self.content.generate_big_picture_jpeg()
Hochladen grosser Datenmengen (z.B. 100MB) ins Zope
Zope ist nicht geeignet, grosse Datenmengen in HTTP-ReQuests zu akzeptieren, da diese immer erst komplett in den Speicher müssten. Ein apache-Plugin kann da weiterhelfen:
http://www.infrae.com/products/tramline
Formulare
Weiterleitung nach Hinzufügen eines Objektes
Wenn die
<addform -ZcmL-Direktive benutzt wurde, um ein Formular automatisch aus einem Schema-InterFace zu erzeugen, will man den Benutzer nach dem Hinzuügen des Objektes evtl. an eine bestimmte URL verweisen (z.B. eine mit dem textuellen Hinweis, dass alles funktioniert hat).
Die Methode
nextURL der assoziierten
VieW-Klasse kann dafür benutzt werden, wobei man jedoch beachten muss, dass man in dieser Methode keinen direkten zugriff auf das
context -Objekt hat. Beispiel:
views.py:
class AddViewClassMixin(object):
def add(self,content):
located = self.context.add(content)
self.absURL = zapi.absoluteURL(located, self.request)
return located
def nextURL(self):
return self.absURL + '@@successful_added.html'
configure.zcml:
[...]
<addform
label="New MyObject object"
name="add_myobject.html"
schema="interfaces.IMyObject"
class="views.AddViewClassMixin"
permission="zope.ManageContent"
/>
[...]
Weiterleitung nach Bearbeitung eines FormLib -Formulares
Dazu ist eine eine zusätzliche Methode in der (
FormLib -)
VieW-Klasse des Formulares nötig:
from zope.formlib import form
class MyFormView(form.EditForm):
[...]
def render(self):
if self.errors is None or self.errors:
return super(myFormView,self).render()
self.request.response.redirect(my_new_url)
[...]
Javascripts mit Markup produzieren Fehler
Wie man Probleme vermeidet, die beim Einbetten von
JavaScript-Code in
PageTemplates entstehen, ist unter
PageTemplate#EmbeddedJavascript beschrieben.
Python-Module aus PageTemplates herraus nutzen
Wie das geht, ist unter
PageTemplates#PythonModules beschrieben.
PageTemplates für anderen Content als Webseiten
PageTemplates lassen sich für alle Arten von Dokumenten benutzen, die auf XML basieren - z.B. RSS-Feeds. Hinweise und ein Beispiel gibt's unter
PageTemplates#ArbitraryXml.
Feststellen, ob der aktuelle Benutzer authentifiziert ist
Siehe
PrinCipal#IsAuthenticated .
Eindeutige IDs für HTML-Tags
Formulare erzeugen automatisch eindeutige IDs -
InputWidgets beherrschen das von Haus aus. Beim Anzeigen von Daten muss man selbst Hand anlegen. Wie das geht, ist unter
KomplexerContent#UniqueLabels beschrieben.
"Dienste" für den Webauftritt
Hier werden Dinge erklärt, die Objektübergreifend von interesse sind - z.B. Erzeugung von Indizes oder Suchmaschinen.
Globale Konfigurationsparameter
Will man parameter einer
ZopeInstance vorgeben (z.B. eMail-Adresse des Webmasters, ...), kann man das in einem Browserkonfigurierbaren
UtilitY machen. Ein Beispiel dafür ist
MpgSiteMpgconfig - ein
UtilitY, das internationalisierte Bezeichnungen (Herr, Mr. , Monsieur, ...). Eine etwas einfachere Methode ist unter
ZcmL#GlobalConfiguration beschrieben.
Webseiten in Druckansicht
Oft ist es sinnvoll, von einer Website eine Druckansicht zu zeigen. Man kann (und sollte) dabei viel mit
CSS machen, jedoch kann zumindest CSS 2.0 noch nicht genug. Das Paket
MpgSite enthält zusätzliche Unterstützung um
ContentObjects druckfreundlich darzustellen:
MpgSitePrintable .
Regelmäßig ausgeführte Aufgaben
Zope3 kennt keinen eigenen Mechanismus, um Aktionen zu gegebenen Zeiten anzustoßen. Üblicherweise benutzt man dafür
cron und
wget , um ein
VieW eines Objektes aufzurufen. Auch der Aufruf per
cron und
XmlRpc ist denkbar.
Es existieren auch einige Methoden, getimte Methodenaufrufe direkt in den Zopeserver zu integrieren, jedoch wird im allgemeinen davon abgeraten.
Suchen
Realisieren einer Volltextsuche
Wie man eine Volltextsuchmaschine aufsetzt, wird unter
SuchenImZope beschrieben.
Objekte finden, die vor dem IntId- UtilitY angelegt wurden
ContentObjects, die angelegt wurden, bevor ein
IntId-
UtilitY registriert war, werden von einer Suchmaschine nicht gefunden. Man kann das mit etwas Aufwand nachholen - mehr dazu unter
IntId#MissingIds .
Wie man die
UrLs des eigenen Webauftritts so gestaltet, dass sie nicht vor Sonderzeichen strotzen, sondern
schön aussehen, ist unter
NiceUrls beschrieben.
Ein Navigationsmenü
Ein einfaches Navigationsmenü erstellt man, indem man es einfach in das
PageTemplate -File schreibt, das das
view - bzw
page -Makro (siehe
MeTaL ) des aktuellen
SkiNs enthält. Dieses Menü ist dann aber statissch. Ein dynamisches Menü wir ebenfalls in das
view -Makro integriert, bindet dann aber z.B. eine
VieW ein, in der das
ReQuest -Objekt ausgewertert wird, um das Menü entsprechend der aktuellen
UrL anzupassen. Das Paket
MpgSite hat dafür eine lösung parat:
MpgSiteAutoMenu .
Eine leere ZodB initialisieren
Hat man eine neue
ZopeInstance mit einer leeren
ZodB, kann man sich an das
zope.app.appsetup.IDatabaseOpenedEvent -EvenT anhängen, testen, ob der
RootFolder noch leer ist und bei Bedarf
ContentObjects anlegen, die eine bestimmte Anwendung aufspannen.
Fehlerbehandlung
Wie passt man die Fehlermeldungen in Zope3 an?
Alle Fehlermeldungen, die unter Zope3 angezeigt werden, basieren darauf, dass man (oder auch irgendein Fremdprodukt) in Python einen event
raise d . Zope versucht, anhand des events eine
VieW zu finden und zeigt diese an - siehe
CustomMessages
Fehler Behandeln, aber Änderungen teilweise speichern
Es ist möglich, per
ExCeption einen Fehler an Zope zu melden, die aktuelle
TransAction und damit alle im
ReQuest gemachten Änderungen zurückzunehmen und trotzdem einige Daten in der
ZodB zu speichern.
Unter
TransAction#SingleObject is beschrieben, wie das geht.
Sicherheit
Zope läßt den Manager nicht mehr rein
Es kommt vor, dass man sich aus Zope komplett aussperrt, indem man das Authenticator-Utility falsch konfiguriert. Beheben läßt sich das, indem man dem
Authenticator-Utility die
Registrierung entzieht.
Testen, ob man eine bestimmte Berechtigung an einem Objekt besitzt
Siehe
PerMission#CheckPermission .
Testen, ob man das Recht hat, auf ein bestimmtes Attribut zuzugreifen
Wie das geht ist unter
PerMission#CanAccess beschrieben.
Testen, ob der aktuelle Benutzer angemeldet ist
So testet man, ob der aktuelle Benutzer anonym ist:
from zope.app.security.interfaces import IUnauthenticatedPrincipal
anonymous=IUnauthenticatedPrincipal.providedBy(request.principal)
Alle nicht anonymen Benutzer sind auf die eine oder andere Weise authentifiziert.
Objekte, Klassen, Komponenten
Registrieren einer Komponente per Python
Siehe
ZopeComponents#RegistrationPython .
Ermitteln der Felder eines InterFaces
Siehe
InterFaces#GetFields.
Hinzufügen von Marker-InterFaces zu existierenden Klassen
Marker-Interfaces lassen sich an existierenden (Fremd-)Klassen durch ein
zusätzliches
ZcmL -
<class> -Statement anhängen. Ganz allgemein kann
man beliebig viele class-Statements pro Klasse haben - solange man nicht z.B.
ein Attribut mehrfach definiert - dann muss man auf
ZcmL#OverRide zurückgreifen.
Siehe
InterFaces#ListAll.
Sicherstellen, dass ein bestimmtes Utility existiert
Mit folgender Methode kann man sicherstellen, dass ein bestimmtes Utility existiert (am Beispiel des
IntID?-Utilities):
from zope.app.appsetup.bootstrap import ensureUtility
from zope.app.intid.interfaces import IIntIds
ensureUtility(context, IIntIds, '', IntIds, copy_to_zlog=False)
Mehrstufiges Adaptieren
Zope baut von sich aus keine Kette von
Adaptern auf. Einige Hinweise dazu gibt's unter
ZopeComponents#AdapterChain .
Wie das geht, ist unter
InterFace#MarkerInterfaces beschrieben.
Ereignissbehandlung
Vermeidung mehrfacher Ereignisbehandlung
Wie man vermeidet, dass nicht-idempotente Operationen im Rahmen einer Aktion mehrfach per
EvenT aufgerufen werden, ist unter
EvenT#MultipleEvents beschrieben.
Netzwerkverhalten
Beschränken der ZopeInstance auf bestimmte IPs/Ports
Siehe
ZopeInstance#NetWork.
Fehlermeldungen (Zope-interne ExCeptions)
ForbiddenAttribute
Alle Objekte sind unter Zope mit jeweils einem
SecurityProxy -Objekt gesichert. Der
SecurityProxy gibt Zugriffe auf Methoden und Attribute des Objektes nur im Rahmen der vorher per
ZcmL definierten Rechte, die der jeweilige Benutzer hat, frei. Mehr dazu gibt's unter
SecurityProxy#ForbiddenAttribute.
UnpickleableError
Dieser Fehler tritt meist auf, wenn man ein per
SecurityProxy gesichertes Objekt an ein anderes Objekt (z.B. als Attribut) anhängen will. Mehr dazu unter
ContentObject#UnpickleableError .
NotYet
Dieser Fehler tritt immer dann auf, wenn versucht wird, eine
KeyReference von einem
ContentObject zu ermitteln, dass noch keine hat - sprich: das noch nicht in der
ZodB abgelegt wurde.
Prominenter Schuldiger für diesen Fehler ist das
IntId -
UtilitY, dass sich jedes in einen
ConTainer gelegte
ContentObject hernimmt und dessen
KeyReference in einem Index ablegt. Unter
ConTainer#AutoSubobjects ist eine Lösung für dieses Problem skizziert.
POSKeyError
Wenn in der ZODB eine Inkosistenz auftritt, wird dieser Fehler gemeldet - jedoch nur ans Logfile: der Benutzer sieht etwas in der Art von
System Error occured . Es ist keine Methode bekannt, das Problem automatisch zu beseitigen - jedoch gibt es hier einige Tips:
PosKeyError .
[Zurück zum Start]