Skip to content

Commit 2bbd810

Browse files
authored
Merge pull request #1 from PyTables/dependabot/pip/datasette-0.46
Bump datasette from 0.25 to 0.51.1
2 parents b0802bd + 903b711 commit 2bbd810

File tree

14 files changed

+487
-276
lines changed

14 files changed

+487
-276
lines changed

ANNOUNCE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Announcing datasette-connectors 1.0.0
1+
# Announcing datasette-connectors 2.0.0
22

33
## What's new
44

5-
This is the first release of datasette-connectors, derived from the [Datasette](https://github.com/simonw/datasette) fork for supporting [Datasette-Pytables](https://github.com/PyTables/datasette-pytables).
5+
This is a mayor version of datasette-connectors. There are two main features added. First, [Datasette 0.51.1](https://github.com/simonw/datasette) is used. Second, our API has been improved to a more pythonic style. Details can be found at [dummy example](https://github.com/PyTables/datasette-connectors/blob/master/tests/dummy.py).
66

77
## What it is
88

RELEASE_NOTES.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Release notes for datasette-connectors
22

33

4-
## Changes from 1.0.0 to 1.0.1
4+
## Changes from 1.0.0 to 2.0.0
55

6-
#XXX version-specific blurb XXX#
6+
* Compatible with Datasette 0.51.1
7+
8+
* A lot of changes in API adopting a more pythonic style.
79

810

911
## Initial version 1.0.0

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.1-dev
1+
2.0.0

datasette_connectors/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .connectors import Connector
2+
from .connection import Connection
3+
from .cursor import OperationalError

datasette_connectors/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .monkey import patch_datasette; patch_datasette()
2-
from .connectors import load; load()
2+
from .connectors import ConnectorList; ConnectorList.load()
33
from datasette.cli import cli

datasette_connectors/connection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from .cursor import Cursor
2+
3+
4+
class Connection:
5+
def __init__(self, path, connector_class):
6+
self.path = path
7+
self.connector_class = connector_class
8+
9+
def execute(self, *args, **kwargs):
10+
cursor = Cursor(self)
11+
cursor.execute(*args, **kwargs)
12+
return cursor
13+
14+
def cursor(self):
15+
return Cursor(self)
16+
17+
def set_progress_handler(self, handler, n):
18+
pass

datasette_connectors/connectors.py

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,123 @@
1-
import pkg_resources
1+
from .connection import Connection
2+
23

34
db_connectors = {}
45

5-
def load():
6-
for entry_point in pkg_resources.iter_entry_points('datasette.connectors'):
7-
db_connectors[entry_point.name] = entry_point.load()
8-
9-
def inspect(path):
10-
for connector in db_connectors.values():
11-
try:
12-
return connector.inspect(path)
13-
except:
14-
pass
15-
else:
16-
raise Exception("No database connector found for %s" % path)
17-
18-
def connect(path, dbtype):
19-
try:
20-
return db_connectors[dbtype].Connection(path)
21-
except:
22-
raise Exception("No database connector found for %s" % path)
6+
7+
class ConnectorList:
8+
@staticmethod
9+
def load():
10+
for entry_point in pkg_resources.iter_entry_points('datasette.connectors'):
11+
db_connectors[entry_point.name] = entry_point.load()
12+
13+
@staticmethod
14+
def add_connector(name, connector):
15+
db_connectors[name] = connector
16+
17+
class DatabaseNotSupported(Exception):
18+
pass
19+
20+
@staticmethod
21+
def connect(path):
22+
for connector in db_connectors.values():
23+
try:
24+
return connector.connect(path)
25+
except:
26+
pass
27+
else:
28+
raise ConnectorList.DatabaseNotSupported
29+
30+
31+
class Connector:
32+
connector_type = None
33+
connection_class = Connection
34+
35+
@classmethod
36+
def connect(cls, path):
37+
return cls.connection_class(path, cls)
38+
39+
def __init__(self, conn):
40+
self.conn = conn
41+
42+
def table_names(self):
43+
"""
44+
Return a list of table names
45+
"""
46+
raise NotImplementedError
47+
48+
def hidden_table_names(self):
49+
raise NotImplementedError
50+
51+
def detect_spatialite(self):
52+
"""
53+
Return boolean indicating if geometry_columns exists
54+
"""
55+
raise NotImplementedError
56+
57+
def view_names(self):
58+
"""
59+
Return a list of view names
60+
"""
61+
raise NotImplementedError
62+
63+
def table_count(self, table_name):
64+
"""
65+
Return an integer with the rows count of the table
66+
"""
67+
raise NotImplementedError
68+
69+
def table_info(self, table_name):
70+
"""
71+
Return a list of dictionaries with columns description, with format:
72+
[
73+
{
74+
'idx': 0,
75+
'name': 'column1',
76+
'primary_key': False,
77+
},
78+
...
79+
]
80+
"""
81+
raise NotImplementedError
82+
83+
def detect_fts(self, table_name):
84+
"""
85+
Return boolean indicating if table has a corresponding FTS virtual table
86+
"""
87+
raise NotImplementedError
88+
89+
def foreign_keys(self, table_name):
90+
"""
91+
Return a list of dictionaries with foreign keys description
92+
id, seq, table_name, from_, to_, on_update, on_delete, match
93+
"""
94+
raise NotImplementedError
95+
96+
def table_exists(self, table_name):
97+
"""
98+
Return boolean indicating if table exists in the database
99+
"""
100+
raise NotImplementedError
101+
102+
def table_definition(self, table_type, table_name):
103+
"""
104+
Return string with a 'CREATE TABLE' sql definition
105+
"""
106+
raise NotImplementedError
107+
108+
def indices_definition(self, table_name):
109+
"""
110+
Return a list of strings with 'CREATE INDEX' sql definitions
111+
"""
112+
raise NotImplementedError
113+
114+
def execute(
115+
self,
116+
sql,
117+
params=None,
118+
truncate=False,
119+
custom_time_limit=None,
120+
page_size=None,
121+
log_sql_errors=True,
122+
):
123+
raise NotImplementedError

datasette_connectors/cursor.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import re
2+
import sqlite3
3+
4+
from .row import Row
5+
6+
7+
class OperationalError(Exception):
8+
pass
9+
10+
11+
class Cursor:
12+
class QueryNotSupported(Exception):
13+
pass
14+
15+
def __init__(self, conn):
16+
self.conn = conn
17+
self.connector = conn.connector_class(conn)
18+
self.rows = []
19+
self.description = ()
20+
21+
def execute(
22+
self,
23+
sql,
24+
params=None,
25+
truncate=False,
26+
custom_time_limit=None,
27+
page_size=None,
28+
log_sql_errors=True,
29+
):
30+
if params is None:
31+
params = {}
32+
results = []
33+
truncated = False
34+
description = ()
35+
36+
# Normalize sql
37+
sql = sql.strip()
38+
sql = ' '.join(sql.split())
39+
40+
if sql == "select name from sqlite_master where type='table'" or \
41+
sql == "select name from sqlite_master where type=\"table\"":
42+
results = [{'name': name} for name in self.connector.table_names()]
43+
elif sql == "select name from sqlite_master where rootpage = 0 and sql like '%VIRTUAL TABLE%USING FTS%'":
44+
results = [{'name': name} for name in self.connector.hidden_table_names()]
45+
elif sql == 'select 1 from sqlite_master where tbl_name = "geometry_columns"':
46+
if self.connector.detect_spatialite():
47+
results = [{'1': '1'}]
48+
elif sql == "select name from sqlite_master where type='view'":
49+
results = [{'name': name} for name in self.connector.view_names()]
50+
elif sql.startswith("select count(*) from ["):
51+
match = re.search(r'select count\(\*\) from \[(.*)\]', sql)
52+
results = [{'count(*)': self.connector.table_count(match.group(1))}]
53+
elif sql.startswith("select count(*) from "):
54+
match = re.search(r'select count\(\*\) from (.*)', sql)
55+
results = [{'count(*)': self.connector.table_count(match.group(1))}]
56+
elif sql.startswith("PRAGMA table_info("):
57+
match = re.search(r'PRAGMA table_info\(\[?\"?([\d\w\/%]*)\"?\]?\)', sql)
58+
results = self.connector.table_info(match.group(1))
59+
elif sql.startswith("select name from sqlite_master where rootpage = 0 and ( sql like \'%VIRTUAL TABLE%USING FTS%content="):
60+
match = re.search(r'select name from sqlite_master where rootpage = 0 and \( sql like \'%VIRTUAL TABLE%USING FTS%content="(.*)"', sql)
61+
if self.connector.detect_fts(match.group(1)):
62+
results = [{'name': match.group(1)}]
63+
elif sql.startswith("PRAGMA foreign_key_list(["):
64+
match = re.search(r'PRAGMA foreign_key_list\(\[(.*)\]\)', sql)
65+
results = self.connector.foreign_keys(match.group(1))
66+
elif sql == "select 1 from sqlite_master where type='table' and name=?":
67+
if self.connector.table_exists(params[0]):
68+
results = [{'1': '1'}]
69+
elif sql == "select sql from sqlite_master where name = :n and type=:t":
70+
if self.connector.table_exists(params['n']):
71+
results = [{'sql': self.connector.table_definition(params['t'], params['n'])}]
72+
elif sql == "select sql from sqlite_master where tbl_name = :n and type='index' and sql is not null":
73+
results = [{'sql': sql} for sql in self.connector.indices_definition(params['n'])]
74+
else:
75+
try:
76+
results, truncated, description = \
77+
self.connector.execute(
78+
sql,
79+
params=params,
80+
truncate=truncate,
81+
custom_time_limit=custom_time_limit,
82+
page_size=page_size,
83+
log_sql_errors=log_sql_errors,
84+
)
85+
except OperationalError as ex:
86+
raise sqlite3.OperationalError(*ex.args)
87+
88+
self.rows = [Row(result) for result in results]
89+
self.description = description
90+
91+
def fetchall(self):
92+
return self.rows
93+
94+
def fetchmany(self, max):
95+
return self.rows[:max]
96+
97+
def __getitem__(self, index):
98+
return self.rows[index]

0 commit comments

Comments
 (0)