jStore - simpler Key/Value Store mit JSON backend

Code-Stücke können hier veröffentlicht werden.
Antworten
monkey
User
Beiträge: 7
Registriert: Montag 22. März 2010, 16:56

Erst mal ein Hallo ins Forum :)

ich hab mal versucht die Speicherung etwas zu abstrahieren. Code findet sich auf http://paste.pocoo.org/show/192572/

Kritik und Vorschläge sind natürlich Willkommen :)


Neuen Store erstellen:

Code: Alles auswählen

users = jStore("users")
Daten einpflegen:

Code: Alles auswählen

for i in [["tim","tim@mail.de"], ["holger","holger@mail.de"], ["daniel","daniel@mail.de"]]:
    users.add(i[0], i[1])
Daten Abfragen:

Code: Alles auswählen

print users.get("tim")
{'info': {'time_create': 1269273246.5548069, 'time_update': ''}, 'value': 'tim@mail.de', 'id': 1, 'key': 'tim'}

print users.get(["tim", "daniel"])
[{'info': {'time_create': 1269273246.5548069, 'time_update': ''}, 'value': 'tim@mail.de', 'id': 1, 'key': 'tim'}, {'info': {'time_create': 1269273246.5548141, 'time_update': ''}, 'value': 'daniel@mail.de', 'id': 3, 'key': 'daniel'}]

print users.get([1, 3])
[{'info': {'time_create': 1269273246.5548069, 'time_update': ''}, 'value': 'tim@mail.de', 'id': 1, 'key': 'tim'}, {'info': {'time_create': 1269273246.5548141, 'time_update': ''}, 'value': 'daniel@mail.de', 'id': 3, 'key': 'daniel'}]
Mit Key's arbeiten:

Code: Alles auswählen

print users.getkeys()
['daniel', 'holger', 'tim']

print users.getkeys(reverse=True)
['tim', 'holger', 'daniel']

print users.getkeys(limit=2)
['daniel', 'holger']

print users.getkeys(like="da")
['daniel']
Mit Id's arbeiten:

Code: Alles auswählen

print users.getids()
[1, 2, 3]
print users.getids(reverse=True)
[3, 2, 1]
print users.getids(limit=2, reverse=True)
[3, 2]
Ein Beispiel:

Code: Alles auswählen

users = jStore("users")

for i in [["tim","tim@mail.de"], ["holger","holger@mail.de"], ["daniel","daniel@mail.de"]]:
    users.add(i[0], i[1])

citys = jStore("posts")

citys.add("Cologne", [users.get("tim")["id"], users.get("daniel")["id"]])
citys.add("Berlin", [users.get("holger")["id"]])
    

for city in citys.getkeys():
    for userid in citys.get(city)["value"]:
        print "%s - %s" % (city, users.get(userid)["key"])
        
citys.rename("Cologne", "Köln")

for city in citys.getkeys():
    for userid in citys.get(city)["value"]:
        print "%s - %s" % (city, users.get(userid)["key"])
gibt aus:

Code: Alles auswählen

Berlin - holger
Cologne - tim
Cologne - daniel

Berlin - holger
Köln - tim
Köln - daniel
BlackJack

Das ist alles nicht sicher, diese Lockdatei funktioniert nicht wirklich.

Was sollen die ganzen sinnlosen ``return``\s?

Warum kann man nicht über ``store[key]`` und ``store[key] = value`` zugreifen?

Diese Typtesterei ist gruselig.

Bei `getkeys()`/`getids()` ist das ``if``/``else`` auf `reverse` jeweils überflüssig.

`Exception` würde ich nicht ``raise``\n, sondern lieber etwas passendere wie `ValueError` oder `KeyError`. Und dann auch syntaktisch nicht so, sondern direkt selbst das Exemplar erzeugen.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Zeile 1: Der shebang ist sinnlos, weil nach Ausführung der Datei lediglich Module importiert und Klassen und Funktionen definiert werden (er schadet aber auch nicht) ;)

Die Zeilen 2 und 3 sind ebenfalls überflüssig, da das gesamt Skript aus ASCII besteht.

Zeile 10: niemals reines except verwenden! Immer mit angeben, welche Exception abgefangen werden soll. In dem Fall ist das ImportError.

Zeile 114: Ich würde die Klasse JsonStore (auch nicht JSONStore) nennen, vgl. PEP8

Mal folgt nach Docstrings eine Leerzeile, mal nicht. Warum?
monkey
User
Beiträge: 7
Registriert: Montag 22. März 2010, 16:56

Vielen Dank erstmal für die vielen Vorschläge. ich habe folgende Sachen mal behoben.

1. Shebang entfernt
2. Encoding entfernt
3. Exception ImportError angepasst
4. Klasse nach JsonStore umbenannt vgl. PEP8
5. sinnlose ``return``\s entfernt

http://paste.pocoo.org/show/192834/

Zu den Kommentaren von BlackJack ergeben sich bei mir einige fragen bzw. verstehe ich sie nicht so ganz.
Das ist alles nicht sicher, diese Lockdatei funktioniert nicht wirklich.
Klar das lock funktioniert nur im Moment des Schreibens und nicht z.B. bei folgendem Code oder mehrere Skripten die gleichzeitig Zugriff auf den Store haben. Dazu einen Lösungsvorschlag oder Ansatz?

Code: Alles auswählen

usersa = JsonStore("users")
usersa.add("tim","tim@mail.de")

usersb = JsonStore("users")
usersb.add("holger","holger@mail.de")

usersa.sync()
usersb.sync()
Warum kann man nicht über ``store[key]`` und ``store[key] = value`` zugreifen?
Jeder Eintrag bekommt ja noch eine id + andere Zusatz-Informationen die momentan in einem ausgegeben werden. alternativ könnte man auch dazu ja einen Parameter verwenden.

Code: Alles auswählen

users.get("tim") = value
users.get("tim", info=True) = {'time_create': 1269342881.2143681, 'time_update': '', 'id': 1, 'key': 'tim'}
Diese Typtesterei ist gruselig.
Der Vorteil besteht doch darin das ich mit einer Funktion einzelne oder mehrere keys bzw. ids abfragen kann. Wie soll ich das ohne Typtest machen! Oder ist es besser das in einzelne Funktion zu verpacken. (Warum ist Typtesterei gruselig?)

Bei `getkeys()`/`getids()` ist das ``if``/``else`` auf `reverse` jeweils überflüssig.
Überflüssig weil man das selber mit sort() auf den Rückgabewert anwenden kann?

`Exception` würde ich nicht ``raise``\n, sondern lieber etwas passendere wie `ValueError` oder `KeyError`. Und dann auch syntaktisch nicht so, sondern direkt selbst das Exemplar erzeugen.
Ok hier mein größtes Problem, was passendes wie `ValueError` oder `KeyError` leuchtet mir ja noch ein. Aber was meinst du mit Exemplar erzeugen vielleicht kannst du mir dazu ein Beispiel geben. Mit google hab ich zwar einiges gefunden aber nichts was mir für diese Fall schlüssig erschien.
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

monkey hat geschrieben:
Diese Typtesterei ist gruselig.
Der Vorteil besteht doch darin das ich mit einer Funktion einzelne oder mehrere keys bzw. ids abfragen kann. Wie soll ich das ohne Typtest machen! Oder ist es besser das in einzelne Funktion zu verpacken. (Warum ist Typtesterei gruselig?)
Es gibt `isinstance`.
monkey
User
Beiträge: 7
Registriert: Montag 22. März 2010, 16:56

monkey
User
Beiträge: 7
Registriert: Montag 22. März 2010, 16:56

Ok, ich habe noch einige andere Probleme gefunden und werde das jetzt nochmal überarbeiten, bei den oben aufgeführten fragen würde ich trotzdem noch einen kleinen Anstoß benötigen.
Benutzeravatar
snafu
User
Beiträge: 6744
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Nutze doch bitte das with-Statement beim Hantieren mit Dateien:

Code: Alles auswählen

with open(filename, 'w') as f:
    f.write(content)
Statt diesem `key_or_id` kannst du das auch gleich in 2 Funktionen aufteilen: `get_key()` u. `get_id()`

Statt das zu erzeugende Wörterbuch mit Copy&Paste zu erzeugen, könntest du am Anfang der Funktion eins machen und dies dann zurückgeben.

Und zum Typcheck bei der Liste könntest du eine Funktion einbauen, die wirklich alle Elemente prüft und nicht nur das erste:

Code: Alles auswählen

def has_exlusive_type(sequence, type):
    return all(isinstance(x, type) for x in sequence)
BlackJack

@monkey: Das Problem bei den Typtests ist, das sie eben nur noch genau mit den getesteten Typen funktionieren, also das "duck typing" aushebeln.

Bei den Ausnahmen meinte ich ``raise Exception('message')`` statt ``raise Exception, 'message'``. Letzteres habe ich in aktuellen Python-Programmen noch nie gesehen und ab Python 3 ist das auch keine gültige Syntax mehr.

Noch zwei Anmerkungen zum letzten Quelltext:

Was hast Du Dir dabei gedacht: ``file = open("%s" % (filename), 'r')``? Was soll die Zeichenkettenformatierung da bewirken?

Und: ``if "ids" and "keys" not in key_map:`` macht sicher nicht das was Du dachtest. Ich klammere das mal wie es ausgeführt wird: ``if "ids" and ("keys" not in key_map):``. Der Term vor dem ``and`` ist immer wahr, da es eine nicht-leere Zeichenkette ist, also könnte man das ``"ids"`` auch weglassen und nur ``if "keys" not in key_map:`` schreiben.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Die Lock-Datei finde ich suboptimal. Wenigsten würde ich einen atexit-Hook definieren, der diese wieder abräumt, in der Hoffnung, ein abstürzendes Python kann zumindest diesen Hook noch ausführen. Denn andernfalls verklemmt dein System durch eine übergebliebene Lock-Datei.

Du machst den selben Fehler wie ich hier http://gist.github.com/173209, indem du die neue Datei auf die alte schreibst. Geht dort etwas schief, hast du auch alle alten Daten verloren. Ich habe auch eine KVStore-Version, die in eine neue Datei schreibt und dann `os.rename` benutzt. Das ist sicher, weil (garantiert unter Unix/OSX und wahrscheinlich unter Windows) atomar.

Wie du in meinem Code siehst, nutze ich Filesystem-Locks statt einer Lock-Datei. Das funktioniert zwar nicht unter Windows, doch da hatte ich damals beschlossen, das wäre mir egal. Ich versuche auch, Schreiboperationen zu vermeiden, wenn sich gar nichts geändert hat bzw. Änderungen zu erkennen und dann einen "concurrency error" zu werfen.

Was du dir vielleicht abgucken kannst, sind die "__"-Methoden, die das ganze mehr wie ein dict aussehen lassen. Das ist IMHO das schönere API.

Übrigens, in `file_lock` fehlt ein `close`. Ohne das löscht das `file_unlock` unter Unix/OSX die Datei nicht, auch wenn du `remove` aufrufst. Du hast offenbar bei deinen Beispielen das Glück, das Pythons Referenzen zählender Aufräumalgorithmus die Datei früh genug wieder schließt. Benutze am besten überall `with`.

Wenn du eine Datei liest, musst du sie auch Locken. Dein Code hat Racing Conditions! Da `write_db` ja das Original übermangelt, kann es dir passieren, dass sich die Datei ändert, während `load_file` noch läuft. Das willst du nicht.

Da du zwei Dateien schreibst, du zueinander konsistent bleiben müssen, hast du auch das Problem, dass ein Absturz nach dem Schreiben der ersten Datei aber vor der zweiten Datei dir die Datenbasis zerschießt. Schreibe lieber nur eine Datei. Das ist sicherer.

Ich finde übrigens, dass die Funktionen für die Dateiverwaltung lieber Methoden eines File-Objekts sein sollten und nicht so einfach da im Modul stehen sollten. Andere mögen das anders sehen. Ein Indiz für Methoden ist jedoch, dass jede Funktion ein "filename"-Parameter hat. Gleiches gilt für die Funktionen, die alle einen "key_map"-Parameter haben. Mutwillig muss man nun Objekte auch nicht ignorieren.

Die ganzen Tests mit isinstance habe ich ehrlich gesagt nicht so ganz verstanden. Mir ist unklar, was das Ding eigentlich machen soll. Vielleicht ein paar Zeilen Dokumentation mit ein paar Beispielen und einer Motivation, warum es so ist, wie es ist?

Stefan
monkey
User
Beiträge: 7
Registriert: Montag 22. März 2010, 16:56

wirklich vielen dank erstmal.

ich werde mein Konzept überdenken und versuchen eure Vorschläge umzusetzen. Poste dann heute Abend oder morgen nochmal ein Update.
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Sag bloss du verwendest als IDE Geany?
monkey
User
Beiträge: 7
Registriert: Montag 22. März 2010, 16:56

Ok, da ich momentan noch im Umzugsstress stecke wird es wohl noch etwas dauern ich versuche erstmal zu definieren was ich will.

Also ich möchte für kleinere Projekte eine art Datenbankersatz haben der einfach zu integrieren ist. das ganze soll wie schon im ersten Ansatz dargestellt nach dem key/value Schema funktionieren. darüber hinaus sollen die Daten in einem lesbaren Format vorliegen. Gleichzeitiger Zugriff mehre Prozesse sollte möglich sein, zumindest lesend. Wie man so was auch für schreibende Zugriffe möglich machen könnte weiß ich nicht. Geht das überhaupt ohne eine art Server der das managt!? Neben dem Key der zwar eindeutig ist aber sich durchaus ändern kann, soll es für jeden Eintrag eine eindeutige ID geben damit man die Möglichkeit hat Daten zu verknüpfen. Vielleicht währe auch so was wie ein Typ oder Taging des Datensatzes interessant.

im Einzelnen,

* Key/Value Zugriff
* Conncurent Read, (Write)
* Eindeutige ID
* Tags, Typ mit Suche zb. alles Typ oder Tag "User"

Zur Speicherung der Daten.

Was sma geschrieben hat stimmt natürlich aber alles in einer Datei zu Speichern bedeutet auch das bei einem Insert oder Update eine "große" Datenmenge geschrieben werden muss. Oder doch besser eine Index Datei und eine für die Daten. Vielleicht auch nach folgendem Schema für jeden Datensatz eine Datei:

Code: Alles auswählen

/index.json
/store/
    /key_a.json
    /key_b.json
    /key_c.json

Zugriff auf die nach dem key benannte Datei sollte doch schneller sein als danach zu suchen und ich müsste die ganzen Daten außer dem Index nicht im RSpeicher vorhalten.

Die Benutzung könnte in etwa so aussehen

Code: Alles auswählen

users = JsonStore("users") 
users["key"] = "value" 
print users["key"]
"value"
wobei dabei die frage ist an welcher stelle ich die zusätzlichen Informationen unterbringe deswegen hatte ich es ursprünglich so angedacht

Code: Alles auswählen

print users["key"]
{ 'id' : 1, 'value' : 'value' 'etc.' : 'etc' }
was meint ihr, wie würdet ihr euch das handling wünschen...


@Dauerbaustelle nein ich verwende Vim/gedit wie kommst du darauf das ich Geany verwende?
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

monkey, weil dein Code diese Geany-typischen Einrückungen hat:

Code: Alles auswählen

foo = [
    a,
    b
    ]
Das ist eher untypisch, meist steht die schließende Klammer auf der selben Ebene wie die öffnende.
lunar

Ich müsse mich jetzt sehr irren, aber soweit ich weiß, rückt Emacs exakt genauso ein. Ich finde die Einrückung auch nicht ungewöhnlich, allenfalls die allein stehende schließende Klammer.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Wenn man bei Änderungen der Daten nicht jedes Mal alles speichern will, bietet sich ein "append only"-Verfahren an, bei dem nur das Neue zu einer immer größer wachsenden Datei hinzugefügt wird. Wenn man sich nicht darauf verlassen will/kann, dass das Dateisystem transaktional ist, muss man aber wieder einigen Aufwand treiben. Auch das Einlesen der Daten wird aufwendiger. Und man ist immer noch auf den eigenen Hauptspeicher begrenzt. Kann mal alles noch relativ einfach bauen - oder etwas wie Redis benutzen.

Will man mehr speichern können, als in den Hauptspeicher passt, könnte man Index und Daten getrennt verwalten und hoffen, dass zumindest der Index in den Hauptspeicher passt. Nun gilt es aber beide Dateien zu sychronisieren bzw. beides wieder in eine "append only"-Datei zu stecken und etwas zu entwickeln, dass andere schon längst erfunden haben.

Wartet man noch etwas, kann Redis wohl auch virtuellen Speicher (auf Platte) effizient verwalten und alles wird gut. Oder man nutzt berkeleyDB oder ein vergleichbares System. Was anderes liegt letztlich auch nicht unter etwas wie Tokyo Tyrant drunter. Übrigens, größeres Kaliber wäre eine Document-DB wie Couch oder Mongo.

Noch einfacher ist es aber wahrscheinlich, einfach SQLite zu benutzen. Man definere sich eine Tabelle mit zwei Spalten: Key und Value und speichere als Werte gepickelte Python-Objekte. Möglicherweise ist eine einfachere Kodierung noch schneller. Weiß man etwa, dass man nur int, bool, str, bytes, list und dict in nicht-rekursiven Datenstrukturen ablegen können will, gibt es effiziente binäre Formate.

Dinge wie Tags oder eine Suchfunktion würde ich da oben drauf setzen. Hält man alles im Hauptspeicher, sind diese Dinge natürlich wieder trivial.

Die Trennung von Key und ID verwirrt mich übrigens. IMHO wäre die Eigenschaft des Keys, ein eindeutiger Schlüssel zu sein und jetzt noch einen zweiten Primärschüssel haben zu wollen finde ich komisch. Eine Indirektion mehr löst das Problem doch auch.

Stefan
monkey
User
Beiträge: 7
Registriert: Montag 22. März 2010, 16:56

Die Trennung von Key und ID verwirrt mich übrigens. IMHO wäre die Eigenschaft des Keys, ein eindeutiger Schlüssel zu sein und jetzt noch einen zweiten Primärschüssel haben zu wollen finde ich komisch. Eine Indirektion mehr löst das Problem doch auch.
Stimmt schon. Mein Ansatz ist, das wenn ich zum Beispiel User habe:

Code: Alles auswählen

    id    key            value
    --------------------------------------------------------------------
    1     u1@bla.de      { 'password' : '12345', 'realname' : 'U1 Bla' }
    2     u2@bla.com     { 'password' : '54321', 'realname' : 'U2 Bla' }
dann kann ich über die Mailadresse einfach auf den User zugreifen z.B bei einem Login, der aus einer Mail/Passwort Kombination besteht und zwar ohne im Value suchen zu müssen. Möchte ich diesen Usern aber dann noch Nachrichten zuordnen, kann ich das über die ID machen und es besteht die Option, den KEY jederzeit umzubenennen, ohne die Verbindung zu verlieren. Darüber hinaus kann ich die Daten über die ID sortieren was sonst nicht möglich wäre.

Ohne KEY müsste ich ja mindestens 2 Datensätze haben.

Code: Alles auswählen

    key            value
    --------------------------------------------------------------
    unic_key_1     { 'password' : '12345', 'realname' : 'U1 Bla' }
    unic_key_2     { 'password' : '54321', 'realname' : 'U2 Bla' }

    key            value
    ---------------------------
    u1@bla.de      'unic_key_1' 
    u2@bla.com     'unic_key_2'
Eines der Hauptprobleme meines Skripts ist ja Persistenz bzw. das Sperren einer Datei für weitere Zugriffe. Da das ganze nur unter Unix/Linux laufen soll, dachte ich, ich könnte das über Bordmittel regeln. Leider war das Ergebnis ziemlich ernüchternd. Ich habe sowohl "fcntl.lockf, fcntl.flock" als auch os.open mit O_EXLOCK/O_SHLOCK ausprobiert. Keine der beiden funktionierte auf meinem Ubuntu System. Dies liegt daran, dass Mandatory Locking im Standart Fall nicht aktiv ist. Dazu benötigt es einen Eintrag "mand" in der "/etc/fstab". Die Methode ist daher unbefriedigend weil das Skript ja auch mal auf einem Rechner laufen könnte, bei dem ich keinen Einfluss auf die Mount Optionen habe.

Das Konzept das Locking über Lockdateien zu realisieren erscheint mir daher gar nicht so abwegig, zumal ja auch andere z.B. Firefox einen solchen Mechanismus nutzen. Damit das Ganze auch dann noch funktioniert, wenn ein Prozess abstürzt, müsste es einen Timeout geben nach dem die Lockdatei auch von anderen Prozessen entfernt werden darf.

Da ja auch Lesezugriffe die Datei gegen Überschreibung sichern müssen, wären zwei verschiedene Lockarten sinnvoll. SharedLock und ExclusivLock.

Realisieren könnte man den SharedLock durch ein Verzeichnis in dem für jede PID eine Datei erstellt wird, die einen Timestamp und den Timeout enthält.

datei.txt
datei.txt.lock/
42342
98797
24377

Nachdem der Lock freigegeben wird, wird die PID-Datei entfernt. Sollte ein Prozess abstürzen, kann die Datei nach dem Timeout gelöscht werden.

Der ExclusivLock muss in dem Fall warten, bis alle Lesezugriffe beendet sind (hier sehe ich noch Schwierigkeiten aber dazu später mehr). Dieser erstellt nur eine einzige Datei, ebenfalls mit Timestamp und Timeout.

datei.txt
datei.txt.lock

Das Problem ist, dass der Schreibzugriff warten muss, bis alle SharedLocks beendet oder aufgeräumt wurden. In einem Szenario, wo es andauernde Lesezugriffe gibt, kann das natürlich ewig dauern. Besser währe daher eine Art Queue doch da gibt es dann das "Henne Ei" Problem da diese natürlich ebenfalls in irgendeiner Art gesperrt werden müsste. Andererseits wie realistisch ist es, das es so großen permanenten Zugriff gibt, dass die Datei ständig gelockt ist?

mokey
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

...oder du nimmst einfach Sqlite (siehe z.B. http://www.python-forum.de/post-165450.html#165450), denn dort hat jemand schon die ganze Locking-Problematik gelöst. Natürlich ginge auch ein externer KV-Store, doch Sqlite hat den Vorteil, eingebaut zu sein.

Stefan
Antworten