JMP 19 将在 JMP 18 中引入的数据连接器功能的基础上进行扩展,从而实现对更多数据源的统一访问。除了新增更多内置连接类型外,JMP 19 还允许用户通过公开的 Python API 定义自定义连接类型。与内置类型一样,这些自定义连接类型也将受益于数据连接器功能,包括其配置管理和查询构建器集成。此外,由于它们是用 Python 实现的,因此可以利用现有的 Python 库。本次演示将介绍该 API,探讨其中涉及的概念,并展示其实际应用。
大家好。今天我来聊聊JMP的数据连接器框架,特别是JMP 19中如何使用Python对其进行扩展。在JMP 18中,我们引入了数据连接器框架,并用它来改进与ODBC的连接。在JMP 19中,我们将进一步扩展这一功能。
您可能在其他地方听说过我们如何利用它重新连接到 SaaS,以及如何提供与 Snowflake 的原生连接。我们还使用我接下来要讲的这个框架——这个 Python API——来实现与 Amazon S3 和 Microsoft SharePoint 的连接,这些连接不会与 JMP 捆绑在一起,而是通过 JMP Marketplace 分发。
本次演讲我想涵盖几个方面。首先,我想简要说明一下,如果您有自己的数据源,为什么要将其与 JMP 集成。如果您确实想要集成,那么您需要了解它应该如何融入 JMP 的系统中,JMP 需要做什么才能与您的数据源通信,以及您需要提供哪些信息才能实现这一目标。特别是,您需要编写哪些代码。
最后,我们将讨论如何共享代码。您编写的代码对您自己有效,但公司里的其他人可能也想使用它。您可能希望与世界各地的其他 JMP 用户共享代码。为了简化这一过程,我们选择了 Python,部分原因是 Python 拥有丰富的库,或者您可能已经编写过相关的代码。我们就是这样实现了与 Amazon S3 和 Microsoft SharePoint 的连接。我们能够封装现有的 Python 库,希望这也能让您更轻松地使用代码。
除了我们力求简化集成流程之外,您可能想要集成的另一个原因是它能够帮助您管理配置。用户可以使用连接器编辑器和查询构建器创建和编辑描述连接的配置,从而以图形化的方式创建查询、筛选数据、选择列等等。最后,您还可以进行协作。您创建的连接和配置可以通过插件导出和共享。
如果您之前没见过这项功能,稍后我会演示其实际应用,您就会更清楚地了解它。但在那之前,我们先来谈谈各个组件之间的关系,以及它们是如何协同工作的。首先,您需要找到数据源,并让它与 JMP 通信。
但是,在 JMP 中,我们所说的数据指的是数据表。有些数据表可以直接从数据源获取,有些则需要一些额外的处理。例如,您可能有一个 Web 端点,它会返回这种层级结构的 JSON 数据。在这种情况下,就需要进行一些处理。但只要它能够以数据表的形式提供给 JMP,就可以被使用。
查询构建器在访问这些数据时,会将其视为传统数据库,例如 MySQL 或 SQL Server。它假定数据已组织成命名表,并且这些表可能进一步组织成命名模式。
但您可能拥有不止一个数据源。即使只有一个数据源,也至少有多种访问方式。用户可能拥有不同的凭据。或者您可能只是不想硬编码数据地址。总之,您很可能需要一些选项。通过为这些选项提供特定值,您可以获得一个配置。然后,使用该配置,您可以创建数据源。这些配置都可以使用数据连接器编辑器创建。
以上就是简要概述。但它如何转化为代码呢?我们来看看。我现在在 JMP 里。我们先把这个窗口移开,重新开始。左边这段代码实现了几种数据连接器类型。类型可以是像 ODBC 这样的 JMP 内置类型。它也可以是 SaaS 类型,例如本地、远程或通过网络连接。它还可以是 Snowflake。但无论如何,对于任何特定的类型,你都可以选择使用 Python 来实现。
这里顶部有一些导入语句。特别是,我们导入了内置的 JMP 模块。这里还有一个实用函数,可以用来确保我们能够看到函数何时被调用。在这个演示中,之所以使用它,是因为 JMP 会调用代码。我们不会频繁地调用代码。但为了理解程序运行的机制,跟踪函数调用的时机非常重要。
下面的代码才是真正实现这个类型的部分。在深入讲解之前,我们先看一下我手头的参考资料。这个列表中的各种元素在很多方面都与我刚才展示的图表相对应。我们需要一些选项。我们将通过继承 jmp.DataConnectorType 类,并定义一个字段属性来定义这些选项。我们需要将配置信息转换为数据源。我们将继承 jmp.DataConnector 类,并定义一个名为 _do_as_data_source 的函数来执行这种转换。
最后,我们来看看数据源本身。这是另一个子类。我们将定义一些函数,用于了解是否存在模式、有哪些表以及特定表的数据内容。虽然我们不会在本演示中看到,但需要记住一点:如果您有一些资源不会自动清理,例如,如果您有一些连接本身没有这种行为,那么您需要找到 Python 的特殊双下划线方法,以便在不再引用这些资源时进行清理。因为 JMP 会将清理工作交给 Python 来处理关闭数据源的情况。
考虑到这一点,我们再来看一下代码。我们看到 SimpleFolderConnectorType,它是 jmp.DataConnectorType 的子类。正如前面提到的,它有一些字段,每个字段都有相应的选项。这里第一个也是唯一一个选项是“Folder”。
在这种情况下,我们所说的文件夹,或者说我们的 SimpleFolder 连接,实际上就是我们的数据源。它对应于我本地文件系统上的一个文件夹,该文件夹将以其中的文件形式公开数据。
首先,我们来定义一个名为“Folder”的元素,它在这个描述选项的字典中的值是字符串类型,即 `str | None`。这意味着我们可以设置一个可选的字符串值。接下来是配置类 `SimpleFolderConnector`,它是 `jmp.DataConnector` 的子类。它定义了 `_do_as_data_source` 函数,该函数会根据我们的配置进行数据转换(或者说连接)。
我们可以像使用内置字典一样,通过索引自身来访问配置值“文件夹”。我们会检查它是否为空(即用户是否省略了这个默认值),如果是,则抛出一个错误来告知用户这是必需的。
最后,我们将它转换为数据源。数据源类是列表中的最后一个。当然,我们必须能够创建它。我们定义一个初始化函数。在本例中,我们检查它是否确实是一个目录,因为我们想要做的是让用户(包括你自己或任何使用代码的人)知道是否存在错误。创建数据源时,应该同时进行连接。在本例中,实际上不需要进行任何连接,但我们至少可以检查文件夹是否存在。
接下来要介绍的是获取模式(get-schemas)。在这种情况下,我们所说的数据源没有模式,可以直接访问表。因此,我们无需定义获取模式,JMP 会自动识别出我们的数据源未使用模式。
但我们必须先定义这些表。虽然这里需要传入一个模式参数,但我们会忽略它,因为我们不会用到这些表。不过,既然我们要查看文件,就需要给这些文件命名。这里就直接使用文件名。我们会遍历文件夹,并返回其中每个文件的名称。
最后,我们需要获取数据表。这就是最后一个打开表函数的作用。首先,我们需要重建包含数据的文件路径。我们将使用表名(即文件名加上文件夹后缀),然后将路径作为字符串返回。
我刚才提到的模型需要返回一个数据表,你可以这样做。如果你有一个像我之前举例的 Web 端点,它会返回一个层级结构的 JSON 或其他格式的数据。你可能需要自己将其转换为数据表或直接返回。
但在这个例子中,由于文件仅来自文件系统,而我们在创建 S3 和 SharePoint 加载项时也遇到过这种情况,而且我认为我们也听到过用户对访问其他类似文件的数据存储方式的兴趣,因此,我们无需直接打开文件,只需传递这个字符串即可。然后 JMP 会为我们打开文件。
它会处理诸如确保文件以私密方式打开之类的事情。这里返回的数据表是一个实现细节,不应该直接暴露给用户。使用查询构建器时,您不会看到中间表,只会看到最终结果。在这种情况下,JMP 会为我们处理这些。这样做还可以让 JMP 保留打开表时可能涉及的任何设置。稍后我会详细讨论这一点。
最后,我们来看一下这里的调用:函数 jmp.DataConnector.tie。它的作用是接收两个参数:类型类和连接器类,并确保它们彼此关联。目前,它们之间没有互相引用,但顶部的 DataConnectorType 类是 JMP 用作访问点的。类型是一个包罗万象的概念,它包含了构成它们连接的所有代码和定义。它们需要彼此关联。
最后,我在这里加一条注释,说明最终的类型字符串是什么。如果是 DBC,那就是 ODBC。也可能是 SAS Viya 或 SAS Remote。在本例中,对于 Python,我们用 Python 冒号表示这是一个函数类型。`__main__` 是模块的名称,因为我们直接从 JMP 运行了它。最后,`SimpleFolderConnectorType` 是这个类的名称。
我们来看个实际操作。如果我打开“数据连接器”对话框,创建一个新的连接,就会打开相应的“数据连接器编辑器”。如果我们把类型改成 Python,就可以在这里输入剩余的字符串。我们把这些内容填进去。
完成上述操作后,我们可以看到“文件夹”选项。可以看到它没有值,也就是 None。所以,如果我尝试测试一下(我们尝试连接),调用这个函数和这个函数,就会看到错误信息:“文件夹是必需的”。
现在,为了实际访问一些数据,我将使用 JMP 自带的示例导入数据。就是这个文件夹。我要复制一下。再显示一遍。我们复制路径,然后在 JMP 中输入,并粘贴到这里。我们看到了测试结果。现在一切正常,所以我们继续连接。
现在连接成功了,您可以看到这里列出了一些表。实际上,这些表对应于我们这里的文件。我们在这里看到了 Animals_L.txt,在这里又看到了它。查看日志,我们可以看到使用日志条目函数非常有效。我们可以看到,在测试过程中,我们多次调用了 _do_as_data_source 函数,也多次调用了数据源的初始化函数。我们还看到 get-tables 函数被调用,它再次提供了这个列表。
如果我在这里调用查询构建器,我们就可以看到我们可以访问哪些列。可以看到,如果我们继续构建查询,就可以添加这些列。我们还可以进行一些筛选。例如,我们可以只选择其中两个季节。如果我们查看 SQL 语句,可以看到它包含了所有这些信息。运行查询,我们就得到了结果。
你可能想知道这一切是如何运作的。我们创建了一个 SQL 查询,并运行了 SQL 代码。然而,在我们之前定义这些内容时,几乎没有 SQL 的概念。它之所以能够运行,是因为我们提供了打开表的调用,提供了获取数据的方法。然后 JMP 会帮我们完成剩下的工作。它使用 SQLite,一种底层嵌入式数据库,以便暴露我们拥有的数据并运行 SQL 代码。例如,那个 WHERE 子句可以让我们只看到其中两个季节的数据。
我们来看看更复杂的文件。或许我们可以回到我们最喜欢的那个大类文件,也就是这里Excel里的文件。再次打开查询生成器。我们看到,在实际操作之前,会弹出这个对话框。我们认为,如果文件(听不清 00:17:43),就必须提示用户才能指定文件内容。当JMP尝试打开这个文件时,它会提示你填写导入向导所需的信息。
一切看起来都没问题,点击“导入”。现在您可以看到,和之前一样,我可以看到列名,也可以看到快照。我现在就点击“导入”。我们看到数据已经导入了。现在,您可能想知道,我每次都要这样做吗?答案是:不用。
我们来看一下这个源脚本。在顶部,我们可以看到预期的内容。这里有“新建 SQL 查询”。这里还有连接信息,在本例中是由数据连接器提供的。可以看到它引用了我们的类型,以及我们指定的文件夹。
但当我们查看表格时,会发现这里有一个新选项:“打开设置”。“打开设置”会捕获导入向导中指定的设置。具体来说,它会获取底层大型类表源脚本中传递给“打开”命令的额外参数,并将其提供在此处。
这样,如果我继续运行,再次执行,你看,所有内容都已打开,一切正常。如果我们对设置做了非标准操作,比如需要从后面的列开始,那么这些操作仍然会反映在这里。从日志中我们可以看到,整个过程又重新开始了。我们可以看到,我们再次调用了 `_do_as_data_source` 函数。然后我们获取了表列表,并再次打开了这个表。
以上是基本操作。这个看起来稍微复杂一些。但它不仅更复杂,功能也更齐全。这再次表明,这是你能做的最基本、最小的配置,能够建立连接。
现在看起来更高级了。我们将不再使用简单的文件夹连接器,而是一个功能齐全的文件夹连接器。你看,字段定义要复杂得多,但它仍然是由与之前类似的构建模块组成的。
首先,这里新增的是 `jmp.DataConnectoGroupedFields` 的使用。它的作用是允许我们指定字段所属的不同组。这会影响它们在数据连接器编辑器中的显示方式。对于 ODBC,选项会被分组,我们可以在 Python 中使用相同的功能。
每个组都指定此列表中的一对值,这些值将传递给 `jmp.DataConnectorGroupedFields`。我们有名称,还有一个字典,类似于之前的字典,其中包含选项名称和字段定义。字段定义中仍然可以看到类型,即 `str | None`。但我们还将其包装在 `jmp.DataConnectorField` 中,我给它起了个别名,因为我们多次使用它,而且它有点长,目的是为了指定附加信息,例如,当您将鼠标悬停在选项名称上时,将在数据连接器编辑器中显示的工具提示。
您可以指定默认值。您还可以指定一个名称,该名称将显示在数据连接器编辑器中,并且与将在 JSL 配置文件中使用的名称不同。例如,我们在 JMP 中使用 ODBC 时就使用了此功能,以便本地化选项名称或提供比默认名称更友好的字符串,因为默认情况下,这些字符串用于 JSL。因此,我们需要一个 JSL 风格的名称。
这里的“Ordner”我认为是德语中“文件夹”的意思。我们稍后会看到。至于数据源的查看方式有何变化,它仍然基于文件夹,但我们也会选择性地支持模式。在这种情况下,我们可以选择不按之前的方式查看数据,而是采用一种新的方式:我们将此处提供的文件夹的子文件夹视为模式,然后将这些子文件夹中的文件视为数据表。
我们将提供另一种选择,通过限制为某些扩展名并进行裁剪,来改变我们查看从表名到底层文件的映射的方式。
连接时,我们将数据转换为数据源的过程类似,但稍微复杂一些,因为我们有更多选项。我们仍然会勾选“文件夹”,但还会勾选扩展名选项。我们会抛出一个错误,然后将所有数据传递给新的数据源。
我们的新数据源会先检查文件夹,并设置其他选项。它会提供模式。如果我们使用这些模式,那么代码会类似这样,遍历文件夹下的每个子文件夹。任何子文件夹的目录都会被获取其名称,并将其用作模式的名称。
但是,如果我们不使用模式,如果我们没有设置新的高级选项,那么我们将返回 None。之前我们没有定义模式,也没有使用 get-schemas,但这里我们不能选择性地定义一个函数,所以,你可以返回 None 来表示“我没有任何模式”。我们没有使用模式。
Get-tables 函数的思路与之前类似。最终目标是获取一系列文件的名称。但由于选项众多,我们可以采用几种不同的方式来实现。首先,我们会获取目录。具体方式取决于数据库模式。实际上,这取决于我们是否使用了数据库模式。然后,我们会先筛选出文件,再获取文件名;如果需要处理文件扩展名,则会采用更复杂的方法。
最后,为了获取数据表,我们反向执行了 get-schemas 和 get-tables 的过程。我们需要重构路径。我们可能会把文件扩展名加回去。但是这样无法获取到相关的目录,所以我们会在再次将其作为路径返回之前,把文件名加回路径末尾……再次将路径作为字符串返回。我们把它组合起来,然后就得到了之前的类型名称。
我们来看看。我先滚动一下回到这里,方便我们参考。和之前一样,我们在这里设置类型。可以看到,这里有我们自定义的名称和工具提示。文件夹方面,我们和之前一样。我们来获取它的路径。实际上,我们再往上找一个。我们使用架构功能。我们复制一下。
那么我们可以说我们确实想使用子文件夹作为架构。既然我们添加了这个高级选项,那就用它吧。确保一切看起来都没问题。嗯,连接成功了。让我们确保所有窗口都位于最前面最显眼的位置。
现在我们在左侧看到模式,它们实际上只是文件夹名称。这是我们导入的数据。可以看到我们已将其限制为 .txt 文件。像之前一样,以动物为例,打开它。数据已导入。一切正常。这里也像之前一样有调用。如果我们查看日志,可以看到调用了 get-schemas、get-tables、我们的实用函数以及 open-table。
你已经做了这么多工作来创建这些类型。现在你想与他人分享。这是我们最后要讨论的内容,即如何做到这一点。我还有另一份清单。
你已经编写了 Python 代码,但根据你的编写方式,你可能需要将其组织成一个包。组织成包的目的是确保所有代码集中在一起,并且可以使用像 pip 这样的工具进行安装,或者在本例中,使用 JMP 封装的 jpip,这样你就可以在安装时指定依赖项。例如,对于 S3,我们依赖于 PyPI 上的 Boto3 包,使用包提供了一种非常自然的方式来指定这种依赖关系。
你需要创建基础配置。记得之前我直接在数据连接器编辑器中填写类型名称吗?这样做当然没问题,但如果我能直接从配置列表中找到合适的配置就方便多了。这样我就可以根据列表中的配置创建新的配置,而不用记住完整的名称了。
你可能需要编写一个安装脚本。我们有一个很棒的 Python 包,但我们需要把它放在一个真正可以使用的地方。如果能自动完成,那就太好了。
最后,我们希望把所有功能都整合到一个插件里。插件安装起来更方便,也更容易通过应用市场分发。总之,所有功能都集中在一个软件包里。
我们来看看它长什么样。我这里有一个插件,其中包含了我们今天早些时候看到的那个代码的进一步开发版本。我已经把它解压出来了,这样我们就能看到里面都有些什么。我记得列表里的第一个东西是 Python 包。我已经把它创建好了。我们有了 Python 代码,但它被组织成一个 Python 项目……抱歉,是 Python 包。我们有 pyproject.toml 文件。今天我实在没时间详细讲解如何构建这样的项目,不过网上有很多相关的资源。
我们有一个“数据连接器”文件夹。这里存放着插件的配置信息,在本例中是基本配置。例如,如果我查看其中一个配置,会发现它只有一行,用于指定类型。它没有指定其他配置,但为用户提供了一个可以指向的参考点。然后,在本例中,我们还有 addinLoad,它包含安装脚本。
当然,您可以尝试直接创建所有这些,但您也可以使用 JMP 的插件创建工具。依次点击“文件”、“新建”、“插件”,然后我可以……我可以输入名称。我可以输入 ID。我可以指定最低 JMP 版本,在本例中应该是 19。启动脚本部分用于放置安装该软件包的脚本。我们希望确保它只安装一次。
我们已经有了要添加的文件。在这个例子中,它们已经解压在这里了,但如果您是第一次自己创建,这些文件可能放在其他地方。我们可以添加软件包。你看,所有东西都在这里了。我们可以添加数据连接器。如果我们有下标,那么我们就可以真正保存并创建我们自己的插件了。一旦所有东西都准备就绪,创建过程应该就非常简单了。
这就是我想在这里展示的全部内容。我们来总结一下。首先,我想谈谈为什么要进行集成。正如我一开始所讲的,数据连接器框架在编辑配置、共享配置以及使用查询构建器交互式地构建查询方面提供了许多功能。这就为封装数据源提供了一个很好的理由。这就是它的作用所在。我们之前讨论过,在 JMP 中,一切都是数据表,而我们希望让它看起来像一个数据库。
要编写哪些代码?我们讲解了需要定义的各种子类、需要编写的各种函数以及如何指定选项。最后,我们讨论了如何创建插件,以及如何与他人分享您的代码或将其发布到 JMP Marketplace。
从某种程度上说,这算是对所有可能性的一个相对快速的概述。你们中的一些人可能会认真做笔记,并截取大量的屏幕截图或照片,但如果你和我一样,那么你可能还需要一些其他的参考资料。好消息是,我们已经有了这些信息,而且我们还在不断创建更多信息。
我今天展示的 API 目前已有文档。您可以在“数据访问”部分的 EA 说明中找到它。您需要确保获取的是最新版本,但如果 EA 没有更新,则最新版本可能并非当前版本。我们正在添加一些文档,这些文档将位于脚本索引中。虽然它无法提供像 EA 说明中那样全面的概述,但希望能作为 API 中各个功能的参考。
最后,我之前展示的那个插件,我们正在进行一些开发,并将把它发布到 JMP Marketplace 上。我们希望它能成为一个资源,既展示 API,也展示如何创建 Python 包,以及如何将其发布为插件。
我们会再次展示所有文档,包括你今天看到的所有内容,还会展示一些我没时间讲解的更高级但可能仍然有用的功能。一定要看看。
最后,我想谈谈我们未来的发展方向。在 JMP 20 及更高版本中,我们正在考虑对该 API 进行一些扩展。例如,您可能已经注意到,为了处理您的数据,JMP 实际上会将所有数据(包括整个数据表)导入本地,因为这是您需要从 `opentable` 函数返回的内容。然后,正如我之前提到的,我们会通过 SQLite 在本地使用它。
但如果部分处理工作可以在服务器端完成,而无需将所有内容下载到本地,那么我们希望支持这种方式。这也是我们正在研究的方向,看看如何提供类似的功能。
好了,我想我们今天的分享就到这里。谢谢聆听。我希望您能运用今天所学的知识,创建新的数据源,并将其封装到这个数据连接器框架中。这样,您就可以将这些数据源发布为插件,从而更轻松、更高效地处理数据,并将其导入 JMP。再次感谢。就这些。