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.
- shackleton-0.0.1/.github/workflows/test.yml +28 -0
- shackleton-0.0.1/.github/workflows/twine_release.yml +30 -0
- shackleton-0.0.1/.gitignore +130 -0
- shackleton-0.0.1/LICENSE +7 -0
- shackleton-0.0.1/PKG-INFO +22 -0
- shackleton-0.0.1/README.md +7 -0
- shackleton-0.0.1/pyproject.toml +22 -0
- shackleton-0.0.1/shackleton/__init__.py +4 -0
- shackleton-0.0.1/shackleton/core.py +116 -0
- shackleton-0.0.1/shackleton/tests/test_core.py +40 -0
|
@@ -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
|
shackleton-0.0.1/LICENSE
ADDED
|
@@ -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
|
+
[](https://shackleton.readthedocs.io/en/latest)
|
|
18
|
+
[](https://codeclimate.com/github/endremborza/shackleton)
|
|
19
|
+
[](https://codecov.io/gh/endremborza/shackleton)
|
|
20
|
+
[](https://pypi.org/project/shackleton/)
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# shackleton
|
|
2
|
+
|
|
3
|
+
[](https://shackleton.readthedocs.io/en/latest)
|
|
4
|
+
[](https://codeclimate.com/github/endremborza/shackleton)
|
|
5
|
+
[](https://codecov.io/gh/endremborza/shackleton)
|
|
6
|
+
[](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,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]
|