use std::collections::{BTreeMap, HashMap};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::card::field::FieldSpec;
use crate::ids::{ColumnName, QueryName, SplitName};
use crate::reference::Ref;
pub mod validate;
use validate::DataCardError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct DataSpec {
pub interface: DataInterface,
pub schema: DataSchema,
#[serde(default)]
pub card_refs: Vec<Ref>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub splits: HashMap<SplitName, DataSplit>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_columns: Vec<ColumnName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sql: Option<SqlLogic>,
pub stats: DataStats,
}
impl DataSpec {
pub fn validate(&self) -> Result<(), DataCardError> {
validate::validate_data_spec(self)
}
#[must_use]
pub fn interface_kind(&self) -> &'static str {
self.interface.kind()
}
#[must_use]
pub fn is_tabular(&self) -> bool {
self.interface.requires_schema_columns()
}
pub fn card_refs(&self) -> impl Iterator<Item = &Ref> {
self.card_refs.iter()
}
#[must_use]
pub fn stats(&self) -> &DataStats {
&self.stats
}
pub fn target_columns(&self) -> impl Iterator<Item = &ColumnName> {
self.target_columns.iter()
}
#[must_use]
pub fn split(&self, label: &SplitName) -> Option<&DataSplit> {
self.splits.get(label)
}
pub fn materialized_split_refs(&self) -> impl Iterator<Item = &Ref> {
self.splits.values().filter_map(DataSplit::card_ref)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "kind", content = "meta")]
pub enum DataInterface {
Pandas(PandasMeta),
Polars(PolarsMeta),
Arrow(ArrowMeta),
Parquet(ParquetMeta),
Numpy(NumpyMeta),
Torch(TorchMeta),
Sql(SqlMeta),
Jsonl(JsonlMeta),
Image(ImageMeta),
Text(TextMeta),
Huggingface(HuggingfaceMeta),
Custom(CustomDataMeta),
}
impl DataInterface {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Pandas(_) => "Pandas",
Self::Polars(_) => "Polars",
Self::Arrow(_) => "Arrow",
Self::Parquet(_) => "Parquet",
Self::Numpy(_) => "Numpy",
Self::Torch(_) => "Torch",
Self::Sql(_) => "Sql",
Self::Jsonl(_) => "Jsonl",
Self::Image(_) => "Image",
Self::Text(_) => "Text",
Self::Huggingface(_) => "Huggingface",
Self::Custom(_) => "Custom",
}
}
#[must_use]
pub const fn requires_schema_columns(&self) -> bool {
matches!(
self,
Self::Pandas(_) | Self::Polars(_) | Self::Arrow(_) | Self::Parquet(_) | Self::Jsonl(_)
)
}
#[must_use]
pub const fn requires_sql_logic(&self) -> bool {
matches!(self, Self::Sql(_))
}
#[must_use]
pub const fn default_media_type(&self) -> &'static str {
match self {
Self::Pandas(_) | Self::Polars(_) | Self::Arrow(_) | Self::Parquet(_) => {
"application/x-parquet"
}
Self::Numpy(_) => "application/x-npy",
Self::Torch(_) => "application/vnd.safetensors",
Self::Sql(_) => "application/vnd.wyrd.sql+json",
Self::Jsonl(_) => "application/jsonl",
Self::Image(_) | Self::Text(_) => "application/json",
Self::Huggingface(_) => "application/vnd.wyrd.huggingface+json",
Self::Custom(_) => "application/octet-stream",
}
}
#[must_use]
pub const fn default_extension(&self) -> &'static str {
match self {
Self::Pandas(_) | Self::Polars(_) | Self::Arrow(_) | Self::Parquet(_) => "parquet",
Self::Numpy(_) => "npy",
Self::Torch(_) => "safetensors",
Self::Sql(_) | Self::Huggingface(_) => "json",
Self::Jsonl(_) => "jsonl",
Self::Image(_) | Self::Text(_) => "manifest.json",
Self::Custom(_) => "bin",
}
}
#[must_use]
pub fn manifest_ref(&self) -> Option<&Ref> {
match self {
Self::Image(meta) => meta.manifest_ref.as_ref(),
Self::Text(meta) => meta.manifest_ref.as_ref(),
_ => None,
}
}
#[must_use]
pub const fn loader_family(&self) -> &'static str {
match self {
Self::Pandas(_) => "pandas",
Self::Polars(_) => "polars",
Self::Arrow(_) => "pyarrow",
Self::Parquet(_) => "parquet",
Self::Numpy(_) => "numpy",
Self::Torch(_) => "torch",
Self::Sql(_) => "sql",
Self::Jsonl(_) => "jsonl",
Self::Image(_) => "image-manifest",
Self::Text(_) => "text-manifest",
Self::Huggingface(_) => "huggingface-datasets",
Self::Custom(_) => "custom-python-loader",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct DataSchema {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub columns: Vec<FieldSpec>,
}
impl DataSchema {
#[must_use]
pub fn new(columns: Vec<FieldSpec>) -> Self {
Self { columns }
}
#[must_use]
pub fn empty() -> Self {
Self {
columns: Vec::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
#[must_use]
pub fn contains_column(&self, name: &ColumnName) -> bool {
self.columns.iter().any(|field| &field.name == name)
}
#[must_use]
pub fn column(&self, name: &ColumnName) -> Option<&FieldSpec> {
self.columns.iter().find(|field| &field.name == name)
}
pub fn column_names(&self) -> impl Iterator<Item = &ColumnName> {
self.columns.iter().map(|field| &field.name)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct DataSplit {
pub label: SplitName,
pub strategy: SplitStrategy,
}
impl DataSplit {
#[must_use]
pub fn materialized(label: SplitName, card_ref: Ref) -> Self {
Self {
label,
strategy: SplitStrategy::Materialized(card_ref),
}
}
#[must_use]
pub fn column(label: SplitName, name: ColumnName, op: Inequality, value: ColValue) -> Self {
Self {
label,
strategy: SplitStrategy::Column { name, op, value },
}
}
#[must_use]
pub fn index_range(label: SplitName, start: i64, stop: i64) -> Self {
Self {
label,
strategy: SplitStrategy::IndexRange { start, stop },
}
}
#[must_use]
pub fn indices(label: SplitName, values: Vec<i64>) -> Self {
Self {
label,
strategy: SplitStrategy::Indices(values),
}
}
#[must_use]
pub fn card_ref(&self) -> Option<&Ref> {
self.strategy.materialized_card_ref()
}
#[must_use]
pub fn referenced_column(&self) -> Option<&ColumnName> {
self.strategy.referenced_column()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "kind", content = "value")]
pub enum SplitStrategy {
Materialized(Ref),
Column {
name: ColumnName,
op: Inequality,
value: ColValue,
},
IndexRange {
start: i64,
stop: i64,
},
Indices(Vec<i64>),
}
impl SplitStrategy {
#[must_use]
pub fn materialized_card_ref(&self) -> Option<&Ref> {
match self {
Self::Materialized(card_ref) => Some(card_ref),
_ => None,
}
}
#[must_use]
pub fn referenced_column(&self) -> Option<&ColumnName> {
match self {
Self::Column { name, .. } => Some(name),
_ => None,
}
}
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Materialized(_) => "Materialized",
Self::Column { .. } => "Column",
Self::IndexRange { .. } => "IndexRange",
Self::Indices(_) => "Indices",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum Inequality {
Eq,
Ne,
Gt,
Ge,
Lt,
Le,
In,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(untagged)]
pub enum ColValue {
Int(i64),
Float(f64),
Str(String),
Bool(bool),
Timestamp(DateTime<Utc>),
List(Vec<ColValue>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct SqlLogic {
pub queries: HashMap<QueryName, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_query: Option<QueryName>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct DataStats {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub row_count: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub col_count: Option<u32>,
pub byte_count: u64,
pub sha256: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct PandasMeta {
pub framework_version: String,
#[serde(default = "ParquetCompression::snappy")]
pub compression: ParquetCompression,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct PolarsMeta {
pub framework_version: String,
#[serde(default = "ParquetCompression::snappy")]
pub compression: ParquetCompression,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct ArrowMeta {
pub framework_version: String,
pub format: ArrowFormat,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct ParquetMeta {
#[serde(default = "ParquetCompression::snappy")]
pub compression: ParquetCompression,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub row_group_size: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct NumpyMeta {
pub dtype: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shape: Vec<i64>,
pub format: NumpyFormat,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct TorchMeta {
pub framework_version: String,
pub save_format: TorchSaveFormat,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct SqlMeta {
pub dialect: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub connection_hint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct JsonlMeta {
pub compression: JsonlCompression,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lines_per_file: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct ImageMeta {
pub format: ImageFormat,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub manifest_ref: Option<Ref>,
pub color_mode: ColorMode,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct TextMeta {
#[serde(default = "default_text_encoding")]
pub encoding: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub manifest_ref: Option<Ref>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct HuggingfaceMeta {
pub dataset_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub split: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct CustomDataMeta {
pub loader_module: String,
pub loader_class: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub extra: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum ParquetCompression {
None,
Snappy,
Gzip,
Zstd,
Lz4,
}
impl ParquetCompression {
#[must_use]
pub const fn snappy() -> Self {
Self::Snappy
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum ArrowFormat {
Ipc,
Parquet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum NumpyFormat {
Npy,
Npz,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum TorchSaveFormat {
Safetensors,
Pickle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum JsonlCompression {
None,
Gzip,
Zstd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum ImageFormat {
Png,
Jpeg,
Webp,
Mixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum ColorMode {
Rgb,
Rgba,
Grayscale,
}
fn default_text_encoding() -> String {
"utf-8".to_string()
}