Skip to content

Commit 8e09861

Browse files
committed
feat(sqlalchemy): initial version of sqlalchemy dialect
1 parent 7b1b435 commit 8e09861

File tree

17 files changed

+379
-11
lines changed

17 files changed

+379
-11
lines changed

.devcontainer/py3.6-dev/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ ADD https://dl.yarnpkg.com/debian/pubkey.gpg /etc/apt/trusted.gpg.d/yarn.asc
44

55
RUN chmod +r /etc/apt/trusted.gpg.d/*.asc && \
66
echo "deb http://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
7+
8+
COPY install-legacy-extensions.sh /usr/local/bin/install-legacy-extensions.sh
9+
RUN chmod +x /usr/local/bin/install-legacy-extensions.sh

.devcontainer/py3.6-dev/devcontainer.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,24 @@
1717
// "forwardPorts": [],
1818

1919
// Use 'postCreateCommand' to run commands after the container is created.
20-
// "postCreateCommand": "pip3 install --user -r requirements.txt",
20+
"postCreateCommand": "install-legacy-extensions.sh",
2121

2222
// Configure tool-specific properties.
2323
// Py3.6 support (switch extensions to `pre-release` and `install another version`):
2424
// Pylance v2022.6.30
2525
// Python v2022.8.1
26-
// Python Debugger v2023.1.XXX (pre-release version | debugpy v1.5.1)
26+
// Python Debugger v2023.1.12492010 (pre-release version | debugpy v1.5.1)
2727
// Black Formatter v2022.2.0
2828
// Isort v2022.1.11601002 (pre-release)
2929
"customizations": {
3030
"vscode": {
3131
"extensions": [
3232
"littlefoxteam.vscode-python-test-adapter",
3333
"jkillian.custom-local-formatters",
34-
"ms-python.vscode-pylance",
35-
"ms-python.python",
36-
"ms-python.debugpy",
37-
"ms-python.black-formatter",
38-
"ms-python.isort",
3934
"ms-toolsai.jupyter"
4035
]
4136
}
42-
}
37+
},
4338

4439
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
4540
// "remoteUser": "root"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
3+
code --install-extension "ms-python.python@2022.8.1"
4+
code --install-extension "ms-python.vscode-pylance@2022.6.30"
5+
code --install-extension "ms-python.debugpy@2023.1.12492010"
6+
code --install-extension "ms-python.black-formatter@2022.2.0"
7+
code --install-extension "ms-python.isort@2022.1.11601002"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<img src="https://sqlitecloud.io/social/logo.png" height="300" alt="SQLite Cloud logo">
55
</p>
66

7-
![Build Status](https://github.com/sqlitecloud/sqlitecloud-py/actions/workflows/deploy.yaml/badge.svg "Build Status")
7+
![Build Status](https://github.com/sqlitecloud/sqlitecloud-py/actions/workflows/test.yaml/badge.svg "Build Status")
88
[![codecov](https://codecov.io/github/sqlitecloud/python/graph/badge.svg?token=38G6FGOWKP)](https://codecov.io/github/sqlitecloud/python)
99
![PyPI - Version](https://img.shields.io/pypi/v/sqlitecloud?link=https%3A%2F%2Fpypi.org%2Fproject%2FSqliteCloud%2F)
1010
![PyPI - Downloads](https://img.shields.io/pypi/dm/sqlitecloud?link=https%3A%2F%2Fpypi.org%2Fproject%2FSqliteCloud%2F)

sqlalchemy-sqlitecloud/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# SQLite Cloud dialect for SQLAlchermy (beta)
2+
3+
This package makes SQLAlchemy to work with SQLite Cloud.
4+
5+
The dialect is based on the `sqlite` dialect included in SQLAlchemy.
6+
7+
## Beta version
8+
9+
This dialect is an early version and it works on Python >= 3.6.
10+
11+
**Tested only on `Python 3.6` and `SQLAlchemy 1.4`.**
12+
13+
The dialect as been tested against the SQLAlchemy `test_suite` as reported in the [documentation](https://github.com/sqlalchemy/sqlalchemy/blob/rel_1_4_53/README.dialects.rst).
14+
15+
You can follow the status of the tests that are still failing [in the dedicated issue](https://github.com/sqlitecloud/sqlitecloud-py/issues/21#issuecomment-2305162632).
16+
17+
References:
18+
- [Official SQLAlchemy documentation](https://docs.sqlalchemy.org/en/14/index.html)
19+
- [https://www.sqlalchemy.org/](https://www.sqlalchemy.org/)
20+
21+
22+
# Install and Usage
23+
24+
```bash
25+
$ pip install sqlalchemy-sqlitecloud
26+
```
27+
28+
> Get your SQLite Cloud connection string from the SQLite Cloud dashboard or register on [sqlitecloud.io](https://sqlitecloud.io) to get one.
29+
30+
Create the SQLAlchemy engine by setting the SQLite Cloud connection string. Add the prefix `sqlite+` like:
31+
32+
```python
33+
from sqlalchemy import create_engine
34+
35+
engine = create_engine('sqlite+sqlitecloud://mynode.sqlite.io?apikey=key1234')
36+
```
37+
38+
39+
# Run the Test Suite
40+
41+
Install the `sqlitecloud` package with:
42+
43+
```bash
44+
$ pip install sqlitecloud # last version
45+
```
46+
or install the reference to the local version:
47+
48+
49+
```bash
50+
$ cd ../src # sqlitecloud src directory
51+
$ pip install -e .
52+
```
53+
54+
Run the test suite with:
55+
56+
```bash
57+
$ cd sqlalchemy-sqlitecloud
58+
$ pytest
59+
```
60+
61+
> Note: VSCode Test Explorer and VSCode GUI debugger doesn't work because the actual implementation
62+
is not in the `test/test_suite.py` file. It cannot find the tests source code in the third-parties directory.
63+
64+
Use `pytest --pdb` with `pdb.set_trace()` to debug with command line:
65+
66+
- `s` step into
67+
- `n` next line
68+
- `r` jump to the end of the function
69+
- `c` continue
70+
- `a` print all variables
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Configuration file to store build system requirements for Python projects
2+
# https://www.python.org/dev/peps/pep-0518/
3+
4+
[tool.pytest.ini_options]
5+
addopts = "--tb native -v -r fxX --maxfail=25 -p no:warnings --requirements sqlalchemy_sqlitecloud.requirements:Requirements"

sqlalchemy-sqlitecloud/setup.cfg

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[tool:pytest]
2+
addopts= --tb native -v -r fxX --maxfail=25 -p no:warnings --requirements sqlalchemy_sqlitecloud.requirements:Requirements
3+
python_files=test/*test_*.py
4+
5+
[sqla_testing]
6+
requirement_cls=sqlalchemy_sqlitecloud.requirements:Requirements
7+
profile_file=test/profiles.txt
8+
9+
[db]
10+
# SQLite Cloud connection string is set in conftest.py with testing apikey
11+
#default=sqlite+sqlitecloud://...

sqlalchemy-sqlitecloud/setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
entry_points = {
2+
"sqlalchemy.dialects": [
3+
"sqlite.cloud = sqlalchemy_sqlitecloud.base:SQLiteCloudDialect",
4+
]
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from sqlalchemy.dialects import registry
2+
3+
registry.register(
4+
"sqlite.sqlitecloud", "sqlalchemy_sqlitecloud.base", "SQLiteCloudDialect"
5+
)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import sqlalchemy.types as sqltypes
2+
from sqlalchemy import util
3+
from sqlalchemy.dialects.sqlite.base import DATE, DATETIME, SQLiteDialect
4+
from sqlalchemy.engine.url import URL
5+
from sqlalchemy.exc import ArgumentError
6+
from sqlalchemy.pool import NullPool
7+
8+
9+
class _SQLite_pysqliteTimeStamp(DATETIME):
10+
def bind_processor(self, dialect):
11+
if dialect.native_datetime:
12+
return None
13+
else:
14+
return DATETIME.bind_processor(self, dialect)
15+
16+
def result_processor(self, dialect, coltype):
17+
if dialect.native_datetime:
18+
return None
19+
else:
20+
return DATETIME.result_processor(self, dialect, coltype)
21+
22+
23+
class _SQLite_pysqliteDate(DATE):
24+
def bind_processor(self, dialect):
25+
if dialect.native_datetime:
26+
return None
27+
else:
28+
return DATE.bind_processor(self, dialect)
29+
30+
def result_processor(self, dialect, coltype):
31+
if dialect.native_datetime:
32+
return None
33+
else:
34+
return DATE.result_processor(self, dialect, coltype)
35+
36+
37+
class SQLiteCloudDialect(SQLiteDialect):
38+
default_paramstyle = "qmark"
39+
supports_statement_cache = False
40+
41+
colspecs = util.update_copy(
42+
SQLiteDialect.colspecs,
43+
{
44+
sqltypes.Date: _SQLite_pysqliteDate,
45+
sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp,
46+
},
47+
)
48+
49+
driver = "sqlitecloud"
50+
51+
@classmethod
52+
def dbapi(cls):
53+
from sqlitecloud import dbapi2
54+
55+
return dbapi2
56+
57+
@classmethod
58+
def get_pool_class(cls, url):
59+
return NullPool
60+
61+
def _get_server_version_info(self, connection):
62+
return self.dbapi.sqlite_version_info
63+
64+
def set_isolation_level(self, connection, level):
65+
if level != "AUTOCOMMIT":
66+
raise ArgumentError(
67+
"SQLite Cloud supports only AUTOCOMMIT isolation level."
68+
)
69+
70+
if hasattr(connection, "dbapi_connection"):
71+
dbapi_connection = connection.dbapi_connection
72+
else:
73+
dbapi_connection = connection
74+
75+
if level == "AUTOCOMMIT":
76+
dbapi_connection.isolation_level = None
77+
else:
78+
dbapi_connection.isolation_level = ""
79+
return super(SQLiteCloudDialect, self).set_isolation_level(
80+
connection, level
81+
)
82+
83+
def on_connect(self):
84+
connect = super(SQLiteCloudDialect, self).on_connect()
85+
86+
fns = []
87+
88+
if self.isolation_level is not None:
89+
90+
def iso_level(conn):
91+
self.set_isolation_level(conn, self.isolation_level)
92+
93+
fns.append(iso_level)
94+
95+
def connect(conn): # noqa: F811
96+
for fn in fns:
97+
fn(conn)
98+
99+
return connect
100+
101+
def create_connect_args(self, url: URL):
102+
if not url.host:
103+
raise ArgumentError(
104+
"SQLite Cloud URL is required.\n"
105+
"Register on https://sqlitecloud.io/ to get your free SQLite Cloud account.\n"
106+
"Valid SQLite Cloud URL are:\n"
107+
" sqlite+sqlitecloud:///myuser:mypass@myserver.sqlite.cloud/mydb.sqlite?non_linearizable=true\n"
108+
" sqlite+sqlitecloud:///myserver.sqlite.cloud/?apikey=mykey1234"
109+
)
110+
111+
# TODO: this should be the list of SQLite Cloud Config params
112+
pysqlite_args = [
113+
# ("timeout", float),
114+
# ("isolation_level", str),
115+
("detect_types", int),
116+
]
117+
opts = url.query
118+
pysqlite_opts = {}
119+
for key, type_ in pysqlite_args:
120+
util.coerce_kw_type(opts, key, type_, dest=pysqlite_opts)
121+
122+
# sqlitecloud//...
123+
url = url.set(drivername="sqlitecloud")
124+
125+
return ([url.render_as_string(hide_password=False)], pysqlite_opts)
126+
127+
def is_disconnect(self, e, connection, cursor):
128+
return isinstance(
129+
e, self.dbapi.ProgrammingError
130+
) and "Cannot operate on a closed database." in str(e)
131+
132+
133+
dialect = SQLiteCloudDialect

0 commit comments

Comments
 (0)