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
Get Datasette compatible with Pyodide #1733
Comments
I released a
datasette/datasette/actor_auth_cookie.py Lines 16 to 20 in 0a7621f
Datasette never actually sets that cookie itself - it instead encourages plugins to set it in the authentication documentation here: https://docs.datasette.io/en/0.61.1/authentication.html#including-an-expiry-time |
I was going to vendor I used https://cs.github.com/ and as far as I can tell there aren't any! So I'm going to remove that dependency and work out a smarter way to do this - probably by providing a utility function within Datasette itself. |
Here's the full diff I applied to Datasette to get it fully working in Pyodide: And as a visible diff: diff --git a/datasette/app.py b/datasette/app.py
index d269372..6c0c5fc 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -15,7 +15,6 @@ import pkg_resources
import re
import secrets
import sys
-import threading
import traceback
import urllib.parse
from concurrent import futures
@@ -26,7 +25,6 @@ from itsdangerous import URLSafeSerializer
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
from jinja2.environment import Template
from jinja2.exceptions import TemplateNotFound
-import uvicorn
from .views.base import DatasetteError, ureg
from .views.database import DatabaseDownload, DatabaseView
@@ -813,7 +811,6 @@ class Datasette:
},
"datasette": datasette_version,
"asgi": "3.0",
- "uvicorn": uvicorn.__version__,
"sqlite": {
"version": sqlite_version,
"fts_versions": fts_versions,
@@ -854,23 +851,7 @@ class Datasette:
]
def _threads(self):
- threads = list(threading.enumerate())
- d = {
- "num_threads": len(threads),
- "threads": [
- {"name": t.name, "ident": t.ident, "daemon": t.daemon} for t in threads
- ],
- }
- # Only available in Python 3.7+
- if hasattr(asyncio, "all_tasks"):
- tasks = asyncio.all_tasks()
- d.update(
- {
- "num_tasks": len(tasks),
- "tasks": [_cleaner_task_str(t) for t in tasks],
- }
- )
- return d
+ return {"num_threads": 0, "threads": []}
def _actor(self, request):
return {"actor": request.actor}
diff --git a/datasette/database.py b/datasette/database.py
index ba594a8..b50142d 100644
--- a/datasette/database.py
+++ b/datasette/database.py
@@ -4,7 +4,6 @@ from pathlib import Path
import janus
import queue
import sys
-import threading
import uuid
from .tracer import trace
@@ -21,8 +20,6 @@ from .utils import (
)
from .inspect import inspect_hash
-connections = threading.local()
-
AttachedDatabase = namedtuple("AttachedDatabase", ("seq", "name", "file"))
@@ -43,12 +40,12 @@ class Database:
self.hash = None
self.cached_size = None
self._cached_table_counts = None
- self._write_thread = None
- self._write_queue = None
if not self.is_mutable and not self.is_memory:
p = Path(path)
self.hash = inspect_hash(p)
self.cached_size = p.stat().st_size
+ self._read_connection = None
+ self._write_connection = None
@property
def cached_table_counts(self):
@@ -134,60 +131,17 @@ class Database:
return results
async def execute_write_fn(self, fn, block=True):
- task_id = uuid.uuid5(uuid.NAMESPACE_DNS, "datasette.io")
- if self._write_queue is None:
- self._write_queue = queue.Queue()
- if self._write_thread is None:
- self._write_thread = threading.Thread(
- target=self._execute_writes, daemon=True
- )
- self._write_thread.start()
- reply_queue = janus.Queue()
- self._write_queue.put(WriteTask(fn, task_id, reply_queue))
- if block:
- result = await reply_queue.async_q.get()
- if isinstance(result, Exception):
- raise result
- else:
- return result
- else:
- return task_id
-
- def _execute_writes(self):
- # Infinite looping thread that protects the single write connection
- # to this database
- conn_exception = None
- conn = None
- try:
- conn = self.connect(write=True)
- self.ds._prepare_connection(conn, self.name)
- except Exception as e:
- conn_exception = e
- while True:
- task = self._write_queue.get()
- if conn_exception is not None:
- result = conn_exception
- else:
- try:
- result = task.fn(conn)
- except Exception as e:
- sys.stderr.write("{}\n".format(e))
- sys.stderr.flush()
- result = e
- task.reply_queue.sync_q.put(result)
+ # We always treat it as if block=True now
+ if self._write_connection is None:
+ self._write_connection = self.connect(write=True)
+ self.ds._prepare_connection(self._write_connection, self.name)
+ return fn(self._write_connection)
async def execute_fn(self, fn):
- def in_thread():
- conn = getattr(connections, self.name, None)
- if not conn:
- conn = self.connect()
- self.ds._prepare_connection(conn, self.name)
- setattr(connections, self.name, conn)
- return fn(conn)
-
- return await asyncio.get_event_loop().run_in_executor(
- self.ds.executor, in_thread
- )
+ if self._read_connection is None:
+ self._read_connection = self.connect()
+ self.ds._prepare_connection(self._read_connection, self.name)
+ return fn(self._read_connection)
async def execute(
self,
diff --git a/setup.py b/setup.py
index 7f0562f..c41669c 100644
--- a/setup.py
+++ b/setup.py
@@ -44,20 +44,20 @@ setup(
install_requires=[
"asgiref>=3.2.10,<3.6.0",
"click>=7.1.1,<8.2.0",
- "click-default-group~=1.2.2",
+ # "click-default-group~=1.2.2",
"Jinja2>=2.10.3,<3.1.0",
"hupper~=1.9",
"httpx>=0.20",
"pint~=0.9",
"pluggy>=1.0,<1.1",
- "uvicorn~=0.11",
+ # "uvicorn~=0.11",
"aiofiles>=0.4,<0.9",
"janus>=0.6.2,<1.1",
"asgi-csrf>=0.9",
"PyYAML>=5.3,<7.0",
"mergedeep>=1.1.1,<1.4.0",
"itsdangerous>=1.1,<3.0",
- "python-baseconv==1.2.2",
+ # "python-baseconv==1.2.2",
],
entry_points="""
[console_scripts] |
Maybe I can leave Welcome to the Pyodide terminal emulator 🐍
Python 3.10.2 (main, Apr 9 2022 20:52:01) on WebAssembly VM
Type "help", "copyright", "credits" or "license" for more information.
>>> import micropip
>>> await micropip.install("uvicorn")
>>> import uvicorn
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/lib/python3.10/site-packages/uvicorn/__init__.py", line 1, in <module>
from uvicorn.config import Config
File "/lib/python3.10/site-packages/uvicorn/config.py", line 8, in <module>
import ssl
File "/lib/python3.10/ssl.py", line 98, in <module>
import _ssl # if we can't import it, let the error propagate
ModuleNotFoundError: No module named '_ssl'
>>> import ssl
>>> import uvicorn
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/lib/python3.10/site-packages/uvicorn/__init__.py", line 2, in <module>
from uvicorn.main import Server, main, run
File "/lib/python3.10/site-packages/uvicorn/main.py", line 24, in <module>
from uvicorn.supervisors import ChangeReload, Multiprocess
File "/lib/python3.10/site-packages/uvicorn/supervisors/__init__.py", line 3, in <module>
from uvicorn.supervisors.basereload import BaseReload
File "/lib/python3.10/site-packages/uvicorn/supervisors/basereload.py", line 12, in <module>
from uvicorn.subprocess import get_subprocess
File "/lib/python3.10/site-packages/uvicorn/subprocess.py", line 14, in <module>
multiprocessing.allow_connection_pickling()
File "/lib/python3.10/multiprocessing/context.py", line 170, in allow_connection_pickling
from . import connection
File "/lib/python3.10/multiprocessing/connection.py", line 21, in <module>
import _multiprocessing
ModuleNotFoundError: No module named '_multiprocessing'
>>> import multiprocessing
>>> import uvicorn
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/lib/python3.10/site-packages/uvicorn/__init__.py", line 2, in <module>
from uvicorn.main import Server, main, run
File "/lib/python3.10/site-packages/uvicorn/main.py", line 24, in <module>
from uvicorn.supervisors import ChangeReload, Multiprocess
File "/lib/python3.10/site-packages/uvicorn/supervisors/__init__.py", line 3, in <module>
from uvicorn.supervisors.basereload import BaseReload
File "/lib/python3.10/site-packages/uvicorn/supervisors/basereload.py", line 12, in <module>
from uvicorn.subprocess import get_subprocess
File "/lib/python3.10/site-packages/uvicorn/subprocess.py", line 14, in <module>
multiprocessing.allow_connection_pickling()
File "/lib/python3.10/multiprocessing/context.py", line 170, in allow_connection_pickling
from . import connection
File "/lib/python3.10/multiprocessing/connection.py", line 21, in <module>
import _multiprocessing
ModuleNotFoundError: No module named '_multiprocessing'
>>> Since the But it looks like i can address this issue just by making |
I'm going to add a Datasette setting to disable threading entirely, designed for usage in this particular case. I thought about adding a new setting, then I noticed this:
I'm going to let users set that to |
I'll release this as a |
I got a build from the
That I can avoid it by running this first though:
|
This is good enough to push an alpha. |
That alpha release works! https://pyodide.org/en/stable/console.html Welcome to the Pyodide terminal emulator 🐍
Python 3.10.2 (main, Apr 9 2022 20:52:01) on WebAssembly VM
Type "help", "copyright", "credits" or "license" for more information.
>>> import micropip
>>> await micropip.install("datasette==0.62a0")
>>> import ssl
>>> import setuptools
>>> from datasette.app import Datasette
>>> ds = Datasette(memory=True, settings={"num_sql_threads": 0})
>>> await ds.client.get("/.json")
<Response [200 OK]>
>>> (await ds.client.get("/.json")).json()
{'_memory': {'name': '_memory', 'hash': None, 'color': 'a6c7b9', 'path': '/_memory', 'tables_and_views_truncated': [], 'tab
les_and_views_more': False, 'tables_count': 0, 'table_rows_sum': 0, 'show_table_row_counts': False, 'hidden_table_rows_sum'
: 0, 'hidden_tables_count': 0, 'views_count': 0, 'private': False}}
>>> |
I've already got this working as a prototype. Here are the changes I had to make:
click-default-group
andpython-baseconv
uvicorn
dependency optional (only needed when Datasette runs in the CLI)TODO:
click-default-group-wheel
uvicorn
import errorGoal is to be able to do the following directly in https://pyodide.org/en/stable/console.html
The text was updated successfully, but these errors were encountered: