跳转到主要内容

cas-manifest 允许开发者使用自描述清单在内容寻址存储中存储工件

项目描述

cas-manifest 提供了一种存储数据的方法,这些数据始终是不可变的,当需要时是稳定的,并且当需要时是灵活的。

cas-manifest 通过内容寻址存储存储数据工件。它促进了使用具有与数据共存并支持数据的标准可序列化包装的CAS。

为什么选择CAS?

简而言之,CAS强制执行不可变性。在使用CAS时,密钥的内容永远不会更改。以下内容随之而来:

  • 不再有 data_final__2_new 文件 - 所有对象都是唯一指定的
  • 不再需要缓存失效 - 自由地缓存对象,知道它们的内容永远不会在上游更改
  • 不再有来源问题 - 模型可以稳健地链接到用于训练它们的数据集

在CAS存储中,您不是在Key处 put 一个Value,而是 put 一个Value,并返回由该值唯一确定的Key。

为什么选择清单?

将一些数据放入二进制工件并保留一个引用它的键是非常好的。难点在于,当你得到一个键时,你怎么知道那里存储了什么?如果你确定了一个标准的序列化方法,你怎么让它进化同时保持向后兼容性?

cas-manifest鼓励使用清单类来应对这些挑战。这些清单类包括序列化和反序列化工件所需的代码。它们提供了一个存储关于工件元数据的地方——这可以用于反序列化,用来指示加载的数据应如何使用,或者可能仅仅是信息性的。最后,清单类中的字段可以引用CAS中的其他对象,从而使对象可以组合和重用。与CAS结合使用,您可以确保您的元数据和底层数据始终保持同步,因为您的元数据将引用底层数据的不可变引用。

示例

实现序列化

假设我们希望存储我们在内存中以pandas Dataframe表示的数据集。我们将创建一个cas_manifest.Serializable的子类来表示我们希望存储的内容

class CSVSerializable(Serializable[pd.DataFrame]):

column_names: List[str]
path: Ref

@classmethod
def pack(cls, inst: pd.DataFrame, fs: HashFS) -> CSVSerializable:
    with tempfile.TemporaryDirectory() as tmpdir:
        tmp_path = Path(tmpdir) / 'tmp.csv'
        with open(tmp_path, mode='w') as f:
            inst.to_csv(f, header=False, index=False)
        csv_addr = fs.put(tmp_path)
        return CSVSerializable(path=Ref(csv_addr.id), column_names=inst.columns.to_list())

def unpack(self, fs: HashFS) -> pd.DataFrame:
    addr = fs.get(self.path.hash_str)
    df = pd.read_csv(addr.abspath, names=self.column_names)
    return df

让我们逐一分析这些项

class CSVSerializable(Serializable[pd.DataFrame]):

Serializable的类型参数是我们内存中使用的类型。无论您在应用程序代码中使用什么,您都将其放在这里。

column_names: List[str]
path: Ref

cas-manifest使用pydantic来定义其清单类。这允许您在顶层指定类字段。为了确保它们可以被序列化,这些字段必须是简单类型或其他pydantic类。Ref是一个特殊包装类,用于在cas中引用其他对象。

@classmethod
def pack(cls, inst: pd.DataFrame, fs: HashFS) -> CSVSerializable:
    with tempfile.TemporaryDirectory() as tmpdir:
        tmp_path = Path(tmpdir) / 'tmp.csv'
        with open(tmp_path, mode='w') as f:
            inst.to_csv(f, header=False, index=False)
        csv_addr = fs.put(tmp_path)
        return CSVSerializable(path=Ref(csv_addr.id), column_names=inst.columns.to_list())

pack是一个必需的方法;它指定了您的数据应如何被序列化。注意参数的类型。

在此方法体内,我们将我们的dataframe保存为csv,不带标题行。然后我们将它放入pack方法作为参数接收的HashFS实例中。HashFS提供了我们使用的CAS的实现。那次put操作返回了我们的csv的地址(或键)。然后我们可以构建一个我们的包装类实例,它包含一个指向csv文件的Ref,以及清单类中的字段作为列名。

def unpack(self, fs: HashFS) -> pd.DataFrame:
    addr = fs.get(self.path.hash_str)
    df = pd.read_csv(addr.abspath, names=self.column_names)
    return df

unpack是另一个必需的方法;它表明您的序列化数据应如何被反序列化。同样,注意参数的类型和返回类型。在这种情况下,我们得到保存的csv文件在文件系统中的位置。然后我们调用pandas.read_csv来读取它,并传递清单类中存储的列名。

请注意,在现实世界中遵循此模式时,通常会难以跟踪列名是作为标题行存储还是保存在其他地方,是否应有索引列等。这听起来很愚蠢,但它确实是一个问题——尤其是如果您想改变主意的话!cas-manifest通过在清单类中嵌入逻辑来标准化这些决策。

存储和检索数据

现在我们已经实现了所有这些,我们该如何使用它呢?

首先,我们需要将某些内容放入cas。假设我们有一个名为df的DataFrame。我们还需要从hashfs包中获取一个HashFS实例。AWS用户可能希望使用此包中的S3HashFS,它提供了由S3支持的HashFS的实现。然后我们可以做以下操作

import pandas as pd
from hashfs import HashFS, HashAddress
df: pd.DataFrame = ...
fs_instance: HashFS = ...
addr: HashAddress = CSVSerializable.dump(df, fs_instance)

给定一个HashFS实例,我们可以对CSVSerializable调用dump来序列化我们的DataFrame并将其存储在fs_instance中。返回的对象是一个HashAddress,它包括序列化对象的不可变哈希,以及诸如其磁盘上的路径等辅助信息。如果我们想访问序列化表示,我们也可以调用CSVSerializable.pack(df, fs_instance)来获取一个CSVSerializable实例。

现在,我们如何从存储中检索我们的序列化对象呢?同样,使用我们的HashFS实例,我们将做以下操作

hash_str = addr.id
# We create a Registry that knows what classes to expect
registry: SerializableRegistry[pd.DataFrame] = \
    SerializableRegistry(fs=fs_instance, classes=[CSVSerializable])
# We can `open` a hash address to get access to the dataframe
with registry.open(hash_str) as df:
    pass # df is the DataFrame that we saved before
# Or we can get the serialized form directly
serialized: CSVSerializable = registry.load(addr.id)

为什么open是一个上下文管理器?一些Serializable的实现可能创建需要清理的临时资源,所以我们把open当作打开和关闭文件一样处理。

序列化模式的演变

现在,让我们假设我们决定想要更改我们的序列化格式。也许我们想利用numpy的序列化方法来存储我们的dataframe中的数据。我们可以创建另一个Serializable的子类,像之前一样实现packunpack

class NPYSerializable(Serializable[pd.DataFrame]):
    ...

这里为了简洁省略了实现细节,但实现可以在tests/dataset.py中找到。

我们可以用这种新格式序列化dataframe,就像我们用CSVSerializable做的那样。当我们想要加载数据时,事情变得有趣起来。

registry_2: SerializableRegistry[pd.DataFrame] = \
    SerializableRegistry(fs=fs_instance, classes=[CSVSerializable, NPYSerializable])

registry_2现在知道如何反序列化存储在两种格式中的数据。你可以传递一个与任何格式相对应的哈希字符串,它将正确地将它反序列化为DataFrame。

这意味着你不需要在代码库中搜索数据是如何存储在磁盘上的,并四处散布代码。你可以在类中整合你的序列化/反序列化逻辑,让cas-manifest处理后续的处理。

注意事项

  • 关于可移植性和模式演变:请记住,你的代码没有被序列化。所以,为了加载类型为X的对象,你仍然需要在代码库中提供X。创建你的注册表应该会清楚地说明这一点。
  • 与上述内容相关,如果你对类进行了更改,你必须确保它们是向后兼容的(例如,添加可选字段),以便能够加载旧数据。
  • 类型:我已经尽我所能提供正确的类型注解,但mypy在推断某些泛型函数的返回类型时很困难。显式类型注解可能很有帮助。

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。

源分发

cas-manifest-0.4.1.tar.gz (8.6 kB 查看哈希值)

上传时间

构建分发

cas_manifest-0.4.1-py3-none-any.whl (8.9 kB 查看哈希值)

上传时间 Python 3

支持者