shackleton 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ name: Python Test
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ push:
8
+ branches:
9
+ - main
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - uses: actions/setup-python@v4
17
+ with:
18
+ python-version: '3.10'
19
+ - name: Install dependencies
20
+ run: |
21
+ python -m pip install --upgrade pip
22
+ pip install -e .[test]
23
+ - name: Test
24
+ run: |
25
+ branb test
26
+ - uses: codecov/codecov-action@v3
27
+ with:
28
+ fail_ci_if_error: true
@@ -0,0 +1,30 @@
1
+
2
+ name: Python Package Release
3
+
4
+ on:
5
+ release:
6
+ types: [published]
7
+ branches:
8
+ - main
9
+
10
+ jobs:
11
+ release:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v4
17
+ with:
18
+ python-version: '3.x'
19
+ - name: Install dependencies
20
+ run: |
21
+ python -m pip install --upgrade pip
22
+ python -m pip install flit
23
+ - name: Release
24
+ env:
25
+ FLIT_PASSWORD: ${{ secrets.PYPI_TOKEN }}
26
+ FLIT_USERNAME: '__token__'
27
+ run: |
28
+ flit publish
29
+
30
+
@@ -0,0 +1,130 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # celery beat schedule file
95
+ celerybeat-schedule
96
+
97
+ # SageMath parsed files
98
+ *.sage.py
99
+
100
+ # Environments
101
+ .env
102
+ .venv
103
+ env/
104
+ venv/
105
+ ENV/
106
+ env.bak/
107
+ venv.bak/
108
+
109
+ # Spyder project settings
110
+ .spyderproject
111
+ .spyproject
112
+
113
+ # Rope project settings
114
+ .ropeproject
115
+
116
+ # mkdocs documentation
117
+ /site
118
+
119
+ # mypy
120
+ .mypy_cache/
121
+ .dmypy.json
122
+ dmypy.json
123
+
124
+ # Pyre type checker
125
+ .pyre/
126
+
127
+ .idea
128
+ .vscode
129
+ dask-worker-space/
130
+ draft*.ipynb
@@ -0,0 +1,7 @@
1
+ Copyright 2023 Endre Márk Borza
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.1
2
+ Name: shackleton
3
+ Version: 0.0.1
4
+ Summary: Polars based database
5
+ Author-email: Endre Márk Borza <endremborza@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: polars
9
+ Requires-Dist: branthebuilder[doc] ; extra == "doc"
10
+ Requires-Dist: branthebuilder ; extra == "test"
11
+ Project-URL: Homepage, https://github.com/endremborza/shackleton
12
+ Provides-Extra: doc
13
+ Provides-Extra: test
14
+
15
+ # shackleton
16
+
17
+ [![Documentation Status](https://readthedocs.org/projects/shackleton/badge/?version=latest)](https://shackleton.readthedocs.io/en/latest)
18
+ [![codeclimate](https://img.shields.io/codeclimate/maintainability/endremborza/shackleton.svg)](https://codeclimate.com/github/endremborza/shackleton)
19
+ [![codecov](https://img.shields.io/codecov/c/github/endremborza/shackleton)](https://codecov.io/gh/endremborza/shackleton)
20
+ [![pypi](https://img.shields.io/pypi/v/shackleton.svg)](https://pypi.org/project/shackleton/)
21
+
22
+
@@ -0,0 +1,7 @@
1
+ # shackleton
2
+
3
+ [![Documentation Status](https://readthedocs.org/projects/shackleton/badge/?version=latest)](https://shackleton.readthedocs.io/en/latest)
4
+ [![codeclimate](https://img.shields.io/codeclimate/maintainability/endremborza/shackleton.svg)](https://codeclimate.com/github/endremborza/shackleton)
5
+ [![codecov](https://img.shields.io/codecov/c/github/endremborza/shackleton)](https://codecov.io/gh/endremborza/shackleton)
6
+ [![pypi](https://img.shields.io/pypi/v/shackleton.svg)](https://pypi.org/project/shackleton/)
7
+
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "shackleton"
3
+ dynamic = ["version", "description"]
4
+ authors = [{name = "Endre Márk Borza", email = "endremborza@gmail.com"}]
5
+ license = {file = "LICENSE"}
6
+ readme = "README.md"
7
+ requires-python = ">=3.10"
8
+ dependencies = ["polars"]
9
+
10
+ [project.optional-dependencies]
11
+ test = ["branthebuilder"]
12
+ doc = ["branthebuilder[doc]"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/endremborza/shackleton"
16
+
17
+ [build-system]
18
+ requires = ["flit_core >=3.2,<4"]
19
+ build-backend = "flit_core.buildapi"
20
+
21
+ [tool.branb]
22
+ line-length = 88
@@ -0,0 +1,4 @@
1
+ """Polars based database"""
2
+ from .core import TableShack # noqa: F401
3
+
4
+ __version__ = "0.0.1"
@@ -0,0 +1,116 @@
1
+ from dataclasses import dataclass, field
2
+ from functools import cached_property, partial
3
+ from pathlib import Path
4
+ from typing import Any, Callable, Iterable
5
+
6
+ import polars as pl
7
+
8
+ FILE_NAME = "data"
9
+
10
+
11
+ @dataclass
12
+ class TableShack:
13
+ root_path: Path
14
+ id_col: str | None = None
15
+ partition_cols: list[str] = field(default_factory=list)
16
+ ipc: bool = False
17
+ compression: str | None = None
18
+
19
+ @cached_property
20
+ def extension(self):
21
+ return ".arrow" if self.ipc else ".parquet"
22
+
23
+ def extend(self, df: pl.DataFrame):
24
+ return self._write_meta(df, self._extend)
25
+
26
+ def replace_records(self, df: pl.DataFrame):
27
+ return self._write_meta(df, self._replace_records)
28
+
29
+ def lazy_read(self, path: Path) -> pl.LazyFrame:
30
+ o = (pl.scan_ipc if self.ipc else pl.scan_parquet)(path)
31
+ if self.id_col:
32
+ return o.set_sorted(self.id_col)
33
+ return o
34
+
35
+ def replace_all(self, df: pl.DataFrame):
36
+ """purges everything and writes df instead"""
37
+ self.purge()
38
+ self.extend(df)
39
+
40
+ def purge(self):
41
+ """purges everything"""
42
+ for p in self.paths:
43
+ p.unlink()
44
+
45
+ def get_full_lf(self) -> pl.LazyFrame:
46
+ return pl.concat(map(self.lazy_read, self.paths))
47
+
48
+ def get_full_df(self) -> pl.DataFrame:
49
+ return self.get_full_lf().collect()
50
+
51
+ def get_partition_paths(self, partitions: dict[str, Any]) -> Iterable[Path]:
52
+ g = "/".join(f"{c}={partitions.get(c, '*')}" for c in self.partition_cols)
53
+ return self.root_path.glob(f"{g}/*{self.extension}")
54
+
55
+ def get_partition_lf(self, partitions: dict[str, Any]) -> pl.LazyFrame:
56
+ return pl.concat(map(self.lazy_read, self.get_partition_paths(partitions)))
57
+
58
+ def get_partition_df(self, partitions: dict[str, Any]) -> pl.DataFrame:
59
+ return self.get_partition_lf(partitions).collect()
60
+
61
+ @property
62
+ def paths(self) -> Iterable[Path]:
63
+ return self.root_path.glob("**/*" + self.extension)
64
+
65
+ @property
66
+ def lfs(self) -> Iterable[pl.LazyFrame]:
67
+ return map(self.lazy_read, self.paths)
68
+
69
+ @property
70
+ def dfs(self) -> Iterable[pl.DataFrame]:
71
+ return map(pl.LazyFrame.collect, self.lfs)
72
+
73
+ def _extend(self, df: pl.LazyFrame, old_df: pl.LazyFrame) -> pl.LazyFrame:
74
+ if self.id_col:
75
+ return old_df.merge_sorted(df.sort(self.id_col), key=self.id_col)
76
+ else:
77
+ return pl.concat([old_df, df])
78
+
79
+ def _replace_records(self, df: pl.LazyFrame, old_df: pl.LazyFrame) -> pl.LazyFrame:
80
+ return old_df.merge_sorted(df, key=self.id_col).unique(
81
+ subset=self.id_col, keep="last"
82
+ )
83
+
84
+ def _write_meta(
85
+ self,
86
+ df: pl.DataFrame,
87
+ fun: Callable[[pl.LazyFrame, pl.LazyFrame], pl.LazyFrame],
88
+ subdirs: tuple[str] = (),
89
+ partitioned=False,
90
+ ):
91
+ if self.partition_cols and not partitioned:
92
+ return self._gb_handle(df, fun)
93
+
94
+ true_path = self._get_df_path(subdirs)
95
+ true_path.parent.mkdir(exist_ok=True, parents=True)
96
+
97
+ if not true_path.exists():
98
+ out = df.sort(self.id_col) if self.id_col else df
99
+ else:
100
+ out = fun(df.lazy(), self.lazy_read(true_path)).collect()
101
+
102
+ fun = pl.DataFrame.write_ipc if self.ipc else pl.DataFrame.write_parquet
103
+ self._add_compression(fun)(out, true_path)
104
+
105
+ def _gb_handle(self, df: pl.DataFrame, fun):
106
+ for gid, gdf in df.groupby(self.partition_cols):
107
+ hive_names = [f"{k}={v}" for k, v in zip(self.partition_cols, gid)]
108
+ self._write_meta(gdf.drop(self.partition_cols), fun, hive_names, True)
109
+
110
+ def _add_compression(self, fun):
111
+ if self.compression:
112
+ return partial(fun, compression=self.compression)
113
+ return fun
114
+
115
+ def _get_df_path(self, subdirs: tuple = ()):
116
+ return Path(self.root_path, *subdirs, FILE_NAME).with_suffix(self.extension)
@@ -0,0 +1,40 @@
1
+ import random
2
+ from base64 import b16encode
3
+
4
+ import polars as pl
5
+ import pytest
6
+
7
+ from shackleton import TableShack
8
+
9
+
10
+ @pytest.mark.parametrize(["id_col", "comp"], [("A", None), (None, "gzip")])
11
+ def test_import(tmp_path, id_col, comp):
12
+ shack = TableShack(
13
+ tmp_path / "data", id_col=id_col, partition_cols=["B", "C"], compression=comp
14
+ )
15
+ n = 2_000
16
+ rng = random.Random(426)
17
+ byte_set = [b16encode(rng.randbytes(4)) for _ in range(5)]
18
+ df = pl.DataFrame(
19
+ {
20
+ "A": [rng.random() for _ in range(n)],
21
+ "B": [rng.randint(0, 20) for _ in range(n)],
22
+ "C": [rng.choice(byte_set) for _ in range(n)],
23
+ }
24
+ )
25
+ shack.extend(df)
26
+ assert shack.get_full_df().shape[0] == n
27
+
28
+ assert len(list(shack.get_partition_paths({"B": 4}))) == 5
29
+ assert len(list(shack.get_partition_paths({"C": byte_set[0]}))) == 21
30
+ assert shack.get_partition_df({"B": 4}).shape[0] == 97
31
+ shack.extend(df)
32
+ assert shack.get_full_df().shape[0] == df.shape[0] * 2
33
+
34
+ shack.replace_all(df)
35
+ assert shack.get_full_df().shape[0] == df.shape[0]
36
+ if id_col:
37
+ shack.replace_records(df)
38
+ assert shack.get_full_df().shape[0] == df.shape[0]
39
+
40
+ assert next(shack.dfs).shape[0]