bleanser 0.1.20250119__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.
- bleanser-0.1.20250119/.ci/release +66 -0
- bleanser-0.1.20250119/.ci/release-uv +60 -0
- bleanser-0.1.20250119/.ci/run +37 -0
- bleanser-0.1.20250119/.github/workflows/main.yml +99 -0
- bleanser-0.1.20250119/.gitignore +160 -0
- bleanser-0.1.20250119/LICENSE +21 -0
- bleanser-0.1.20250119/PKG-INFO +45 -0
- bleanser-0.1.20250119/README.md +197 -0
- bleanser-0.1.20250119/conftest.py +47 -0
- bleanser-0.1.20250119/doall +36 -0
- bleanser-0.1.20250119/doc/options.md +38 -0
- bleanser-0.1.20250119/mypy.ini +17 -0
- bleanser-0.1.20250119/old/README.md +1 -0
- bleanser-0.1.20250119/old/common.py +16 -0
- bleanser-0.1.20250119/old/foursquare.py +65 -0
- bleanser-0.1.20250119/old/jq_normaliser.py +391 -0
- bleanser-0.1.20250119/old/json.py +239 -0
- bleanser-0.1.20250119/old/json2.py +169 -0
- bleanser-0.1.20250119/old/reddit.py +283 -0
- bleanser-0.1.20250119/pyproject.toml +76 -0
- bleanser-0.1.20250119/pytest.ini +13 -0
- bleanser-0.1.20250119/ruff.toml +146 -0
- bleanser-0.1.20250119/scripts/apt.sh +10 -0
- bleanser-0.1.20250119/setup.cfg +4 -0
- bleanser-0.1.20250119/src/bleanser/__init__.py +0 -0
- bleanser-0.1.20250119/src/bleanser/__init__.pyi +0 -0
- bleanser-0.1.20250119/src/bleanser/core/__init__.py +5 -0
- bleanser-0.1.20250119/src/bleanser/core/__main__.py +12 -0
- bleanser-0.1.20250119/src/bleanser/core/common.py +138 -0
- bleanser-0.1.20250119/src/bleanser/core/compat.py +6 -0
- bleanser-0.1.20250119/src/bleanser/core/ext/__init__.py +0 -0
- bleanser-0.1.20250119/src/bleanser/core/ext/dummy_executor.py +31 -0
- bleanser-0.1.20250119/src/bleanser/core/ext/logging.py +97 -0
- bleanser-0.1.20250119/src/bleanser/core/ext/sqlite_dumben.py +246 -0
- bleanser-0.1.20250119/src/bleanser/core/main.py +160 -0
- bleanser-0.1.20250119/src/bleanser/core/modules/__init__.py +0 -0
- bleanser-0.1.20250119/src/bleanser/core/modules/extract.py +60 -0
- bleanser-0.1.20250119/src/bleanser/core/modules/json.py +110 -0
- bleanser-0.1.20250119/src/bleanser/core/modules/sqlite.py +486 -0
- bleanser-0.1.20250119/src/bleanser/core/modules/tests/sqlite.py +103 -0
- bleanser-0.1.20250119/src/bleanser/core/modules/xml.py +173 -0
- bleanser-0.1.20250119/src/bleanser/core/processor.py +1311 -0
- bleanser-0.1.20250119/src/bleanser/core/sqlite.py +12 -0
- bleanser-0.1.20250119/src/bleanser/core/utils.py +128 -0
- bleanser-0.1.20250119/src/bleanser/modules/__init__.pyi +0 -0
- bleanser-0.1.20250119/src/bleanser/modules/antennapod_android.py +54 -0
- bleanser-0.1.20250119/src/bleanser/modules/binary.py +16 -0
- bleanser-0.1.20250119/src/bleanser/modules/bluemaestro.py +87 -0
- bleanser-0.1.20250119/src/bleanser/modules/bumble_android.py +67 -0
- bleanser-0.1.20250119/src/bleanser/modules/chrome.py +59 -0
- bleanser-0.1.20250119/src/bleanser/modules/fbmessenger_android.py +247 -0
- bleanser-0.1.20250119/src/bleanser/modules/firefox.py +241 -0
- bleanser-0.1.20250119/src/bleanser/modules/foursquare.py +106 -0
- bleanser-0.1.20250119/src/bleanser/modules/ghexport.py +85 -0
- bleanser-0.1.20250119/src/bleanser/modules/goodreads.py +21 -0
- bleanser-0.1.20250119/src/bleanser/modules/hinge_android.py +103 -0
- bleanser-0.1.20250119/src/bleanser/modules/hpi/fbmessenger_android.py +43 -0
- bleanser-0.1.20250119/src/bleanser/modules/hpi/twitter_android.py +42 -0
- bleanser-0.1.20250119/src/bleanser/modules/hpi/whatsapp_android.py +40 -0
- bleanser-0.1.20250119/src/bleanser/modules/instagram_android.py +181 -0
- bleanser-0.1.20250119/src/bleanser/modules/json_new.py +9 -0
- bleanser-0.1.20250119/src/bleanser/modules/kobo.py +95 -0
- bleanser-0.1.20250119/src/bleanser/modules/lastfm.py +70 -0
- bleanser-0.1.20250119/src/bleanser/modules/monzo.py +46 -0
- bleanser-0.1.20250119/src/bleanser/modules/pinboard.py +78 -0
- bleanser-0.1.20250119/src/bleanser/modules/pocket.py +14 -0
- bleanser-0.1.20250119/src/bleanser/modules/podcastaddict_android.py +125 -0
- bleanser-0.1.20250119/src/bleanser/modules/reddit.py +288 -0
- bleanser-0.1.20250119/src/bleanser/modules/rescuetime.py +39 -0
- bleanser-0.1.20250119/src/bleanser/modules/rescuetime_android.py +24 -0
- bleanser-0.1.20250119/src/bleanser/modules/skype_android.py +44 -0
- bleanser-0.1.20250119/src/bleanser/modules/sleepasandroid_android.py +24 -0
- bleanser-0.1.20250119/src/bleanser/modules/smscalls.py +84 -0
- bleanser-0.1.20250119/src/bleanser/modules/spotify.py +56 -0
- bleanser-0.1.20250119/src/bleanser/modules/spotifyexport.py +22 -0
- bleanser-0.1.20250119/src/bleanser/modules/stackexchange.py +52 -0
- bleanser-0.1.20250119/src/bleanser/modules/talon_android.py +23 -0
- bleanser-0.1.20250119/src/bleanser/modules/tiktok_android.py +28 -0
- bleanser-0.1.20250119/src/bleanser/modules/tinder_android.py +116 -0
- bleanser-0.1.20250119/src/bleanser/modules/twitter_android.py +144 -0
- bleanser-0.1.20250119/src/bleanser/modules/vk_android.py +167 -0
- bleanser-0.1.20250119/src/bleanser/modules/whatsapp_android.py +268 -0
- bleanser-0.1.20250119/src/bleanser/modules/xml_clean.py +9 -0
- bleanser-0.1.20250119/src/bleanser/py.typed +0 -0
- bleanser-0.1.20250119/src/bleanser/tests/common.py +69 -0
- bleanser-0.1.20250119/src/bleanser/tests/test_binary.py +45 -0
- bleanser-0.1.20250119/src/bleanser/tests/test_hypothesis.py +53 -0
- bleanser-0.1.20250119/src/bleanser.egg-info/PKG-INFO +45 -0
- bleanser-0.1.20250119/src/bleanser.egg-info/SOURCES.txt +91 -0
- bleanser-0.1.20250119/src/bleanser.egg-info/dependency_links.txt +1 -0
- bleanser-0.1.20250119/src/bleanser.egg-info/requires.txt +21 -0
- bleanser-0.1.20250119/src/bleanser.egg-info/top_level.txt +1 -0
- bleanser-0.1.20250119/tox.ini +67 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
'''
|
|
3
|
+
Run [[file:scripts/release][scripts/release]] to deploy Python package onto [[https://pypi.org][PyPi]] and [[https://test.pypi.org][test PyPi]].
|
|
4
|
+
|
|
5
|
+
The script expects =TWINE_PASSWORD= environment variable to contain the [[https://pypi.org/help/#apitoken][PyPi token]] (not the password!).
|
|
6
|
+
|
|
7
|
+
The script can be run manually.
|
|
8
|
+
It's also running as =pypi= job in [[file:.github/workflows/main.yml][Github Actions config]]. Packages are deployed on:
|
|
9
|
+
- every master commit, onto test pypi
|
|
10
|
+
- every new tag, onto production pypi
|
|
11
|
+
|
|
12
|
+
You'll need to set =TWINE_PASSWORD= and =TWINE_PASSWORD_TEST= in [[https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets][secrets]]
|
|
13
|
+
for Github Actions deployment to work.
|
|
14
|
+
'''
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from subprocess import check_call
|
|
20
|
+
import shutil
|
|
21
|
+
|
|
22
|
+
is_ci = os.environ.get('CI') is not None
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
import argparse
|
|
26
|
+
p = argparse.ArgumentParser()
|
|
27
|
+
p.add_argument('--test', action='store_true', help='use test pypi')
|
|
28
|
+
args = p.parse_args()
|
|
29
|
+
|
|
30
|
+
extra = []
|
|
31
|
+
if args.test:
|
|
32
|
+
extra.extend(['--repository', 'testpypi'])
|
|
33
|
+
|
|
34
|
+
root = Path(__file__).absolute().parent.parent
|
|
35
|
+
os.chdir(root) # just in case
|
|
36
|
+
|
|
37
|
+
if is_ci:
|
|
38
|
+
# see https://github.com/actions/checkout/issues/217
|
|
39
|
+
check_call('git fetch --prune --unshallow'.split())
|
|
40
|
+
|
|
41
|
+
dist = root / 'dist'
|
|
42
|
+
if dist.exists():
|
|
43
|
+
shutil.rmtree(dist)
|
|
44
|
+
|
|
45
|
+
check_call(['python3', '-m', 'build'])
|
|
46
|
+
|
|
47
|
+
TP = 'TWINE_PASSWORD'
|
|
48
|
+
password = os.environ.get(TP)
|
|
49
|
+
if password is None:
|
|
50
|
+
print(f"WARNING: no {TP} passed", file=sys.stderr)
|
|
51
|
+
import pip_secrets
|
|
52
|
+
password = pip_secrets.token_test if args.test else pip_secrets.token # meh
|
|
53
|
+
|
|
54
|
+
check_call([
|
|
55
|
+
'python3', '-m', 'twine',
|
|
56
|
+
'upload', *dist.iterdir(),
|
|
57
|
+
*extra,
|
|
58
|
+
], env={
|
|
59
|
+
'TWINE_USERNAME': '__token__',
|
|
60
|
+
TP: password,
|
|
61
|
+
**os.environ,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
'''
|
|
3
|
+
Deploys Python package onto [[https://pypi.org][PyPi]] or [[https://test.pypi.org][test PyPi]].
|
|
4
|
+
|
|
5
|
+
- running manually
|
|
6
|
+
|
|
7
|
+
You'll need =UV_PUBLISH_TOKEN= env variable
|
|
8
|
+
|
|
9
|
+
- running on Github Actions
|
|
10
|
+
|
|
11
|
+
Instead of env variable, relies on configuring github as Trusted publisher (https://docs.pypi.org/trusted-publishers/) -- both for test and regular pypi
|
|
12
|
+
|
|
13
|
+
It's running as =pypi= job in [[file:.github/workflows/main.yml][Github Actions config]].
|
|
14
|
+
Packages are deployed on:
|
|
15
|
+
- every master commit, onto test pypi
|
|
16
|
+
- every new tag, onto production pypi
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
UV_PUBLISH_TOKEN = 'UV_PUBLISH_TOKEN'
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from subprocess import check_call
|
|
26
|
+
|
|
27
|
+
is_ci = os.environ.get('CI') is not None
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
p = argparse.ArgumentParser()
|
|
31
|
+
p.add_argument('--use-test-pypi', action='store_true')
|
|
32
|
+
args = p.parse_args()
|
|
33
|
+
|
|
34
|
+
publish_url = ['--publish-url', 'https://test.pypi.org/legacy/'] if args.use_test_pypi else []
|
|
35
|
+
|
|
36
|
+
root = Path(__file__).absolute().parent.parent
|
|
37
|
+
os.chdir(root) # just in case
|
|
38
|
+
|
|
39
|
+
if is_ci:
|
|
40
|
+
# see https://github.com/actions/checkout/issues/217
|
|
41
|
+
check_call('git fetch --prune --unshallow'.split())
|
|
42
|
+
|
|
43
|
+
# TODO ok, for now uv won't remove dist dir if it already exists
|
|
44
|
+
# https://github.com/astral-sh/uv/issues/10293
|
|
45
|
+
dist = root / 'dist'
|
|
46
|
+
if dist.exists():
|
|
47
|
+
shutil.rmtree(dist)
|
|
48
|
+
|
|
49
|
+
# todo what is --force-pep517?
|
|
50
|
+
check_call(['uv', 'build'])
|
|
51
|
+
|
|
52
|
+
if not is_ci:
|
|
53
|
+
# CI relies on trusted publishers so doesn't need env variable
|
|
54
|
+
assert UV_PUBLISH_TOKEN in os.environ, f'no {UV_PUBLISH_TOKEN} passed'
|
|
55
|
+
|
|
56
|
+
check_call(['uv', 'publish', *publish_url])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
cd "$(dirname "$0")"
|
|
5
|
+
cd .. # git root
|
|
6
|
+
|
|
7
|
+
if ! command -v sudo; then
|
|
8
|
+
# CI or Docker sometimes doesn't have it, so useful to have a dummy
|
|
9
|
+
function sudo {
|
|
10
|
+
"$@"
|
|
11
|
+
}
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# --parallel-live to show outputs while it's running
|
|
15
|
+
tox_cmd='run-parallel --parallel-live'
|
|
16
|
+
if [ -n "${CI-}" ]; then
|
|
17
|
+
# install OS specific stuff here
|
|
18
|
+
case "$OSTYPE" in
|
|
19
|
+
darwin*)
|
|
20
|
+
# macos
|
|
21
|
+
brew install libmagic # for python-magic
|
|
22
|
+
brew install diffutils # for GNU diff
|
|
23
|
+
;;
|
|
24
|
+
cygwin* | msys* | win*)
|
|
25
|
+
# windows
|
|
26
|
+
# ugh. parallel stuff seems super flaky under windows, some random failures, "file used by other process" and crap like that
|
|
27
|
+
tox_cmd='run'
|
|
28
|
+
;;
|
|
29
|
+
*)
|
|
30
|
+
# must be linux?
|
|
31
|
+
:
|
|
32
|
+
;;
|
|
33
|
+
esac
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# NOTE: expects uv installed
|
|
37
|
+
uv tool run --with tox-uv tox $tox_cmd "$@"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# see https://github.com/karlicoss/pymplate for up-to-date reference
|
|
2
|
+
|
|
3
|
+
name: CI
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: '*'
|
|
7
|
+
tags: 'v[0-9]+.*' # only trigger on 'release' tags for PyPi
|
|
8
|
+
# Ideally I would put this in the pypi job... but github syntax doesn't allow for regexes there :shrug:
|
|
9
|
+
# P.S. fuck made up yaml DSLs.
|
|
10
|
+
pull_request: # needed to trigger on others' PRs
|
|
11
|
+
# Note that people who fork it need to go to "Actions" tab on their fork and click "I understand my workflows, go ahead and enable them".
|
|
12
|
+
workflow_dispatch: # needed to trigger workflows manually
|
|
13
|
+
# todo cron?
|
|
14
|
+
inputs:
|
|
15
|
+
debug_enabled:
|
|
16
|
+
type: boolean
|
|
17
|
+
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
|
18
|
+
required: false
|
|
19
|
+
default: false
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
build:
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
platform: [ubuntu-latest, macos-latest] # , windows-latest]
|
|
28
|
+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
|
29
|
+
exclude: [
|
|
30
|
+
# min and max version is enough for osx running (it's kinda slow)
|
|
31
|
+
{platform: macos-latest, python-version: '3.10'},
|
|
32
|
+
{platform: macos-latest, python-version: '3.11'},
|
|
33
|
+
{platform: macos-latest, python-version: '3.12'},
|
|
34
|
+
]
|
|
35
|
+
runs-on: ${{ matrix.platform }}
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
|
|
39
|
+
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
40
|
+
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
|
|
45
|
+
- uses: astral-sh/setup-uv@v5
|
|
46
|
+
with:
|
|
47
|
+
enable-cache: false # we don't have lock files, so can't use them as cache key
|
|
48
|
+
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
with:
|
|
51
|
+
submodules: recursive
|
|
52
|
+
fetch-depth: 0 # nicer to have all git history when debugging/for tests
|
|
53
|
+
|
|
54
|
+
- uses: mxschmitt/action-tmate@v3
|
|
55
|
+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
|
|
56
|
+
|
|
57
|
+
# explicit bash command is necessary for Windows CI runner, otherwise it thinks it's cmd...
|
|
58
|
+
- run: bash .ci/run
|
|
59
|
+
|
|
60
|
+
- if: matrix.platform == 'ubuntu-latest' # no need to compute coverage for other platforms
|
|
61
|
+
uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
include-hidden-files: true
|
|
64
|
+
name: .coverage.mypy_${{ matrix.platform }}_${{ matrix.python-version }}
|
|
65
|
+
path: .coverage.mypy/
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
pypi:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
needs: [build] # add all other jobs here
|
|
71
|
+
permissions:
|
|
72
|
+
# necessary for Trusted Publishing
|
|
73
|
+
id-token: write
|
|
74
|
+
steps:
|
|
75
|
+
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
|
|
76
|
+
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
77
|
+
|
|
78
|
+
- uses: actions/setup-python@v5
|
|
79
|
+
with:
|
|
80
|
+
python-version: '3.10'
|
|
81
|
+
|
|
82
|
+
- uses: astral-sh/setup-uv@v5
|
|
83
|
+
with:
|
|
84
|
+
enable-cache: false # we don't have lock files, so can't use them as cache key
|
|
85
|
+
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
with:
|
|
88
|
+
submodules: recursive
|
|
89
|
+
|
|
90
|
+
- name: 'release to test pypi'
|
|
91
|
+
# always deploy merged master to test pypi
|
|
92
|
+
if: github.event_name != 'pull_request' && github.event.ref == 'refs/heads/master'
|
|
93
|
+
run: .ci/release-uv --use-test-pypi
|
|
94
|
+
|
|
95
|
+
- name: 'release to pypi'
|
|
96
|
+
# always deploy tags to release pypi
|
|
97
|
+
# NOTE: release tags are guarded by on: push: tags on the top
|
|
98
|
+
if: github.event_name != 'pull_request' && startsWith(github.event.ref, 'refs/tags')
|
|
99
|
+
run: .ci/release-uv
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
|
|
2
|
+
# Created by https://www.gitignore.io/api/python,emacs
|
|
3
|
+
# Edit at https://www.gitignore.io/?templates=python,emacs
|
|
4
|
+
|
|
5
|
+
### Emacs ###
|
|
6
|
+
# -*- mode: gitignore; -*-
|
|
7
|
+
*~
|
|
8
|
+
\#*\#
|
|
9
|
+
/.emacs.desktop
|
|
10
|
+
/.emacs.desktop.lock
|
|
11
|
+
*.elc
|
|
12
|
+
auto-save-list
|
|
13
|
+
tramp
|
|
14
|
+
.\#*
|
|
15
|
+
|
|
16
|
+
# Org-mode
|
|
17
|
+
.org-id-locations
|
|
18
|
+
*_archive
|
|
19
|
+
|
|
20
|
+
# flymake-mode
|
|
21
|
+
*_flymake.*
|
|
22
|
+
|
|
23
|
+
# eshell files
|
|
24
|
+
/eshell/history
|
|
25
|
+
/eshell/lastdir
|
|
26
|
+
|
|
27
|
+
# elpa packages
|
|
28
|
+
/elpa/
|
|
29
|
+
|
|
30
|
+
# reftex files
|
|
31
|
+
*.rel
|
|
32
|
+
|
|
33
|
+
# AUCTeX auto folder
|
|
34
|
+
/auto/
|
|
35
|
+
|
|
36
|
+
# cask packages
|
|
37
|
+
.cask/
|
|
38
|
+
dist/
|
|
39
|
+
|
|
40
|
+
# Flycheck
|
|
41
|
+
flycheck_*.el
|
|
42
|
+
|
|
43
|
+
# server auth directory
|
|
44
|
+
/server/
|
|
45
|
+
|
|
46
|
+
# projectiles files
|
|
47
|
+
.projectile
|
|
48
|
+
|
|
49
|
+
# directory configuration
|
|
50
|
+
.dir-locals.el
|
|
51
|
+
|
|
52
|
+
# network security
|
|
53
|
+
/network-security.data
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Python ###
|
|
57
|
+
# Byte-compiled / optimized / DLL files
|
|
58
|
+
__pycache__/
|
|
59
|
+
*.py[cod]
|
|
60
|
+
*$py.class
|
|
61
|
+
|
|
62
|
+
# C extensions
|
|
63
|
+
*.so
|
|
64
|
+
|
|
65
|
+
# Distribution / packaging
|
|
66
|
+
.Python
|
|
67
|
+
build/
|
|
68
|
+
develop-eggs/
|
|
69
|
+
downloads/
|
|
70
|
+
eggs/
|
|
71
|
+
.eggs/
|
|
72
|
+
lib/
|
|
73
|
+
lib64/
|
|
74
|
+
parts/
|
|
75
|
+
sdist/
|
|
76
|
+
var/
|
|
77
|
+
wheels/
|
|
78
|
+
pip-wheel-metadata/
|
|
79
|
+
share/python-wheels/
|
|
80
|
+
*.egg-info/
|
|
81
|
+
.installed.cfg
|
|
82
|
+
*.egg
|
|
83
|
+
MANIFEST
|
|
84
|
+
|
|
85
|
+
# PyInstaller
|
|
86
|
+
# Usually these files are written by a python script from a template
|
|
87
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
88
|
+
*.manifest
|
|
89
|
+
*.spec
|
|
90
|
+
|
|
91
|
+
# Installer logs
|
|
92
|
+
pip-log.txt
|
|
93
|
+
pip-delete-this-directory.txt
|
|
94
|
+
|
|
95
|
+
# Unit test / coverage reports
|
|
96
|
+
htmlcov/
|
|
97
|
+
.tox/
|
|
98
|
+
.nox/
|
|
99
|
+
.coverage
|
|
100
|
+
.coverage.*
|
|
101
|
+
.cache
|
|
102
|
+
nosetests.xml
|
|
103
|
+
coverage.xml
|
|
104
|
+
*.cover
|
|
105
|
+
.hypothesis/
|
|
106
|
+
.pytest_cache/
|
|
107
|
+
|
|
108
|
+
# Translations
|
|
109
|
+
*.mo
|
|
110
|
+
*.pot
|
|
111
|
+
|
|
112
|
+
# Scrapy stuff:
|
|
113
|
+
.scrapy
|
|
114
|
+
|
|
115
|
+
# Sphinx documentation
|
|
116
|
+
docs/_build/
|
|
117
|
+
|
|
118
|
+
# PyBuilder
|
|
119
|
+
target/
|
|
120
|
+
|
|
121
|
+
# pyenv
|
|
122
|
+
.python-version
|
|
123
|
+
|
|
124
|
+
# pipenv
|
|
125
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
126
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
127
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
128
|
+
# install all needed dependencies.
|
|
129
|
+
#Pipfile.lock
|
|
130
|
+
|
|
131
|
+
# celery beat schedule file
|
|
132
|
+
celerybeat-schedule
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Spyder project settings
|
|
138
|
+
.spyderproject
|
|
139
|
+
.spyproject
|
|
140
|
+
|
|
141
|
+
# Rope project settings
|
|
142
|
+
.ropeproject
|
|
143
|
+
|
|
144
|
+
# Mr Developer
|
|
145
|
+
.mr.developer.cfg
|
|
146
|
+
.project
|
|
147
|
+
.pydevproject
|
|
148
|
+
|
|
149
|
+
# mkdocs documentation
|
|
150
|
+
/site
|
|
151
|
+
|
|
152
|
+
# mypy
|
|
153
|
+
.mypy_cache/
|
|
154
|
+
.dmypy.json
|
|
155
|
+
dmypy.json
|
|
156
|
+
|
|
157
|
+
# Pyre type checker
|
|
158
|
+
.pyre/
|
|
159
|
+
|
|
160
|
+
# End of https://www.gitignore.io/api/python,emacs
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Dmitrii Gerasimov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: bleanser
|
|
3
|
+
Version: 0.1.20250119
|
|
4
|
+
Author-email: "Dima Gerasimov (@karlicoss)" <karlicoss@gmail.com>
|
|
5
|
+
Maintainer-email: "Dima Gerasimov (@karlicoss)" <karlicoss@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2021 Dmitrii Gerasimov
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/karlicoss/bleanser
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Requires-Dist: more-itertools
|
|
31
|
+
Requires-Dist: typing-extensions
|
|
32
|
+
Requires-Dist: click
|
|
33
|
+
Requires-Dist: plumbum
|
|
34
|
+
Requires-Dist: kompress
|
|
35
|
+
Provides-Extra: extra
|
|
36
|
+
Requires-Dist: python-magic; extra == "extra"
|
|
37
|
+
Requires-Dist: logzero; extra == "extra"
|
|
38
|
+
Provides-Extra: json
|
|
39
|
+
Requires-Dist: orjson; extra == "json"
|
|
40
|
+
Provides-Extra: xml
|
|
41
|
+
Requires-Dist: lxml; extra == "xml"
|
|
42
|
+
Provides-Extra: zstd
|
|
43
|
+
Requires-Dist: kompress[zstd]; extra == "zstd"
|
|
44
|
+
Provides-Extra: hpi
|
|
45
|
+
Requires-Dist: HPI; extra == "hpi"
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
bleanser or 'backup cleanser' is a tool for cleaning old and redundant backups
|
|
2
|
+
|
|
3
|
+
## Installing
|
|
4
|
+
|
|
5
|
+
To install, run: `pip install bleanser`.
|
|
6
|
+
|
|
7
|
+
There are also extra install options. You can use none or multiple depending on your needs, i.e. `pip install bleanser[flavor1,flavor2]`:
|
|
8
|
+
|
|
9
|
+
- `bleanser[extra]` : some recommended but optional extras
|
|
10
|
+
- `bleanser[json]` : dependencies for JSON based modules
|
|
11
|
+
- `bleanser[xml]` : dependencies for XML based modules
|
|
12
|
+
- `bleanser[HPI]` : dependencies for [HPI](https://github.com/karlicoss/HPI) based modules
|
|
13
|
+
|
|
14
|
+
See `optional-dependencies` section in [pyproject.toml](pyproject.toml) if you're curious what libraries these extras pull.
|
|
15
|
+
|
|
16
|
+
## What bleanser does
|
|
17
|
+
|
|
18
|
+
In this context, backup typically means something like a GDPR export, an XML or JSON file which includes your data from some website/API, or a sqlite database from an application
|
|
19
|
+
|
|
20
|
+
<https://beepb00p.xyz/exobrain/projects/bleanser.html>
|
|
21
|
+
|
|
22
|
+
This is used to find 'redundant backups'. As an example, say you save your data to a JSON file by making API requests to some API service once a day. If your export of the data you exported today is a [superset](https://en.wikipedia.org/wiki/Subset) of the export yesterday, you know you can safely delete the old file and still have a complete backup of your data. This helps:
|
|
23
|
+
|
|
24
|
+
- save on disk space
|
|
25
|
+
- save of data access time; how long it takes to parse all your input files (see [data access layer](https://beepb00p.xyz/exports.html#dal))
|
|
26
|
+
|
|
27
|
+
This works for both [full](https://beepb00p.xyz/exports.html#full) (you're able to get all your data from a service) and [incremental](https://beepb00p.xyz/exports.html#incremental) exports.
|
|
28
|
+
|
|
29
|
+
This is especially relevant for incremental data exports, as they're harder to reason about. So, this handles the complex bits of diffing adjacent backups.
|
|
30
|
+
|
|
31
|
+
As an example of an incremental export, imagine the service you were using only gave you access to the latest 3 items in your history (a real example of this is the [github activity feed](https://github.com/karlicoss/ghexport))
|
|
32
|
+
|
|
33
|
+
| Day 1 | Day 2 | Day 3 |
|
|
34
|
+
| ----- | ----- | ----- |
|
|
35
|
+
| A | B | C |
|
|
36
|
+
| B | C | D |
|
|
37
|
+
| C | D | E |
|
|
38
|
+
|
|
39
|
+
To parse this in your [data access layer](https://beepb00p.xyz/exports.html#dal), you could imagine something like this:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
events = set()
|
|
43
|
+
for file in inputs:
|
|
44
|
+
for line in file:
|
|
45
|
+
events.add(line)
|
|
46
|
+
# events is now {'A', 'B', 'C', 'D', 'E'}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You might notice that if you removed 'Day 2', you'd still have an accurate backup, and we'd still have all 5 items, but its not obvious you can remove it since none of these are supersets of each other.
|
|
50
|
+
|
|
51
|
+
`bleanser` is meant to solve this problem in a data agnostic way, so any export can be converted to a normalised representation, and those can be compared against each other to find redundant data
|
|
52
|
+
|
|
53
|
+
Sidenote: in particular this is describing how `--multiway` finds redundant files, see [`options.md`](./doc/options.md) for more info
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
This has `Normaliser`s for different data sources (see [modules](src/bleanser/modules)), and generally follows a pattern like this:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from contextlib import contextmanager
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import Iterator
|
|
63
|
+
|
|
64
|
+
from bleanser.core.processor import BaseNormaliser, unique_file_in_tempdir
|
|
65
|
+
|
|
66
|
+
class Normaliser(BaseNormaliser):
|
|
67
|
+
|
|
68
|
+
@contextmanager
|
|
69
|
+
def normalise(self, *, path: Path) -> Iterator[Path]:
|
|
70
|
+
# if the input file was compressed, the "path" you receive here will be decompressed
|
|
71
|
+
|
|
72
|
+
# a temporary file we write 'normalised' data to, that can be easily diffed/compared
|
|
73
|
+
normalised = unique_file_in_tempdir(input_filepath=path, dir=self.tmp_dir)
|
|
74
|
+
|
|
75
|
+
# some custom code here per-module that writes to 'normalised'
|
|
76
|
+
|
|
77
|
+
yield normalised
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# this script should be run as a module like
|
|
81
|
+
# python3 -m bleanser.modules.smscalls --glob ...
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
Normaliser.main()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This is **always** acting on the data loaded into memory/temporary files, it is not modifying the files itself. Once it determines an input file can be pruned, it will warn you by default, and you can specify `--move` or `--remove` with the CLI (see below) to remove it.
|
|
87
|
+
|
|
88
|
+
There are particular normalisers for different filetypes, e.g. [`json`](./src/bleanser/core/modules/json.py), [`xml`](./src/bleanser/core/modules/xml.py), [`sqlite`](./src/bleanser/core/modules/sqlite.py) which might work if your data is especially basic, but typically this requires subclassing one of those and writing some custom code to 'cleanup' the data, so it can be properly compared/diffed.
|
|
89
|
+
|
|
90
|
+
### normalise
|
|
91
|
+
|
|
92
|
+
There are two ways you can think about `normalise` (creating a 'cleaned'/normalised representation of an input file) -- by specifying an 'upper' or 'lower' bound:
|
|
93
|
+
|
|
94
|
+
- upper: specify which data you want to drop, dumping everything else to `normalised`
|
|
95
|
+
- lower: specify which keys/data you want to keep, e.g. only returning a few keys which uniquely identify events in the data
|
|
96
|
+
|
|
97
|
+
As an example say you had a JSON export:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
[
|
|
101
|
+
{ "id": 5, "images": [{}], "href": "..." },
|
|
102
|
+
{ "id": 6, "images": [{}], "href": "..." },
|
|
103
|
+
{ "id": 7, "images": [{}], "href": "..." }
|
|
104
|
+
]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When comparing this, you could possibly:
|
|
108
|
+
|
|
109
|
+
1. Just write the `id` to the file. This is somewhat risky as you don't know if the `href` will always remain the same, so you may be losing data
|
|
110
|
+
2. Write the `id` and the `href`, by specifying those two keys you're interested in
|
|
111
|
+
3. Write the `id` and the `href`, by deleting the `images` key (this is different from 2!)
|
|
112
|
+
|
|
113
|
+
There is a trade-off to be made here. For especially noisy exports with lots of metadata that might change over time that you're not interested in, number 3 means every couple months you might have to check and add new keys to delete (as an example see [spotify](./src/bleanser/modules/spotify.py)). This could be seen as a positive as well, as it means when the schema for the API/data changes underneath you, you may notice it quicker
|
|
114
|
+
|
|
115
|
+
With option 2, you are more likely to remove redundant data files if additional metadata fields are added, and if you only really care about the `id` and `href` and you don't think the export format will change often, this is fine.
|
|
116
|
+
|
|
117
|
+
Option 3. is generally the safest, but most verbose/tedious, it makes sure you're not removing files that may possibly contain new fields you want to preserve/parse.
|
|
118
|
+
|
|
119
|
+
Ideally you meet somewhere in the middle, it depends a lot on the specific export data you're comparing.
|
|
120
|
+
|
|
121
|
+
As it can be a bit difficult to follow, generally this is doing something like:
|
|
122
|
+
|
|
123
|
+
- Decompress file if its a known compressed format into a `cleaned` file (`unpacked` in [`BaseNormaliser`](./src/bleanser/core/processor.py)), see [`kompress`](https://github.com/karlicoss/kompress/) for supported compression formats
|
|
124
|
+
- Creating a temporary file to write data to (`unique_file_in_tempdir` in [`BaseNormaliser`](./src/bleanser/core/processor.py))
|
|
125
|
+
- Parse the `cleaned` file into python objects (`JsonNormaliser`, `XmlNormaliser`, or something custom)
|
|
126
|
+
- Let the user `cleanup` the data to remove noisy keys/data (specific modules, e.g. [spotify](./src/bleanser/modules/spotify.py))
|
|
127
|
+
- Diff those against each other to find and/or remove files which dont contribute new data (module agnostic, run in `main`)
|
|
128
|
+
|
|
129
|
+
### Subclassing
|
|
130
|
+
|
|
131
|
+
For example, the JSON normaliser calls a `cleanup` function before it starts processing the data. If you wanted to remove the `images` key like shown above, you could do so like:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from bleanser.core.modules.json import JsonNormaliser, delkeys, Json
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Normaliser(JsonNormaliser):
|
|
138
|
+
# here, j is a dict, each file that this gets passed from the CLI call
|
|
139
|
+
# below is pre-processed by the cleanup function
|
|
140
|
+
def cleanup(self, j: Json) -> Json:
|
|
141
|
+
delkeys(j, keys={
|
|
142
|
+
'images',
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return j
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == '__main__':
|
|
149
|
+
Normaliser.main()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
For common formats, the helper classes handle all the tedious bits like loading/parsing data, managing the temporary files. The `Normaliser.main` calls the CLI, which looks like this:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
$ python3 -m bleanser.core.modules.json prune --help
|
|
156
|
+
Usage: python -m bleanser.core.modules.json prune [OPTIONS] PATH
|
|
157
|
+
|
|
158
|
+
Options:
|
|
159
|
+
--glob Treat the path as glob (in the glob.glob sense)
|
|
160
|
+
--sort-by [size|name] how to sort input files [default: name]
|
|
161
|
+
--dry Do not prune the input files, just print what would happen after pruning.
|
|
162
|
+
--remove Prune the input files by REMOVING them (be careful!)
|
|
163
|
+
--move PATH Prune the input files by MOVING them to the specified path. A bit safer than --remove mode.
|
|
164
|
+
--yes Do not prompt before pruning files (useful for cron etc)
|
|
165
|
+
--threads INTEGER Number of threads (processes) to use. Without the flag won't use any, with the flag will try
|
|
166
|
+
using all available, can also take a specific value. Passed down to PoolExecutor.
|
|
167
|
+
--from INTEGER
|
|
168
|
+
--to INTEGER
|
|
169
|
+
--multiway force "multiway" cleanup
|
|
170
|
+
--prune-dominated
|
|
171
|
+
--help Show this message and exit.
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
You'd provide input paths/globs to this file, and possibly `--remove` or `--move /tmp/removed` to remove/move files
|
|
175
|
+
|
|
176
|
+
If you're not able to subclass one of the those, you might be able to subclass [extract](./src/bleanser/core/modules/extract.py), which lets you just yield any sort of string-afiable data, which is then used to diff/compare the input files. For example, if you only wanted to return the `id` and `href` in the JSON example above, you could just return a tuple:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
import json
|
|
180
|
+
from pathlib import Path
|
|
181
|
+
from typing import Iterator, Any
|
|
182
|
+
|
|
183
|
+
from bleanser.core.modules.extract import ExtractObjectsNormaliser
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Normaliser(ExtractObjectsNormaliser):
|
|
187
|
+
def extract_objects(self, path: Path) -> Iterator[Any]:
|
|
188
|
+
data = json.loads(path.read_text())
|
|
189
|
+
for blob in data:
|
|
190
|
+
yield (blob["id"], blob["href"])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
Normaliser.main()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Otherwise if you have some complex data source you need to handle yourself, you can override `do_normalise` and `unpacked` (how the data gets uncompressed/pre-processed) methods yourself, see handling the [discord zip files](https://github.com/purarue/bleanser/blob/master/src/bleanser_pura/modules/discord.py) as an example.
|