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