diff --git a/pins/_adaptors.py b/pins/_adaptors.py
new file mode 100644
index 00000000..5df3b982
--- /dev/null
+++ b/pins/_adaptors.py
@@ -0,0 +1,154 @@
+from __future__ import annotations
+
+import json
+from abc import abstractmethod
+from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias, overload
+
+from databackend import AbstractBackend
+
+if TYPE_CHECKING:
+ import pandas as pd
+
+ PandasDataFrame: TypeAlias = pd.DataFrame
+ DataFrame: TypeAlias = PandasDataFrame
+
+
+class AbstractPandasFrame(AbstractBackend):
+ _backends = [("pandas", "DataFrame")]
+
+
+AbstractDF: TypeAlias = AbstractPandasFrame
+
+
+class Adaptor:
+ def __init__(self, data: Any) -> None:
+ self._d = data
+
+ def write_json(self, file: str) -> None:
+ with open(file, "w") as f:
+ f.write(self.to_json())
+
+ def to_json(self) -> str:
+ import json
+
+ return json.dumps(self._d)
+
+ def write_joblib(self, file: str) -> None:
+ import joblib
+
+ joblib.dump(self._d, file)
+
+ def write_csv(self, file: str) -> None:
+ msg = f"Writing to CSV is not supported for {type(self._d)}"
+ raise NotImplementedError(msg)
+
+ def write_parquet(self, file: str) -> None:
+ msg = f"Writing to Parquet is not supported for {type(self._d)}"
+ raise NotImplementedError(msg)
+
+ def write_feather(self, file: str) -> None:
+ msg = f"Writing to Feather is not supported for {type(self._d)}"
+ raise NotImplementedError(msg)
+
+ @property
+ def data_preview(self) -> str:
+ # note that the R library uses jsonlite::toJSON
+ import json
+
+ # TODO(compat): set display none in index.html
+ return json.dumps({})
+
+ def default_title(self, name: str) -> str:
+ # TODO(compat): title says CSV rather than data.frame
+ # see https://github.com/machow/pins-python/issues/5
+ return f"{name}: a pinned {self._obj_name}"
+
+ @property
+ def _obj_name(self) -> str:
+ return f"{type(self._d).__qualname__} object"
+
+
+class DFAdaptor(Adaptor):
+ _d: ClassVar[DataFrame]
+
+ def __init__(self, data: DataFrame) -> None:
+ super().__init__(data)
+
+ @property
+ def df_type(self) -> str:
+ # Consider over-riding this for specialized dataframes
+ return "DataFrame"
+
+ @property
+ @abstractmethod
+ def columns(self) -> list[Any]: ...
+
+ @property
+ @abstractmethod
+ def shape(self) -> tuple[int, int]: ...
+
+ @abstractmethod
+ def head(self, n: int) -> DFAdaptor: ...
+
+ @property
+ def data_preview(self) -> str:
+ # TODO(compat) is 100 hard-coded?
+ # Note that we go df -> json -> dict, to take advantage of type conversions in the dataframe library
+ data: list[dict[Any, Any]] = json.loads(self.head(100).to_json())
+ columns = [
+ {"name": [col], "label": [col], "align": ["left"], "type": [""]}
+ for col in self.columns
+ ]
+
+ # this reproduces R pins behavior, by omitting entries that would be null
+ data_no_nulls = [{k: v for k, v in row.items() if v is not None} for row in data]
+
+ return json.dumps({"data": data_no_nulls, "columns": columns})
+
+ @property
+ def _obj_name(self) -> str:
+ row, col = self.shape
+ return f"{row} x {col} {self.df_type}"
+
+
+class PandasAdaptor(DFAdaptor):
+ _d: ClassVar[PandasDataFrame]
+
+ def __init__(self, data: AbstractPandasFrame) -> None:
+ super().__init__(data)
+
+ @property
+ def columns(self) -> list[Any]:
+ return self._d.columns.tolist()
+
+ @property
+ def shape(self) -> tuple[int, int]:
+ return self._d.shape
+
+ def head(self, n: int) -> PandasAdaptor:
+ return PandasAdaptor(self._d.head(n))
+
+ def to_json(self) -> str:
+ return self._d.to_json(orient="records")
+
+ def write_csv(self, file: str) -> None:
+ self._d.to_csv(file, index=False)
+
+ def write_parquet(self, file: str) -> None:
+ self._d.to_parquet(file)
+
+ def write_feather(self, file: str) -> None:
+ self._d.to_feather(file)
+
+
+@overload
+def create_adaptor(obj: DataFrame) -> DFAdaptor: ...
+@overload
+def create_adaptor(obj: Any) -> Adaptor: ...
+def create_adaptor(obj: Any | DataFrame) -> Adaptor | DFAdaptor:
+ if isinstance(obj, AbstractPandasFrame):
+ return PandasAdaptor(obj)
+ elif isinstance(obj, Adaptor):
+ return obj
+ else:
+ return Adaptor(obj)
diff --git a/pins/boards.py b/pins/boards.py
index 70fc8ba4..2ec7a269 100644
--- a/pins/boards.py
+++ b/pins/boards.py
@@ -10,11 +10,12 @@
from datetime import datetime, timedelta
from io import IOBase
from pathlib import Path
-from typing import Protocol
+from typing import Any, Protocol
from importlib_resources import files
from importlib_resources.abc import Traversable
+from ._adaptors import Adaptor, create_adaptor
from .cache import PinsCache
from .config import get_allow_rsc_short_name
from .drivers import REQUIRES_SINGLE_FILE, default_title, load_data, load_file, save_data
@@ -25,6 +26,8 @@
_log = logging.getLogger(__name__)
+_ = default_title # Keep this import for backward compatibility
+
class IFileSystem(Protocol):
protocol: str | list
@@ -715,7 +718,7 @@ def prepare_pin_version(
def _create_meta(
self,
pin_dir_path,
- x,
+ x: Adaptor | Any,
name: str | None = None,
type: str | None = None,
title: str | None = None,
@@ -732,7 +735,7 @@ def _create_meta(
raise NotImplementedError("Type argument is required.")
if title is None:
- title = default_title(x, name)
+ title = create_adaptor(x).default_title(name)
# create metadata from object on disk ---------------------------------
# save all pin data to a temporary folder (including data.txt), so we
@@ -1223,7 +1226,7 @@ def prepare_pin_version(self, pin_dir_path, x, name: str | None, *args, **kwargs
# render index.html ------------------------------------------------
all_files = [meta.file] if isinstance(meta.file, str) else meta.file
- pin_files = ", ".join(f"""{x}""" for x in all_files)
+ pin_files = ", ".join(f"""{file}""" for file in all_files)
context = {
"date": meta.version.created.replace(microsecond=0),
@@ -1231,38 +1234,9 @@ def prepare_pin_version(self, pin_dir_path, x, name: str | None, *args, **kwargs
"pin_files": pin_files,
"pin_metadata": meta,
"board_deparse": board_deparse(self),
+ "data_preview": create_adaptor(x).data_preview,
}
- # data preview ----
-
- # TODO: move out data_preview logic? Can we draw some limits here?
- # note that the R library uses jsonlite::toJSON
-
- import json
-
- import pandas as pd
-
- if isinstance(x, pd.DataFrame):
- # TODO(compat) is 100 hard-coded?
- # Note that we go df -> json -> dict, to take advantage of pandas type conversions
- data = json.loads(x.head(100).to_json(orient="records"))
- columns = [
- {"name": [col], "label": [col], "align": ["left"], "type": [""]}
- for col in x
- ]
-
- # this reproduces R pins behavior, by omitting entries that would be null
- data_no_nulls = [
- {k: v for k, v in row.items() if v is not None} for row in data
- ]
-
- context["data_preview"] = json.dumps(
- {"data": data_no_nulls, "columns": columns}
- )
- else:
- # TODO(compat): set display none in index.html
- context["data_preview"] = json.dumps({})
-
# do not show r code if not round-trip friendly
if meta.type in ["joblib"]:
context["show_r_style"] = "display:none"
diff --git a/pins/drivers.py b/pins/drivers.py
index fcf9bee3..f0262979 100644
--- a/pins/drivers.py
+++ b/pins/drivers.py
@@ -1,5 +1,8 @@
from collections.abc import Sequence
from pathlib import Path
+from typing import Any
+
+from pins._adaptors import Adaptor, create_adaptor
from .config import PINS_ENV_INSECURE_READ, get_allow_pickle_read
from .errors import PinsInsecureReadError
@@ -13,15 +16,6 @@
REQUIRES_SINGLE_FILE = frozenset(["csv", "joblib"])
-def _assert_is_pandas_df(x, file_type: str) -> None:
- import pandas as pd
-
- if not isinstance(x, pd.DataFrame):
- raise NotImplementedError(
- f"Currently only pandas.DataFrame can be saved as type {file_type!r}."
- )
-
-
def load_path(filename: str, path_to_version, pin_type=None):
# file path creation ------------------------------------------------------
if pin_type == "table":
@@ -126,7 +120,7 @@ def load_data(
def save_data(
- obj, fname, pin_type=None, apply_suffix: bool = True
+ obj: "Adaptor | Any", fname, pin_type=None, apply_suffix: bool = True
) -> "str | Sequence[str]":
# TODO: extensible saving with deferred importing
# TODO: how to encode arguments to saving / loading drivers?
@@ -135,6 +129,11 @@ def save_data(
# as argument to board, and then type dispatchers for explicit cases
# of saving / loading objects different ways.
+ if isinstance(obj, Adaptor):
+ adaptor, obj = obj, obj._d
+ else:
+ adaptor = create_adaptor(obj)
+
if apply_suffix:
if pin_type == "file":
suffix = "".join(Path(obj).suffixes)
@@ -149,39 +148,22 @@ def save_data(
final_name = f"{fname}{suffix}"
if pin_type == "csv":
- _assert_is_pandas_df(obj, file_type=type)
-
- obj.to_csv(final_name, index=False)
-
+ adaptor.write_csv(final_name)
elif pin_type == "arrow":
# NOTE: R pins accepts the type arrow, and saves it as feather.
# we allow reading this type, but raise an error for writing.
- _assert_is_pandas_df(obj, file_type=type)
-
- obj.to_feather(final_name)
-
+ adaptor.write_feather(final_name)
elif pin_type == "feather":
- _assert_is_pandas_df(obj, file_type=type)
-
- raise NotImplementedError(
+ msg = (
'Saving data as type "feather" no longer supported. Use type "arrow" instead.'
)
-
+ raise NotImplementedError(msg)
elif pin_type == "parquet":
- _assert_is_pandas_df(obj, file_type=type)
-
- obj.to_parquet(final_name)
-
+ adaptor.write_parquet(final_name)
elif pin_type == "joblib":
- import joblib
-
- joblib.dump(obj, final_name)
-
+ adaptor.write_joblib(final_name)
elif pin_type == "json":
- import json
-
- json.dump(obj, open(final_name, "w"))
-
+ adaptor.write_json(final_name)
elif pin_type == "file":
import contextlib
import shutil
@@ -202,14 +184,6 @@ def save_data(
return final_name
-def default_title(obj, name):
- import pandas as pd
-
- if isinstance(obj, pd.DataFrame):
- # TODO(compat): title says CSV rather than data.frame
- # see https://github.com/machow/pins-python/issues/5
- shape_str = " x ".join(map(str, obj.shape))
- return f"{name}: a pinned {shape_str} DataFrame"
- else:
- obj_name = type(obj).__qualname__
- return f"{name}: a pinned {obj_name} object"
+def default_title(obj: Any, name: str) -> str:
+ # Kept for backward compatibility only.
+ return create_adaptor(obj).default_title(name)
diff --git a/pins/tests/test_adaptors.py b/pins/tests/test_adaptors.py
new file mode 100644
index 00000000..d771c062
--- /dev/null
+++ b/pins/tests/test_adaptors.py
@@ -0,0 +1,166 @@
+from pathlib import Path
+
+import joblib
+import pandas as pd
+import pytest
+from pandas.testing import assert_frame_equal
+
+from pins._adaptors import (
+ AbstractPandasFrame,
+ Adaptor,
+ DFAdaptor,
+ PandasAdaptor,
+ create_adaptor,
+)
+
+
+class TestCreateAdaptor:
+ def test_pandas(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = create_adaptor(df)
+ assert isinstance(adaptor, Adaptor)
+ assert isinstance(adaptor, PandasAdaptor)
+
+ def test_non_df(self):
+ adaptor = create_adaptor(42)
+ assert isinstance(adaptor, Adaptor)
+ assert not isinstance(adaptor, PandasAdaptor)
+ assert not isinstance(adaptor, DFAdaptor)
+
+ def test_already_adaptor(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = create_adaptor(df)
+ assert isinstance(adaptor, PandasAdaptor)
+ assert create_adaptor(adaptor) is adaptor
+
+
+class TestAdaptor:
+ def test_write_json(self, tmp_path: Path):
+ data = {"a": 1, "b": 2}
+ adaptor = Adaptor(data)
+ file = tmp_path / "file.json"
+ adaptor.write_json(file)
+ assert file.read_text() == '{"a": 1, "b": 2}'
+
+ def test_write_joblib(self, tmp_path: Path):
+ data = {"a": 1, "b": 2}
+ adaptor = Adaptor(data)
+ file = tmp_path / "file.joblib"
+ adaptor.write_joblib(file)
+
+ # Dump independently and check contents
+ expected_file = tmp_path / "expected.joblib"
+ joblib.dump(data, expected_file)
+ assert expected_file.read_bytes() == file.read_bytes()
+
+ def test_write_csv(self):
+ with pytest.raises(NotImplementedError):
+ adaptor = Adaptor(42)
+ adaptor.write_csv("file.csv")
+
+ def test_write_parquet(self):
+ with pytest.raises(NotImplementedError):
+ adaptor = Adaptor(42)
+ adaptor.write_parquet("file.parquet")
+
+ def test_write_feather(self):
+ with pytest.raises(NotImplementedError):
+ adaptor = Adaptor(42)
+ adaptor.write_feather("file.feather")
+
+ class TestDataPreview:
+ def test_int(self):
+ adaptor = Adaptor(42)
+ assert adaptor.data_preview == "{}"
+
+ def test_dict(self):
+ data = {"a": 1, "b": 2}
+ adaptor = Adaptor(data)
+ assert adaptor.data_preview == "{}"
+
+ def test_default_title(self):
+ adaptor = Adaptor(42)
+ assert adaptor.default_title("my_data") == "my_data: a pinned int object"
+
+
+class TestPandasAdaptor:
+ def test_df_type(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ assert adaptor.df_type == "DataFrame"
+
+ def test_columns(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ assert isinstance(adaptor, DFAdaptor)
+ assert isinstance(adaptor, PandasAdaptor)
+ assert adaptor.columns == ["a", "b"]
+
+ def test_shape(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ assert isinstance(adaptor, DFAdaptor)
+ assert isinstance(adaptor, PandasAdaptor)
+ assert adaptor.shape == (3, 2)
+
+ def test_head(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ head1_df = pd.DataFrame({"a": [1], "b": [4]})
+ expected = create_adaptor(head1_df)
+ assert isinstance(adaptor, DFAdaptor)
+ assert isinstance(adaptor.head(1), DFAdaptor)
+ assert isinstance(adaptor.head(1), PandasAdaptor)
+ assert_frame_equal(adaptor.head(1)._d, expected._d)
+
+ def test_to_json(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ assert isinstance(adaptor, DFAdaptor)
+ assert adaptor.to_json() == """[{"a":1,"b":4},{"a":2,"b":5},{"a":3,"b":6}]"""
+
+ def test_write_csv(self, tmp_path: Path):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ file = tmp_path / "file.csv"
+ adaptor.write_csv(file)
+ assert file.read_text() == "a,b\n1,4\n2,5\n3,6\n"
+
+ def test_write_parquet(self, tmp_path: Path):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ file = tmp_path / "file.parquet"
+ adaptor.write_parquet(file)
+ assert_frame_equal(pd.read_parquet(file), df)
+
+ def test_write_feather(self, tmp_path: Path):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ file = tmp_path / "file.feather"
+ adaptor.write_feather(file)
+ assert_frame_equal(pd.read_feather(file), df)
+
+ def test_data_preview(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ expected = (
+ '{"data": [{"a": 1, "b": 4}, {"a": 2, "b": 5}, {"a": 3, "b": 6}], '
+ '"columns": [{"name": ["a"], "label": ["a"], "align": ["left"], "type": [""]}, '
+ '{"name": ["b"], "label": ["b"], "align": ["left"], "type": [""]}]}'
+ )
+ assert adaptor.data_preview == expected
+
+ def test_default_title(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ adaptor = PandasAdaptor(df)
+ assert adaptor.default_title("my_df") == "my_df: a pinned 3 x 2 DataFrame"
+
+
+class TestAbstractBackends:
+ class TestAbstractPandasFrame:
+ def test_isinstance(self):
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ assert isinstance(df, AbstractPandasFrame)
+
+ def test_not_isinstance(self):
+ assert not isinstance(42, AbstractPandasFrame)
diff --git a/pins/tests/test_drivers.py b/pins/tests/test_drivers.py
index 351550af..5959e028 100644
--- a/pins/tests/test_drivers.py
+++ b/pins/tests/test_drivers.py
@@ -6,6 +6,7 @@
import pandas as pd
import pytest
+from pins._adaptors import create_adaptor
from pins.config import PINS_ENV_INSECURE_READ
from pins.drivers import default_title, load_data, load_path, save_data
from pins.errors import PinsInsecureReadError
@@ -163,6 +164,23 @@ def test_driver_apply_suffix_false(tmp_path: Path):
assert Path(res_fname).name == "some_df"
+class TestSaveData:
+ def test_accepts_pandas_df(self, tmp_path: Path):
+ import pandas as pd
+
+ df = pd.DataFrame({"x": [1, 2, 3]})
+ result = save_data(df, tmp_path / "some_df", "csv")
+ assert Path(result) == tmp_path / "some_df.csv"
+
+ def test_accepts_adaptor(self, tmp_path: Path):
+ import pandas as pd
+
+ df = pd.DataFrame({"x": [1, 2, 3]})
+ adaptor = create_adaptor(df)
+ result = save_data(adaptor, tmp_path / "some_df", "csv")
+ assert Path(result) == tmp_path / "some_df.csv"
+
+
class TestLoadFile:
def test_str_file(self):
class _MockMetaStrFile:
diff --git a/pyproject.toml b/pyproject.toml
index 96b24ad5..a91ca77a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ dependencies = [
"pyyaml>=3.13",
"requests",
"xxhash>=1",
+ "databackend>=0.0.3",
]
[project.optional-dependencies]
@@ -39,6 +40,7 @@ azure = ["adlfs"]
check = [
"pre-commit",
"pyright==1.1.372", # Pinned; manually sync with .github/workflows/code-checks.yml
+ "ruff==0.5.4", # Pinned; manually sync with pre-commit-config.yaml
"types-appdirs",
]
doc = [
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 24473daf..effdf5a3 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -2,26 +2,23 @@
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
-# pip-compile --extra=check --extra=doc --extra=test --output-file=- pyproject.toml
+# pip-compile --extra=doc --extra=test --extra=check --output-file=- --strip-extras pyproject.toml
#
--index-url https://pypi.python.org/simple/
--trusted-host pypi.org
-
-adlfs==2024.12.0
+adlfs==2024.7.0
# via pins (pyproject.toml)
-aiobotocore==2.15.2
+aiobotocore==2.13.1
# via s3fs
-aiohappyeyeballs==2.4.4
- # via aiohttp
-aiohttp==3.11.10
+aiohttp==3.9.5
# via
# adlfs
# aiobotocore
# gcsfs
# s3fs
-aioitertools==0.12.0
+aioitertools==0.11.0
# via aiobotocore
-aiosignal==1.3.2
+aiosignal==1.3.1
# via aiohttp
annotated-types==0.7.0
# via pydantic
@@ -31,68 +28,67 @@ appnope==0.1.4
# via
# ipykernel
# ipython
-asttokens==3.0.0
+asttokens==2.4.1
# via stack-data
-attrs==24.3.0
+attrs==23.2.0
# via
# aiohttp
# jsonschema
# pytest
# referencing
# sphobjinv
-azure-core==1.32.0
+azure-core==1.30.2
# via
# adlfs
# azure-identity
# azure-storage-blob
azure-datalake-store==0.0.53
# via adlfs
-azure-identity==1.19.0
+azure-identity==1.17.1
# via adlfs
-azure-storage-blob==12.24.0
+azure-storage-blob==12.20.0
# via adlfs
backcall==0.2.0
# via ipython
-beartype==0.19.0
+beartype==0.18.5
# via plum-dispatch
-black==24.10.0
- # via quartodoc
-botocore==1.35.36
+botocore==1.34.131
# via aiobotocore
-build==1.2.2.post1
+build==1.2.1
# via pip-tools
-cachetools==5.5.0
+cachetools==5.4.0
# via google-auth
-certifi==2024.12.14
+certifi==2024.7.4
# via
# requests
# sphobjinv
-cffi==1.17.1
+cffi==1.16.0
# via
# azure-datalake-store
# cryptography
cfgv==3.4.0
# via pre-commit
-charset-normalizer==3.4.0
+charset-normalizer==3.3.2
# via requests
click==8.1.7
# via
- # black
# pip-tools
# quartodoc
colorama==0.4.6
# via griffe
comm==0.2.2
# via ipykernel
-cramjam==2.9.1
+cramjam==2.8.3
# via fastparquet
-cryptography==44.0.0
+cryptography==42.0.8
# via
# azure-identity
# azure-storage-blob
# msal
# pyjwt
-debugpy==1.8.11
+databackend==0.0.3
+ # via pins (pyproject.toml)
+debugpy==1.8.2
# via ipykernel
decopatch==1.4.10
# via pytest-cases
@@ -100,34 +96,34 @@ decorator==5.1.1
# via
# gcsfs
# ipython
-distlib==0.3.9
+distlib==0.3.8
# via virtualenv
-executing==2.1.0
+executing==2.0.1
# via stack-data
-fastjsonschema==2.21.1
+fastjsonschema==2.20.0
# via nbformat
-fastparquet==2024.11.0
+fastparquet==2024.5.0
# via pins (pyproject.toml)
-filelock==3.16.1
+filelock==3.15.4
# via virtualenv
-frozenlist==1.5.0
+frozenlist==1.4.1
# via
# aiohttp
# aiosignal
-fsspec==2024.10.0
+fsspec==2024.6.1
# via
+ # pins (pyproject.toml)
# adlfs
# fastparquet
# gcsfs
- # pins (pyproject.toml)
# s3fs
-gcsfs==2024.10.0
+gcsfs==2024.6.1
# via pins (pyproject.toml)
-google-api-core==2.24.0
+google-api-core==2.19.1
# via
# google-cloud-core
# google-cloud-storage
-google-auth==2.37.0
+google-auth==2.32.0
# via
# gcsfs
# google-api-core
@@ -138,31 +134,31 @@ google-auth-oauthlib==1.2.1
# via gcsfs
google-cloud-core==2.4.1
# via google-cloud-storage
-google-cloud-storage==2.19.0
+google-cloud-storage==2.17.0
# via gcsfs
-google-crc32c==1.6.0
+google-crc32c==1.5.0
# via
# google-cloud-storage
# google-resumable-media
-google-resumable-media==2.7.2
+google-resumable-media==2.7.1
# via google-cloud-storage
-googleapis-common-protos==1.66.0
+googleapis-common-protos==1.63.2
# via google-api-core
-griffe==1.5.1
+griffe==0.48.0
# via quartodoc
-humanize==4.11.0
+humanize==4.10.0
# via pins (pyproject.toml)
-identify==2.6.3
+identify==2.6.0
# via pre-commit
-idna==3.10
+idna==3.7
# via
# requests
# yarl
-importlib-metadata==8.5.0
+importlib-metadata==8.0.0
# via
# pins (pyproject.toml)
# quartodoc
-importlib-resources==6.4.5
+importlib-resources==6.4.0
# via
# pins (pyproject.toml)
# quartodoc
@@ -172,11 +168,11 @@ ipykernel==6.29.5
# via pins (pyproject.toml)
ipython==8.12.0
# via
- # ipykernel
# pins (pyproject.toml)
-isodate==0.7.2
+ # ipykernel
+isodate==0.6.1
# via azure-storage-blob
-jedi==0.19.2
+jedi==0.19.1
# via ipython
jinja2==3.1.4
# via pins (pyproject.toml)
@@ -188,9 +184,9 @@ jsonschema==4.23.0
# via
# nbformat
# sphobjinv
-jsonschema-specifications==2024.10.1
+jsonschema-specifications==2023.12.1
# via jsonschema
-jupyter-client==8.6.3
+jupyter-client==8.6.2
# via
# ipykernel
# nbclient
@@ -200,13 +196,13 @@ jupyter-core==5.7.2
# jupyter-client
# nbclient
# nbformat
-makefun==1.15.6
+makefun==1.15.4
# via
# decopatch
# pytest-cases
markdown-it-py==3.0.0
# via rich
-markupsafe==3.0.2
+markupsafe==2.1.5
# via jinja2
matplotlib-inline==0.1.7
# via
@@ -214,116 +210,110 @@ matplotlib-inline==0.1.7
# ipython
mdurl==0.1.2
# via markdown-it-py
-msal==1.31.1
+msal==1.29.0
# via
# azure-datalake-store
# azure-identity
# msal-extensions
msal-extensions==1.2.0
# via azure-identity
-multidict==6.1.0
+multidict==6.0.5
# via
# aiohttp
# yarl
-mypy-extensions==1.0.0
- # via black
-nbclient==0.10.1
+nbclient==0.10.0
# via pins (pyproject.toml)
nbformat==5.10.4
# via
- # nbclient
# pins (pyproject.toml)
+ # nbclient
nest-asyncio==1.6.0
# via ipykernel
nodeenv==1.9.1
# via
# pre-commit
# pyright
-numpy==2.2.0
+numpy==2.0.0
# via
# fastparquet
# pandas
+ # pyarrow
+ # rdata
+ # xarray
oauthlib==3.2.2
# via requests-oauthlib
-packaging==24.2
+packaging==24.1
# via
- # black
# build
# fastparquet
# ipykernel
# pytest
# pytest-cases
-pandas==2.2.3
+ # xarray
+pandas==2.2.2
# via
- # fastparquet
# pins (pyproject.toml)
+ # fastparquet
+ # rdata
+ # xarray
parso==0.8.4
# via jedi
-pathspec==0.12.1
- # via black
pexpect==4.9.0
# via ipython
pickleshare==0.7.5
# via ipython
pip-tools==7.4.1
# via pins (pyproject.toml)
-platformdirs==4.3.6
+platformdirs==4.2.2
# via
- # black
# jupyter-core
# virtualenv
pluggy==1.5.0
# via pytest
-plum-dispatch==2.5.4
+plum-dispatch==2.5.1.post1
# via quartodoc
portalocker==2.10.1
# via msal-extensions
-pre-commit==4.0.1
+pre-commit==3.7.1
# via pins (pyproject.toml)
-prompt-toolkit==3.0.48
+prompt-toolkit==3.0.47
# via ipython
-propcache==0.2.1
- # via
- # aiohttp
- # yarl
-proto-plus==1.25.0
+proto-plus==1.24.0
# via google-api-core
-protobuf==5.29.1
+protobuf==5.27.2
# via
# google-api-core
# googleapis-common-protos
# proto-plus
-psutil==6.1.0
+psutil==6.0.0
# via ipykernel
ptyprocess==0.7.0
# via pexpect
-pure-eval==0.2.3
+pure-eval==0.2.2
# via stack-data
py==1.11.0
# via pytest
-pyarrow==18.1.0
+pyarrow==16.1.0
# via pins (pyproject.toml)
-pyasn1==0.6.1
+pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
-pyasn1-modules==0.4.1
+pyasn1-modules==0.4.0
# via google-auth
pycparser==2.22
# via cffi
-pydantic==2.10.3
+pydantic==2.8.2
# via quartodoc
-pydantic-core==2.27.1
+pydantic-core==2.20.1
# via pydantic
pygments==2.18.0
# via
# ipython
# rich
-pyjwt[crypto]==2.10.1
- # via
- # msal
- # pyjwt
-pyproject-hooks==1.2.0
+pyjwt==2.8.0
+ # via msal
+pyproject-hooks==1.1.0
# via
# build
# pip-tools
@@ -334,7 +324,7 @@ pytest==7.1.3
# pins (pyproject.toml)
# pytest-dotenv
# pytest-parallel
-pytest-cases==3.8.6
+pytest-cases==3.8.5
# via pins (pyproject.toml)
pytest-dotenv==0.5.2
# via pins (pyproject.toml)
@@ -347,18 +337,18 @@ python-dateutil==2.9.0.post0
# pandas
python-dotenv==1.0.1
# via pytest-dotenv
-pytz==2024.2
+pytz==2024.1
# via pandas
-pyyaml==6.0.2
+pyyaml==6.0.1
# via
# pins (pyproject.toml)
# pre-commit
# quartodoc
-pyzmq==26.2.0
+pyzmq==26.0.3
# via
# ipykernel
# jupyter-client
-quartodoc==0.9.1
+quartodoc==0.7.5
# via pins (pyproject.toml)
rdata==0.11.2
# via pins (pyproject.toml)
@@ -368,30 +358,32 @@ referencing==0.35.1
# jsonschema-specifications
requests==2.32.3
# via
+ # pins (pyproject.toml)
# azure-core
# azure-datalake-store
# gcsfs
# google-api-core
# google-cloud-storage
# msal
- # pins (pyproject.toml)
# quartodoc
# requests-oauthlib
requests-oauthlib==2.0.0
# via google-auth-oauthlib
-rich==13.9.4
+rich==13.7.1
# via plum-dispatch
-rpds-py==0.22.3
+rpds-py==0.19.0
# via
# jsonschema
# referencing
rsa==4.9
# via google-auth
-s3fs==2024.10.0
+s3fs==2024.6.1
# via pins (pyproject.toml)
-six==1.17.0
+six==1.16.0
# via
+ # asttokens
# azure-core
+ # isodate
# python-dateutil
sphobjinv==2.3.1.1
# via quartodoc
@@ -401,9 +393,9 @@ tabulate==0.9.0
# via quartodoc
tblib==3.0.0
# via pytest-parallel
-tomli==2.2.1
+tomli==2.0.1
# via pytest
-tornado==6.4.2
+tornado==6.4.1
# via
# ipykernel
# jupyter-client
@@ -424,33 +416,33 @@ typing-extensions==4.12.2
# azure-core
# azure-identity
# azure-storage-blob
- # plum-dispatch
# pydantic
# pydantic-core
# quartodoc
-tzdata==2024.2
+ # rdata
+tzdata==2024.1
# via pandas
-urllib3==2.2.3
+urllib3==2.2.2
# via
# botocore
# requests
-virtualenv==20.28.0
+virtualenv==20.26.3
# via pre-commit
-watchdog==6.0.0
+watchdog==4.0.1
# via quartodoc
wcwidth==0.2.13
# via prompt-toolkit
-wheel==0.45.1
+wheel==0.43.0
# via pip-tools
-wrapt==1.17.0
+wrapt==1.16.0
# via aiobotocore
-xarray==2024.6.0
+xarray==2024.11.0
# via rdata
-xxhash==3.5.0
+xxhash==3.4.1
# via pins (pyproject.toml)
-yarl==1.18.3
+yarl==1.9.4
# via aiohttp
-zipp==3.21.0
+zipp==3.19.2
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/minimum.txt b/requirements/minimum.txt
index 325990dc..73c3f3f3 100644
--- a/requirements/minimum.txt
+++ b/requirements/minimum.txt
@@ -7,3 +7,4 @@ importlib-metadata==4.4
importlib-resources==1.3
appdirs<2.0.0
humanize==1.0.0
+databackend==0.0.3