Skip to content

Commit 52416a7

Browse files
committed
Adapting project to last datasette version (WIP)
1 parent 1a34f76 commit 52416a7

File tree

7 files changed

+225
-126
lines changed

7 files changed

+225
-126
lines changed

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/connectors.py

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,101 @@
11
import pkg_resources
2+
import functools
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+
def for_each_connector(func):
7+
@functools.wraps(func)
8+
def wrapper_for_each_connector(*args, **kwargs):
9+
for connector in db_connectors.values():
10+
try:
11+
return func(connector, *args, **kwargs)
12+
except:
13+
pass
14+
else:
15+
raise Exception("No database connector found!!")
16+
return wrapper_for_each_connector
17+
18+
19+
class ConnectorList:
20+
@staticmethod
21+
def load():
22+
for entry_point in pkg_resources.iter_entry_points('datasette.connectors'):
23+
db_connectors[entry_point.name] = entry_point.load()
24+
25+
@staticmethod
26+
def add_connector(name, connector):
27+
db_connectors[name] = connector
28+
29+
@staticmethod
30+
@for_each_connector
31+
def table_names(connector, path):
32+
return connector.table_names(path)
33+
34+
@staticmethod
35+
@for_each_connector
36+
def hidden_table_names(connector, path):
37+
return connector.hidden_table_names(path)
38+
39+
@staticmethod
40+
@for_each_connector
41+
def view_names(connector, path):
42+
return connector.view_names(path)
43+
44+
@staticmethod
45+
@for_each_connector
46+
def table_columns(connector, path, table):
47+
return connector.table_columns(path, table)
48+
49+
@staticmethod
50+
@for_each_connector
51+
def primary_keys(connector, path, table):
52+
return connector.primary_keys(path, table)
53+
54+
@staticmethod
55+
@for_each_connector
56+
def fts_table(connector, path, table):
57+
return connector.fts_table(path, table)
58+
59+
@staticmethod
60+
@for_each_connector
61+
def get_all_foreign_keys(connector, path):
62+
return connector.get_all_foreign_keys(path)
63+
64+
@staticmethod
65+
@for_each_connector
66+
def table_counts(connector, path, *args, **kwargs):
67+
return connector.table_counts(path, *args, **kwargs)
68+
69+
70+
class Connector:
71+
@staticmethod
72+
def table_names(path):
73+
return []
74+
75+
@staticmethod
76+
def hidden_table_names(path):
77+
return []
78+
79+
@staticmethod
80+
def view_names(path):
81+
return []
82+
83+
@staticmethod
84+
def table_columns(path, table):
85+
return []
86+
87+
@staticmethod
88+
def primary_keys(path, table):
89+
return []
90+
91+
@staticmethod
92+
def fts_table(path, table):
93+
return None
94+
95+
@staticmethod
96+
def get_all_foreign_keys(path):
97+
return {}
98+
99+
@staticmethod
100+
def table_counts(path, *args, **kwargs):
101+
return {}

datasette_connectors/monkey.py

Lines changed: 84 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,93 @@
1-
import asyncio
2-
import datasette
3-
from datasette.app import connections
4-
from datasette.inspect import inspect_hash
5-
from datasette.utils import Results
6-
from pathlib import Path
1+
import threading
72
import sqlite3
3+
import datasette.views.base
4+
from datasette.database import Database
85

9-
from . import connectors
6+
from .connectors import ConnectorList
7+
8+
connections = threading.local()
109

1110

1211
def patch_datasette():
1312
"""
1413
Monkey patching for original Datasette
1514
"""
1615

17-
def inspect(self):
18-
" Inspect the database and return a dictionary of table metadata "
19-
if self._inspect:
20-
return self._inspect
21-
22-
_inspect = {}
23-
files = self.files
24-
25-
for filename in files:
26-
self.files = (filename,)
27-
path = Path(filename)
28-
name = path.stem
29-
if name in _inspect:
30-
raise Exception("Multiple files with the same stem %s" % name)
31-
try:
32-
_inspect[name] = self.original_inspect()[name]
33-
except sqlite3.DatabaseError:
34-
tables, views, dbtype = connectors.inspect(path)
35-
_inspect[name] = {
36-
"hash": inspect_hash(path),
37-
"file": str(path),
38-
"dbtype": dbtype,
39-
"tables": tables,
40-
"views": views,
41-
}
42-
43-
self.files = files
44-
self._inspect = _inspect
45-
return self._inspect
46-
47-
datasette.app.Datasette.original_inspect = datasette.app.Datasette.inspect
48-
datasette.app.Datasette.inspect = inspect
49-
50-
51-
async def execute(self, db_name, sql, params=None, truncate=False, custom_time_limit=None, page_size=None):
52-
"""Executes sql against db_name in a thread"""
53-
page_size = page_size or self.page_size
54-
55-
def is_sqlite3_conn():
56-
conn = getattr(connections, db_name, None)
57-
if not conn:
58-
info = self.inspect()[db_name]
59-
return info.get('dbtype', 'sqlite3') == 'sqlite3'
60-
else:
61-
return isinstance(conn, sqlite3.Connection)
62-
63-
def sql_operation_in_thread():
64-
conn = getattr(connections, db_name, None)
65-
if not conn:
66-
info = self.inspect()[db_name]
67-
conn = connectors.connect(info['file'], info['dbtype'])
68-
setattr(connections, db_name, conn)
69-
70-
rows, truncated, description = conn.execute(
71-
sql,
72-
params or {},
73-
truncate=truncate,
74-
page_size=page_size,
75-
max_returned_rows=self.max_returned_rows,
76-
)
77-
return Results(rows, truncated, description)
78-
79-
if is_sqlite3_conn():
80-
return await self.original_execute(db_name, sql, params=params, truncate=truncate, custom_time_limit=custom_time_limit, page_size=page_size)
81-
else:
82-
return await asyncio.get_event_loop().run_in_executor(
83-
self.executor, sql_operation_in_thread
84-
)
85-
86-
datasette.app.Datasette.original_execute = datasette.app.Datasette.execute
87-
datasette.app.Datasette.execute = execute
16+
async def table_names(self):
17+
try:
18+
return await self.original_table_names()
19+
except sqlite3.DatabaseError:
20+
return ConnectorList.table_names(self.path)
21+
22+
Database.original_table_names = Database.table_names
23+
Database.table_names = table_names
24+
25+
26+
async def hidden_table_names(self):
27+
try:
28+
return await self.original_hidden_table_names()
29+
except sqlite3.DatabaseError:
30+
return ConnectorList.hidden_table_names(self.path)
31+
32+
Database.original_hidden_table_names = Database.hidden_table_names
33+
Database.hidden_table_names = hidden_table_names
34+
35+
36+
async def view_names(self):
37+
try:
38+
return await self.original_view_names()
39+
except sqlite3.DatabaseError:
40+
return ConnectorList.view_names(self.path)
41+
42+
Database.original_view_names = Database.view_names
43+
Database.view_names = view_names
44+
45+
46+
async def table_columns(self, table):
47+
try:
48+
return await self.original_table_columns(table)
49+
except sqlite3.DatabaseError:
50+
return ConnectorList.table_columns(self.path, table)
51+
52+
Database.original_table_columns = Database.table_columns
53+
Database.table_columns = table_columns
54+
55+
56+
async def primary_keys(self, table):
57+
try:
58+
return await self.original_primary_keys(table)
59+
except sqlite3.DatabaseError:
60+
return ConnectorList.primary_keys(self.path, table)
61+
62+
Database.original_primary_keys = Database.primary_keys
63+
Database.primary_keys = primary_keys
64+
65+
66+
async def fts_table(self, table):
67+
try:
68+
return await self.original_fts_table(table)
69+
except sqlite3.DatabaseError:
70+
return ConnectorList.fts_table(self.path, table)
71+
72+
Database.original_fts_table = Database.fts_table
73+
Database.fts_table = fts_table
74+
75+
76+
async def get_all_foreign_keys(self):
77+
try:
78+
return await self.original_get_all_foreign_keys()
79+
except sqlite3.DatabaseError:
80+
return ConnectorList.get_all_foreign_keys(self.path)
81+
82+
Database.original_get_all_foreign_keys = Database.get_all_foreign_keys
83+
Database.get_all_foreign_keys = get_all_foreign_keys
84+
85+
86+
async def table_counts(self, *args, **kwargs):
87+
counts = await self.original_table_counts(**kwargs)
88+
# If all tables has None as counts, an error had ocurred
89+
if len(list(filter(lambda table_count: table_count is not None, counts.values()))) == 0:
90+
return ConnectorList.table_counts(self.path, *args, **kwargs)
91+
92+
Database.original_table_counts = Database.table_counts
93+
Database.table_counts = table_counts

setup.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ def get_long_description():
2222
url='https://github.com/pytables/datasette-connectors',
2323
license='Apache License, Version 2.0',
2424
packages=['datasette_connectors'],
25-
install_requires=['datasette==0.46'],
26-
tests_require=['pytest', 'aiohttp'],
25+
install_requires=['datasette==0.48'],
26+
tests_require=[
27+
'pytest',
28+
'aiohttp',
29+
'asgiref',
30+
],
2731
entry_points='''
2832
[console_scripts]
2933
datasette=datasette_connectors.cli:cli

tests/dummy.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
11
from datasette_connectors.row import Row
2+
from datasette_connectors.connectors import Connector
23

34

4-
_connector_type = 'dummy'
5+
class DummyConnector(Connector):
6+
_connector_type = 'dummy'
7+
8+
@staticmethod
9+
def table_names(path):
10+
return ['table1', 'table2']
11+
12+
@staticmethod
13+
def table_columns(path, table):
14+
return ['c1', 'c2', 'c3']
15+
16+
@staticmethod
17+
def get_all_foreign_keys(path):
18+
return {
19+
'table1': {'incoming': [], 'outgoing': []},
20+
'table2': {'incoming': [], 'outgoing': []},
21+
}
22+
23+
@staticmethod
24+
def table_counts(path, *args, **kwargs):
25+
return {
26+
'table1': 2,
27+
'table2': 2,
28+
}
29+
530

631
def inspect(path):
732
tables = {}

tests/fixtures.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from datasette_connectors import monkey; monkey.patch_datasette()
2-
from datasette_connectors import connectors
3-
from . import dummy
4-
connectors.db_connectors['dummy'] = dummy
2+
from datasette_connectors.connectors import ConnectorList
3+
from .dummy import DummyConnector
4+
ConnectorList.add_connector('dummy', DummyConnector)
55

66
from datasette.app import Datasette
7+
from datasette.utils.testing import TestClient
78
import os
89
import pytest
910
import tempfile
1011

12+
1113
@pytest.fixture(scope='session')
1214
def app_client(max_returned_rows=None):
1315
with tempfile.TemporaryDirectory() as tmpdir:
@@ -20,7 +22,7 @@ def app_client(max_returned_rows=None):
2022
'max_returned_rows': max_returned_rows or 1000,
2123
}
2224
)
23-
client = ds.app().test_client
25+
client = TestClient(ds.app())
2426
client.ds = ds
2527
yield client
2628

0 commit comments

Comments
 (0)