python -m pip install wsqlite3 --upgrade
WSQLite3
(v0.7 under development)
versions <0.5 are dysfunctional
versions <0.7 contains unclean thread management, which can cause zombie processes
WSQLite3 (WebSocket SQLite3) is a simple and lightweight solution to manage access from multiple processes (or threads) to SQLite3 databases.
What it is and what it is not:The focus of WSQLite3 is on simple local session management of multiple instances of a program or compatibilization of different programs. WSQLite3 is not intended as a database in a wide-ranging system for multiple end users. For this, other advanced database systems with client servers should be used.
WSQLite3 in the basic version is designed for friendly connections, an SSL is NOT implemented and there is NO permission handling (Caution: WSQLite3 also allows remote code execution).
WSQLite3 manages connections to SQLite3 databases for multiple clients per server. The communication via the WebSocket protocol with JSON data provides simple and high compatibility between different programs and programming languages. The focus of the project is on the WSQLite3 service, which is intended as an independent process. For complex projects, a separate implementation of the client side should be embedded.
install:
The service can be started and managed via the command line. For more information, run:
wsqlite3 help
since 0.5, extensive upgrades since version 0.7
Project Wordings
Connection | refers to the |
Cursor | refers to the |
side/main (with direct reference to Cursor) | the main cursor refers to the cursor managed by the |
Order | refers to incoming order JSONs that are processed on the |
Identifications
wsqlite3uid | for the identification of Connection's and database operations.
Follows the scheme:
<hex(seconds since the Epoch)>-<hex(pid|tid)>#<hex(instance counter)> tid is used in almost all systems for the middle segment)
|
wsqlite3threadName | for the identification of ConnectionsThread's.
Follows the scheme:
t<n> |
The service module
The objects in the service module of WSQLite3 offer several interfaces and preliminary work for inheritance in order to add application-related functionalities.
The main object is the
server = wsqlite3.Server(("localhost", 9998))
server.add_database(None, ":memory:")
and started via
server.run()
or as a daemon thread via
server.daemon = True
server.start()
By default, the
Orders
Orders to the [<order> , ...]
.
The sequence in which the order (sub)sections and their instructions are listed here corresponds to the sequence of
Order (sub)sections are basically optional. Depending on the section, a specific instruction pattern may be expected. As a general rule, an instruction that is not set but is expected leads to a KeyError; if a more specific instruction pattern is expected but not fulfilled, this leads to an OrderError (see also Error Handling).
In the basic version, the entire order JSON is otherwise NOT checked for plausibility, unknown (or incorrectly written or set in the wrong place) sections or instructions are simply ignored (however, values in the methods are obtained via dict.pop).
The return value is usually:
{
"orders": [
{
<Orders>...,
"errors": (0) |(code) ,
"error": null | <last occurred Exception Message>,
"flag": null | <"flag">
},
...
],
"errors": (0) |(code) ,
"error": null | <last occurred Exception Message>,
"flags": null | <"flag">, ...
}
(see also Error Handling)
The outgoing order is structurally the same as the incoming order and is made up of valid sections that were in the order and the return fields according to the instructions.
The value under "errors" indicates whether errors have occurred in one of the sections (see also Error Handling).
How to deal with binary data from/to the database
As a pure text-based data format, bytes cannot be transferred directly via JSON. Therefore, binary data is always transmitted in base64 format. If a binary value is retrieved from the database on the server side, it is transmitted to the client as a base64 string. The client must therefore be able to determine when and where base64 strings appear that need to be decoded, as the coding cannot be distinguished.
On the other hand, in order to be able to transmit binary data to the server, the
"tb:params" statement (which replaces the
"params" statement) is available for "sql" operations.
"t:" | following characters are plain text and are not decoded on the server side |
"b:" | following characters are a base64 string that is decoded into bytes on the server side |
server << {
"sql": {
"exec": "INSERT INTO main VALUES (?, ?)",
"tb:params": [ "t:Hello", "b:V29ybGQ=" ]
}
}
server << {
"sql": {
"exec": "SELECT * FROM main",
"fetchone": true
}
}
server >> {
"sql": {
"fetch": [ "Hello", "V29ybGQ=" ]
}
}["sql"]["fetch"][1].b64decode().decode("utf8") == "World"
Structure
"code": (pythoncode) –> "result": ,
"error", "error"
"code":
"result" is obtained by
globals().get("result") or locals().get("result")
after exec("code", globals(), locals())
"of": ("connection"|"server"|"thread")
"path": (attribute path)
[ "call": args , kwargs ]
–> "result": ,
"error", "error"
"of":
"of" describes the object from which the attribute is to be obtained.
"path" represents a direct attribute name or a python attribute path ("." separated).
If "call" is defined, the obtained object is called with
(*args, **kwargs)
.
"result" is the attribute respectively the result from the call.
"get": (wsqlite3-uid) | description record, ...
[ "set": ("session"|"response") | ],
"id": –> "id": (wsqlite3uid) ,
"send": ,
"description": –> "description": description record, ... ,
"description.pop": (key) –> "description.pop": (value) ,
"description.set": description record, ... ,
"description.update": description record, ... ,
"properties": –> "properties": properties ,
"destroy": ,
"error", "error"
"get":
Via "get" a foreign connection object can be obtained.
If
also references the connection to all subsequent sections of the order | |
references the connection to the entire session | |
references the connection as the connection to be sent back to |
Query the id of the current connection.
"send":Send to the client of the current connection.
"description":Query the description or edit it. description is a dict of the
Query the properties of the current connection:
{
"id": <wsqlite3uid>,
"description": <description>,
"timestamp": <creation time>,
"address": <client socket (host, port)>,
"thread": <thread.properties>,
}
Close the current connection.
"connections": –> "connections": connection.id: connection.properties , ... ,
"threads": –> "threads": thread.id: thread.properties , ... ,
"shutdown": | ("force") | ("commit") | ("force,commit") ,
"error", "error"
"connections": Query a
{
<connection.id>: {<connection.properties>},
...
}
Query a thread register:
{
<thread.id>: {<thread.properties>},
...
}
Shutdown the
"get": (wsqlite3threadName) ,
"id": –> "id": (wsqlite3threadName) ,
"broadcast": "broadcast.self": ,
"connections": –> "connections": connection.id: connection.properties , ... ,
"properties": –> "properties": properties ,
"error", "error"
"get":
Via "get" a specific
Query the id of the current
Send the
{
"broadcast": < >,
"from": <wsqlite3threadName>
}
Query a
{
<connection.id>: {<connection.properties>},
...
}
Query the properties of the current
{
"id": <wsqlite3threadName>,
"timestamp": <start time>,
"thread.ident": <thread.ident>,
"TID": <thread.native_id>,
}
"keys": (key) | , ... ,
"open": sqlite3-args , sqlite3-kwargs [ "get": (key) | ] –> "open": (key) | ,
"get": (key) ,
*"side":
*"force":
*"sudo": ;
"lock": –> "lock": (wsqlite3uid) | ,
"arraysize": (True) | (size) –> "arraysize": (size) ,
"description": –> "description": (column name) , , , , , , , ... | ,
"lastrowid": –> "lastrowid": | ,
"rowcount": –> "rowcount": ,
( "script": (SQL) )
| ( "exec": (SQL) [ "params"|"tb:params": placeholder bindings | placeholder bindings ] )
| ( "many": (SQL) "params"|"tb:params": placeholder bindings | placeholder bindings ),
(
("fetchone"|"fetchall": )
| ("fetchmany": (True) | (size) )
)
–> "fetch": database record , ... | ,
"release": | ("finally") –> "release": ,
"rollback": ,
"commit": ,
"close": –> "close": ,
"error", "error"
"keys": Query the database key list.
"open":
Open and register a database.
Define a previously registered database to which the instructions refer. If the field is not set, an attempt is made to obtain the default database. Raises a ConfigurationError if the database is not registered.
*"side":
Parameterizes which/how a False
.
If a
Evaluation of parameters for execution instructions ("exec" "scrip" "many") |
|
||||||
Evaluation of parameters for fetch instructions ("fetchone" "fetchmany" "fetchall") |
|
||||||
Evaluation of parameters for the "release" instruction |
|
||||||
Evaluation of parameters for the "close" instruction |
|
Query the id of the connection that locks the cursor.
"arraysize":
Query or set the attribute arraysize of
Query the attribute description of
Query the attribute lastrowid of
Query the attribute rowcount of
Execute
"script": | sqlite3.Cursor.executescript |
"exec": | sqlite3.Cursor.execute |
"many": | sqlite3.Cursor.executemany |
Execute the corresponding fetch method. Only one instruction is allowed within the section, otherwise an OrderError is raised. If no cursor is locked or cannot be obtained successfully, CursorNotLockedError respectively CursorLockedError is raised.
"fetchone" | sqlite3.Cursor.fetchone |
"fetchall" | sqlite3.Cursor.fetchall |
"fetchmany" | sqlite3.Cursor.fetchmany |
Release the lock on the cursor.
If
In combination with "side", only the side cursor is closed, otherwise the database is closed and removed from the register. Returns whether the closing was performed.
"message": ,
"self": ,
"error", "error"
"message":
Send the
{
"broadcast": < >,
"from": <wsqlite3uid>
}
"cancel": (true|explicit false) | (2) | ,
"value": ("block") | ("request") | (0) | ("skip!") ,
"config": –> "config": autoclose config ,
"config.block": ,
"config.request": ,
"config.wait_response": (seconds) ,
"config.wait_close": (seconds) ,
"config.force_shutdown": ,
"config.sql_commit": ,
"trigger": ,
"error", "error"
since 0.5, fixes with 0.6
Response to a request (see autoclose).
cancel the automatic closing | |
cancel the automatic closing even if there is no connection after the timeout | |
execute the automatic closing explicitly |
Set the value of the current connection (see autoclose).
the connection blocks the automatic closing (default) | |
if no connection is blocking, this connection triggers the request whether to close | |
neutral | |
special value: skips the complete feature if the triggering connection has the value |
Query or edit the autoclose configuration (see autoclose).
"config": | Query the current configuration |
"config.block": | Block the automatic closing if a connection contains the value |
"config.request": | Request from all clients (except the causing client) whether the automatic closing should be executed if no connection blocks and if a connection contains the value |
"config.wait_response": | Wait for the reactions of the requests. The server will remain open for this time in any case. The previous conditions are then checked again; if the request condition is met again, the execution of the automatic closing depends on the reactions of the requested connections ("cancel"). Default is |
"config.wait_close": | Wait after neither the condition for prevention nor request has occurred. Then check the conditions again. The server remains open for this time in any case. (Subsequently, the feature of requests could be triggered ("config.request_at")). Default is |
"config.force_shutdown": | Is passed to "server": "shutdown" during the automatic closing. Default is |
"config.sql_commit": | Is passed to "server": "shutdown" during the automatic closing. Default is |
Trigger an autoclose check (see autoclose).
| Exception Message
This field is set to the last Exception Message that occurred within this order.
autoclose
since 0.5, fixes with 0.6
The service closes automatically by default.
The conditions for this are checked each time a client disconnects.
As long as a connection exists and has the
"autoclose" "value"
{
"autoclose": {
"trigger": <connection.id>,
"reason":
("handshake timeout") |
("loop canceled") |
("erract") |
("fatal error") |
("trigger ordered") |
("destroy ordered") ,
"connection_total": <current connection total>
}
}
Error Handling
Own Exception Types
- CursorLockedError
- CursorNotLockedError
- OrderError
- ConfigurationError
- IdError
Exception catching
All exceptions that are directly related to the processing of an order are caught and handled according to the next subchapter.
The handling of exceptions that are raised outside of this is described in the subsection after next.
Error handling when processing orders
The "error" instruction can be assigned to each section of the incoming order that contains instructions. Possible parameters:
cancels the entire order | |
cancels the entire session | |
cancels the entire session and destroys the current connection | |
cancels the entire session and shuts down the server | |
cancels the entire session and shuts down the server forcefully | |
continues to execute the following sections of the order |
In addition, the "sql" section evaluates whether the parameter
If the instruction is not set or is set differently, the entire session is canceled.
Regardless of the handling, the current section is canceled and the "error" field in the section of the outgoing order is set according to the error formatting. In the basic version this is:
{
"type": <exception class name>,
"args": <exception args>,
"repr": <representation of the exception>,
"message": <string of the exception>,
"pypickle": <the representative string of bytes of the picked object representation>,
"params": null | <the params attribute of the exception ( set in some Own Exception Types )>
}
"pypickle": Only useful if the client is in the python system.
Generated by
repr(pickle.dumps(exc))
,
which is faster in encoding but slower with decoding (
raise pickle.loads(eval(error["pypickle"]))
) than base64 encoding.
If an error occurs at any time, the "errors" field of the return order is set to the highest error code that has occurred:
{
"orders": [
{
<Orders>...,
"errors": (0) |(code) ,
"error": null | <last occurred Exception Message>,
"flag": null | <"flag">
},
...
],
"errors": (0) |(code) ,
"error": null | <last occurred Exception Message>,
"flags": null | <"flag">, ...
}
no errors occurred | |
errors occurred but no action performed | |
errors occurred and order canceled | |
errors occurred and session canceled |
Error handling after fatal errors during order processing
For extreme exceptional cases, e.g. errors are not intercepted in a modified basic version or if a connection is interrupted exactly during the processing of an order, there is a graduated handling:
First an attempt is made to send the JSON
{"errors": -1, "error": Exception Message}
if this fails, an attempt is made to send a null byte, if this leads to an error, an attempt is made to terminate and remove the connection properly, if this also leads to an error,
is executed finally.