今天晚上,我使用Datasette Lite在Datasette中发现了一个不起眼的错误。我认为这是一个很好的机会来强调拥有一个由 Pyodide 和 WebAssembly 提供支持的 URL 可寻址 Python 环境是多么有用。
这是帮助我发现该错误的页面:
为了解释这里发生的事情,让我们首先回顾一下各个组件。
数据集精简版
Datasette Lite是完全在浏览器中运行的Datasette版本。它运行在Pyodide上,我认为它仍然是 Python 生态系统中最被低估的项目。
大约三年前,我构建了 Datasette Lite 作为一个周末黑客项目,尝试看看是否可以让 Datasette(服务器端 Python Web 应用程序)完全在浏览器中运行。
从那时起,我添加了一系列功能,如 README 中所述– 最重要的是通过将 URL 传递给查询字符串参数来加载 SQLite 数据库、CSV 文件、JSON 文件或 Parquet 文件的能力。
我构建 Datasette Lite 几乎是一个笑话,认为没有人会愿意在每次想要探索一些数据时等待完整的 Python 解释器下载到他们的浏览器。事实证明,如今互联网连接速度很快,拥有一个只需要浏览器、GitHub Pages 的 Datasette 版本实际上非常有用。
就在前几天,我看到 Bellingcat 的 Logan Williams使用它来分享此 Excel 工作表的更好版本:
美国国家科学基金会 (NSF) 认可特德·克鲁兹 (Ted Cruz) 在 Datasette-Lite 中挑出的“新马克思主义阶级战争宣传”: lite.datasette.io?url=https://…
让我们看一下该 URL 的完整内容:
https://lite.datasette.io/?url=https://data-house-lake.nyc3.cdn.digitaloceanspaces.com/cruz_nhs.db#/cruz_nhs/grants
那里的?url=
参数指向一个 SQLite 数据库文件,该文件托管在 DigitalOcean Spaces 上,并提供最重要的access-control-allow-origin: *
标头,该标头允许 Datasette Lite 跨域加载它。
URL 的#/cruz_nhs/grants
部分告诉 Datasette Lite 在您访问该链接时要加载哪个页面。
Datasette Lite 中#
之后的任何内容都是传递到 WebAssembly 托管的 Datasette 实例的 URL。在此之前的任何查询字符串项都可用于影响 Datasette 实例的初始状态、导入数据甚至安装其他插件。
Datasette 1.0 阿尔法
我已经发布了很多Datasette alpha – 最新的是Datasette 1.0a17 。这些 alpha 版本被发布到PyPI ,这意味着它们可以使用pip install datasette==1.0a17
安装。
不久前,我向 Datasette Lite 本身添加了相同的功能。现在,您可以将&ref=1.0a17
传递到 Datasette Lite URL 以加载该特定版本的 Datasette。
这要归功于 Pyodide 的micropip机制的魔力。每次在浏览器中加载 Datasette Lite 时,它实际上是使用micropip
直接从 PyPI 安装所需的包。代码看起来像这样:
等待pyodide 。 loadPackage ( 'micropip' , { messageCallback : log } ) ; 让datasetteToInstall = '数据集' ; 让pre = 'False' ; if (设置. ref ) { if ( settings.ref == ' pre ' ) { 预= '真' ; }别的{ datasetteToInstall = `datasette== ${设置.参考} ` ; } } 等待自己。 pyodide 。 runPythonAsync ( ` 导入微管 等待 micropip.install(" ${ datasetteToInstall } ", pre= ${ pre } ) ` ) ;
该settings
对象已传递给加载 Datasette 的 Web Worker,其中包含各种查询字符串参数。
这意味着我可以将?ref=1.0a17
传递给 Datasette Lite 来加载特定版本,或者传递?ref=pre
来获取最近发布的预发布版本。
这也适用于插件
由于通过micropip
从 PyPI 加载额外的包非常简单,因此我更进一步添加了插件支持。
?install=
参数可以多次传递,每次指定应安装到浏览器中的 PyPI 中的 Datasette 插件。
自述文件包含大量该机制的实际示例。这是一个有趣的插件,它加载 datasette-mp3-audio以提供内联 MP3 播放小部件,最初是为我的ScotRail 音频公告项目创建的。
这仅适用于某些插件。它们需要是纯 Python 轮子 – 让具有编译的二进制依赖项的插件在 Pyodide WebAssembly 中工作需要一整套我还没有完全弄清楚的步骤。
令人沮丧的是,它还不适用于运行自己的 JavaScript 的插件!我可能需要重新构建 Datasette 和 Datasette Lite 的重要部分才能使其工作。
还值得注意的是,这是一个远程代码执行安全漏洞。我认为这不是问题,因为lite.datasette.io
故意托管在我从不打算使用 cookie 的域的子域上。破坏lite.datasette.io
的视觉显示是可能的,但不可能窃取任何私人数据或造成任何持久的损害。
数据集可见内部数据库
今晚的调试练习使用了一个名为datasette-visible-internal-db 的插件。
Datasette 的内部数据库是一个不可见的 SQLite 数据库,它位于 Datasette 的核心,跟踪加载的元数据和当前附加表的架构等内容。
不可见意味着我们可以将它用于用户不可见的功能 – 例如,记录 API 秘密或权限或跟踪评论或数据导入进度的插件。
在 Python 代码中,它的访问方式如下:
内部数据库=数据集。获取内部数据库()
与 Datasette 的其他数据库相反,它们的访问方式如下:
db =数据集。 get_database ( “我的数据库” )
有时,在攻击 Datasette 时,能够使用默认的 Datasette UI 浏览内部数据库会很有用。
这就是datasette-visible-internal-db
所做的。该插件的实现只有五行代码:
导入数据集 @数据集。胡克普尔 def启动(数据集): db =数据集。获取内部数据库() 数据集。 add_database ( db , name = "_internal" ,路线= "_internal" )
启动时,插件会获取对该内部数据库的引用,然后使用 Datasette 的add_database() 方法对其进行注册。这就是让它在 Datasette 内的/_internal
路径上显示为可见数据库所需的全部操作。
发现错误
我今天纯粹出于好奇而研究这个 – 我之前没有尝试过使用 Datasette Lite 进行?install=datasette-visible-internal-db
,我想看看它是否有效。
这是之前的 URL ,这次带有注释:
https://lite.datasette.io/ // Datasette Lite ?install=datasette-visible-internal-db // Install the visible internal DB plugin &ref=1.0a17 // Load the 1.0a17 alpha release #/_internal/catalog_columns // Navigate to the /_internal/catalog_columns table page &_facet=database_name // Facet by database_name for good measure
这就是我所看到的:
这一切看起来都不错…直到我单击了database_name
列中的_internal
链接…它带我到了这个 /_internal/databases/_internal 404 页面。
为什么是 404? Datasette 内省 SQLite 表架构以识别外键关系,然后将其转换为超链接。 catalog_columns
表(显示在表页面底部)的 SQL 架构如下所示:
创建表catalog_columns ( 数据库名称TEXT , 表名文本, cid整数, 名称文本, 输入文本, “非空”整数, default_value TEXT , --从 dflt_value 重命名 is_pk INTEGER , --从 pk 重命名 隐藏整数, 主键(数据库名称,表名称,名称), 外键(数据库名称)引用数据库(数据库名称), FOREIGN KEY (database_name, table_name) REFERENCES表(database_name, table_name) );
那些外键引用是一个错误!我不久前将内部表从databases
和tables
重命名为catalog_databases
和catalog_tables
,但显然忘记更新引用 – SQLite让我摆脱了它。
修复错误
我修复了这个提交中的错误。通常情况下,修复中最有趣的部分是附带的测试。我决定使用sqlite-utils中的内省助手来防止将来再次犯这样的错误:
@pytest 。标记。异步 异步def test_internal_foreign_key_references ( ds_client ): 内部数据库=等待确保内部( ds_client ) def内部( conn ): 数据库= sqlite_utils 。数据库(连接) 表名=数据库.表名() 对于db中的表。表: 对于表中的fk 。外键: 其他表= fk 。其他表 other_column = fk 。其他列 message = '列“{}.{}”引用了不存在的其他列“{}.{}”' 。格式( 桌子。名字,操。列、其他表、其他列 ) 在table_names中断言other_table ,消息+ “(坏表)” 在数据库[ other_table ]中断言other_column 。列_字典,( 消息+ “(坏列)” ) 等待内部数据库。执行_fn (内部)
这使用 Datasette 的await db.execute_fn()方法,该方法允许您运行在线程中访问 SQLite 的 Python 代码。然后,该代码可以使用阻塞sqlite-utils
内省方法– 在这里,我循环遍历该内部数据库中的每个表,循环遍历每个表.foreign_keys
并确认.other_table
和.other_column
值引用真正存在的表和列。
我运行了这个测试,看着它失败,然后应用修复程序并且它通过了。
URL 可寻址步骤重现
我在这里最想强调的想法是URL 可寻址步骤重现所提供的巨大价值。
拥有良好的重现步骤对于有效修复错误至关重要。您可以点击查看错误,这是 STR 最有效的形式。
理想情况下,这些 URL 将在未来很长一段时间内继续有效。
像 Datasette Lite 这样的系统的伟大之处在于,所有内容都是静态托管的文件。该应用程序本身托管在 GitHub Pages 上,它的工作原理是从各种不同的 CDN 加载附加文件。唯一的动态方面是针对 PyPI API 的缓存查找,我希望它在未来很长一段时间内保持稳定。
作为近 8 年以来Web 平台的稳定组件,WebAssembly 显然将继续存在。我预计 20 多年后我们将能够在浏览器中执行今天的 WASM 代码。
我相信过去几年我在 Datasette Lite 中探索的模式对于其他项目同样有价值。想象一下,使用静态 WebAssembly 构建来演示 Django 应用程序中的错误,并作为问题跟踪系统的一部分永久存档。
我认为 WebAssembly 和 Pyodide 对于更广泛的 Python 世界来说仍然有大量未开发的潜力。
标签: python 、 urls 、 datasette 、 web assembly 、 pyodide 、 datasette-lite
原文: https://simonwillison.net/2025/Feb/13/url-addressable-python/#atom-everything