Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for create_async_engine #7

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 8 additions & 146 deletions sqlalchemy_libsql/__init__.py
Original file line number Diff line number Diff line change
@@ -1,155 +1,17 @@
r"""
.. dialect:: sqlite+libsql
:name: libsql
:dbapi: libsql_client.dbapi2
:connectstring: sqlite+libsql://your-db.your-server.com?authToken=JWT_HERE&secure=true
:url: https://github.com/libsql/libsql-client-py/

Note that this driver is based on the standard SQLAlchemy ``pysqlite``
dialect, the only change is how to connect, accepting remote URL in
addition to the file dialects

Driver
------

The ``libsql_client.dbapi2`` offers compatibility with standard library's
``sqlite3.dbapi2``. For local files or ``:memory:``, the standard library
connection is used. Whenever a host is provided, then the connection
will use LibSQL network protocol via ``ws`` (WebSocket) or ``wss``
(secure WebSocket), the decision depends on the presence of ``secure=true``
query parameter.

Connect Strings
---------------

In addition to `Pysqlite
<https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#connect-strings>`_,
this driver accepts URL with user, password, hostname and port.

These will use the LibSQL network protocol on top of WebSockets. The selection
between ``ws://`` and ``wss://` (secure) is defined by the query/search
parameter ``secure=true``. It defaults to ``secure=false``.

If the given URL provides a hostname, then it will default to ``uri=true``.

""" # noqa: E501

import os
import urllib.parse

from sqlalchemy import util
from sqlalchemy.dialects import registry as _registry
from sqlalchemy.dialects.sqlite.pysqlite import SQLiteDialect_pysqlite

from sqlalchemy_libsql.libsql import dialect
from sqlalchemy_libsql.libsql import SQLiteDialect_libsql
from sqlalchemy_libsql.libsql_async import SQLiteDialect_libsql_async

__version__ = "0.1.0-pre"

_registry.register(
"sqlite.libsql", "sqlalchemy_libsql", "SQLiteDialect_libsql"
)

_registry.register(
"sqlite.libsql_async", "sqlalchemy_libsql", "SQLiteDialect_libsql_async"
)

def _build_connection_url(url, query, secure):
# sorting of keys is for unit test support
query_str = urllib.parse.urlencode(sorted(query.items()))

if not url.host:
if query_str:
return f"{url.database}?{query_str}"
return url.database
elif secure: # yes, pop to remove
scheme = "wss"
else:
scheme = "ws"

if url.username and url.password:
netloc = f"{url.username}:{url.password}@{url.host}"
elif url.username:
netloc = f"{url.username}@{url.host}"
else:
netloc = url.host

if url.port:
netloc += f":{url.port}"

return urllib.parse.urlunsplit(
(
scheme,
netloc,
url.database or "",
query_str,
"", # fragment
)
)


class SQLiteDialect_libsql(SQLiteDialect_pysqlite):
driver = "libsql"
# need to be set explicitly
supports_statement_cache = SQLiteDialect_pysqlite.supports_statement_cache

@classmethod
def import_dbapi(cls):
from libsql_client import dbapi2 as libsql_client

return libsql_client

def on_connect(self):
from libsql_client.dbapi2 import Connection

sqlite3_connect = super().on_connect()

def connect(conn):
# LibSQL: there is no support for create_function()
if isinstance(conn, Connection):
return
return sqlite3_connect(conn)

return connect

def create_connect_args(self, url):
pysqlite_args = (
("uri", bool),
("timeout", float),
("isolation_level", str),
("detect_types", int),
("check_same_thread", bool),
("cached_statements", int),
("secure", bool), # LibSQL extra, selects between ws and wss
)
opts = url.query
libsql_opts = {}
for key, type_ in pysqlite_args:
util.coerce_kw_type(opts, key, type_, dest=libsql_opts)

if url.host:
libsql_opts["uri"] = True

if libsql_opts.get("uri", False):
uri_opts = dict(opts)
# here, we are actually separating the parameters that go to
# sqlite3/pysqlite vs. those that go the SQLite URI. What if
# two names conflict? again, this seems to be not the case right
# now, and in the case that new names are added to
# either side which overlap, again the sqlite3/pysqlite parameters
# can be passed through connect_args instead of in the URL.
# If SQLite native URIs add a parameter like "timeout" that
# we already have listed here for the python driver, then we need
# to adjust for that here.
for key, type_ in pysqlite_args:
uri_opts.pop(key, None)

secure = libsql_opts.pop("secure", False)
connect_url = _build_connection_url(url, uri_opts, secure)
else:
connect_url = url.database or ":memory:"
if connect_url != ":memory:":
connect_url = os.path.abspath(connect_url)

libsql_opts.setdefault(
"check_same_thread", not self._is_url_file_db(url)
)

return ([connect_url], libsql_opts)


dialect = SQLiteDialect_libsql
__all__ = ("SQLiteDialect_libsql", "SQLiteDialect_libsql_async", "dialect")
148 changes: 148 additions & 0 deletions sqlalchemy_libsql/libsql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
r"""
.. dialect:: sqlite+libsql
:name: libsql
:dbapi: libsql_client.dbapi2
:connectstring: sqlite+libsql://your-db.your-server.com?authToken=JWT_HERE&secure=true
:url: https://github.com/libsql/libsql-client-py/

Note that this driver is based on the standard SQLAlchemy ``pysqlite``
dialect, the only change is how to connect, accepting remote URL in
addition to the file dialects

Driver
------

The ``libsql_client.dbapi2`` offers compatibility with standard library's
``sqlite3.dbapi2``. For local files or ``:memory:``, the standard library
connection is used. Whenever a host is provided, then the connection
will use LibSQL network protocol via ``ws`` (WebSocket) or ``wss``
(secure WebSocket), the decision depends on the presence of ``secure=true``
query parameter.

Connect Strings
---------------

In addition to `Pysqlite
<https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#connect-strings>`_,
this driver accepts URL with user, password, hostname and port.

These will use the LibSQL network protocol on top of WebSockets. The selection
between ``ws://`` and ``wss://` (secure) is defined by the query/search
parameter ``secure=true``. It defaults to ``secure=false``.

If the given URL provides a hostname, then it will default to ``uri=true``.

""" # noqa: E501

import os
import urllib.parse

from sqlalchemy import util
from sqlalchemy.dialects.sqlite.pysqlite import SQLiteDialect_pysqlite


def _build_connection_url(url, query, secure):
# sorting of keys is for unit test support
query_str = urllib.parse.urlencode(sorted(query.items()))

if not url.host:
if query_str:
return f"{url.database}?{query_str}"
return url.database
elif secure: # yes, pop to remove
scheme = "wss"
else:
scheme = "ws"

if url.username and url.password:
netloc = f"{url.username}:{url.password}@{url.host}"
elif url.username:
netloc = f"{url.username}@{url.host}"
else:
netloc = url.host

if url.port:
netloc += f":{url.port}"

return urllib.parse.urlunsplit(
(
scheme,
netloc,
url.database or "",
query_str,
"", # fragment
)
)


class SQLiteDialect_libsql(SQLiteDialect_pysqlite):
driver = "libsql"
# need to be set explicitly
supports_statement_cache = SQLiteDialect_pysqlite.supports_statement_cache

@classmethod
def import_dbapi(cls):
from libsql_client import dbapi2 as libsql_client

return libsql_client

def on_connect(self):
from libsql_client.dbapi2 import Connection

sqlite3_connect = super().on_connect()

def connect(conn):
# LibSQL: there is no support for create_function()
if isinstance(conn, Connection):
return
return sqlite3_connect(conn)

return connect

def create_connect_args(self, url):
pysqlite_args = (
("uri", bool),
("timeout", float),
("isolation_level", str),
("detect_types", int),
("check_same_thread", bool),
("cached_statements", int),
("secure", bool), # LibSQL extra, selects between ws and wss
)
opts = url.query
libsql_opts = {}
for key, type_ in pysqlite_args:
util.coerce_kw_type(opts, key, type_, dest=libsql_opts)

if url.host:
libsql_opts["uri"] = True

if libsql_opts.get("uri", False):
uri_opts = dict(opts)
# here, we are actually separating the parameters that go to
# sqlite3/pysqlite vs. those that go the SQLite URI. What if
# two names conflict? again, this seems to be not the case right
# now, and in the case that new names are added to
# either side which overlap, again the sqlite3/pysqlite parameters
# can be passed through connect_args instead of in the URL.
# If SQLite native URIs add a parameter like "timeout" that
# we already have listed here for the python driver, then we need
# to adjust for that here.
for key, type_ in pysqlite_args:
uri_opts.pop(key, None)

secure = libsql_opts.pop("secure", False)
connect_url = _build_connection_url(url, uri_opts, secure)
else:
connect_url = url.database or ":memory:"
if connect_url != ":memory:":
connect_url = os.path.abspath(connect_url)

libsql_opts.setdefault(
"check_same_thread", not self._is_url_file_db(url)
)

return ([connect_url], libsql_opts)


dialect = SQLiteDialect_libsql
50 changes: 50 additions & 0 deletions sqlalchemy_libsql/libsql_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
r"""
.. dialect:: sqlite+libsql_async
:name: libsql_async
:dbapi: libsql_client.dbapi2
:connectstring: sqlite+libsql_async://your-db.your-server.com?authToken=JWT_HERE&secure=true
:url: https://github.com/libsql/libsql-client-py/
Note that this driver is based on the standard SQLAlchemy ``pysqlite``
dialect, the only change is how to connect, accepting remote URL in
addition to the file dialects
Disclaimer: While this dialect allows for async_engine compatibility with
libsql, the dbapi remains synchronous
Driver
------
The ``libsql_client.dbapi2`` offers compatibility with standard library's
``sqlite3.dbapi2``. For local files or ``:memory:``, the standard library
connection is used. Whenever a host is provided, then the connection
will use LibSQL network protocol via ``ws`` (WebSocket) or ``wss``
(secure WebSocket), the decision depends on the presence of ``secure=true``
query parameter.
Connect Strings
---------------
In addition to `Pysqlite
<https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#connect-strings>`_,
this driver accepts URL with user, password, hostname and port.
These will use the LibSQL network protocol on top of WebSockets. The selection
between ``ws://`` and ``wss://` (secure) is defined by the query/search
parameter ``secure=true``. It defaults to ``secure=false``.
If the given URL provides a hostname, then it will default to ``uri=true``.
""" # noqa: E501

from sqlalchemy_libsql.libsql import SQLiteDialect_libsql


class SQLiteDialect_libsql_async(SQLiteDialect_libsql):
driver = "libsql"
# need to be set explicitly
supports_statement_cache = SQLiteDialect_libsql.supports_statement_cache
is_async = True


dialect = SQLiteDialect_libsql_async