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.
Files changed (93) hide show
  1. bleanser-0.1.20250119/.ci/release +66 -0
  2. bleanser-0.1.20250119/.ci/release-uv +60 -0
  3. bleanser-0.1.20250119/.ci/run +37 -0
  4. bleanser-0.1.20250119/.github/workflows/main.yml +99 -0
  5. bleanser-0.1.20250119/.gitignore +160 -0
  6. bleanser-0.1.20250119/LICENSE +21 -0
  7. bleanser-0.1.20250119/PKG-INFO +45 -0
  8. bleanser-0.1.20250119/README.md +197 -0
  9. bleanser-0.1.20250119/conftest.py +47 -0
  10. bleanser-0.1.20250119/doall +36 -0
  11. bleanser-0.1.20250119/doc/options.md +38 -0
  12. bleanser-0.1.20250119/mypy.ini +17 -0
  13. bleanser-0.1.20250119/old/README.md +1 -0
  14. bleanser-0.1.20250119/old/common.py +16 -0
  15. bleanser-0.1.20250119/old/foursquare.py +65 -0
  16. bleanser-0.1.20250119/old/jq_normaliser.py +391 -0
  17. bleanser-0.1.20250119/old/json.py +239 -0
  18. bleanser-0.1.20250119/old/json2.py +169 -0
  19. bleanser-0.1.20250119/old/reddit.py +283 -0
  20. bleanser-0.1.20250119/pyproject.toml +76 -0
  21. bleanser-0.1.20250119/pytest.ini +13 -0
  22. bleanser-0.1.20250119/ruff.toml +146 -0
  23. bleanser-0.1.20250119/scripts/apt.sh +10 -0
  24. bleanser-0.1.20250119/setup.cfg +4 -0
  25. bleanser-0.1.20250119/src/bleanser/__init__.py +0 -0
  26. bleanser-0.1.20250119/src/bleanser/__init__.pyi +0 -0
  27. bleanser-0.1.20250119/src/bleanser/core/__init__.py +5 -0
  28. bleanser-0.1.20250119/src/bleanser/core/__main__.py +12 -0
  29. bleanser-0.1.20250119/src/bleanser/core/common.py +138 -0
  30. bleanser-0.1.20250119/src/bleanser/core/compat.py +6 -0
  31. bleanser-0.1.20250119/src/bleanser/core/ext/__init__.py +0 -0
  32. bleanser-0.1.20250119/src/bleanser/core/ext/dummy_executor.py +31 -0
  33. bleanser-0.1.20250119/src/bleanser/core/ext/logging.py +97 -0
  34. bleanser-0.1.20250119/src/bleanser/core/ext/sqlite_dumben.py +246 -0
  35. bleanser-0.1.20250119/src/bleanser/core/main.py +160 -0
  36. bleanser-0.1.20250119/src/bleanser/core/modules/__init__.py +0 -0
  37. bleanser-0.1.20250119/src/bleanser/core/modules/extract.py +60 -0
  38. bleanser-0.1.20250119/src/bleanser/core/modules/json.py +110 -0
  39. bleanser-0.1.20250119/src/bleanser/core/modules/sqlite.py +486 -0
  40. bleanser-0.1.20250119/src/bleanser/core/modules/tests/sqlite.py +103 -0
  41. bleanser-0.1.20250119/src/bleanser/core/modules/xml.py +173 -0
  42. bleanser-0.1.20250119/src/bleanser/core/processor.py +1311 -0
  43. bleanser-0.1.20250119/src/bleanser/core/sqlite.py +12 -0
  44. bleanser-0.1.20250119/src/bleanser/core/utils.py +128 -0
  45. bleanser-0.1.20250119/src/bleanser/modules/__init__.pyi +0 -0
  46. bleanser-0.1.20250119/src/bleanser/modules/antennapod_android.py +54 -0
  47. bleanser-0.1.20250119/src/bleanser/modules/binary.py +16 -0
  48. bleanser-0.1.20250119/src/bleanser/modules/bluemaestro.py +87 -0
  49. bleanser-0.1.20250119/src/bleanser/modules/bumble_android.py +67 -0
  50. bleanser-0.1.20250119/src/bleanser/modules/chrome.py +59 -0
  51. bleanser-0.1.20250119/src/bleanser/modules/fbmessenger_android.py +247 -0
  52. bleanser-0.1.20250119/src/bleanser/modules/firefox.py +241 -0
  53. bleanser-0.1.20250119/src/bleanser/modules/foursquare.py +106 -0
  54. bleanser-0.1.20250119/src/bleanser/modules/ghexport.py +85 -0
  55. bleanser-0.1.20250119/src/bleanser/modules/goodreads.py +21 -0
  56. bleanser-0.1.20250119/src/bleanser/modules/hinge_android.py +103 -0
  57. bleanser-0.1.20250119/src/bleanser/modules/hpi/fbmessenger_android.py +43 -0
  58. bleanser-0.1.20250119/src/bleanser/modules/hpi/twitter_android.py +42 -0
  59. bleanser-0.1.20250119/src/bleanser/modules/hpi/whatsapp_android.py +40 -0
  60. bleanser-0.1.20250119/src/bleanser/modules/instagram_android.py +181 -0
  61. bleanser-0.1.20250119/src/bleanser/modules/json_new.py +9 -0
  62. bleanser-0.1.20250119/src/bleanser/modules/kobo.py +95 -0
  63. bleanser-0.1.20250119/src/bleanser/modules/lastfm.py +70 -0
  64. bleanser-0.1.20250119/src/bleanser/modules/monzo.py +46 -0
  65. bleanser-0.1.20250119/src/bleanser/modules/pinboard.py +78 -0
  66. bleanser-0.1.20250119/src/bleanser/modules/pocket.py +14 -0
  67. bleanser-0.1.20250119/src/bleanser/modules/podcastaddict_android.py +125 -0
  68. bleanser-0.1.20250119/src/bleanser/modules/reddit.py +288 -0
  69. bleanser-0.1.20250119/src/bleanser/modules/rescuetime.py +39 -0
  70. bleanser-0.1.20250119/src/bleanser/modules/rescuetime_android.py +24 -0
  71. bleanser-0.1.20250119/src/bleanser/modules/skype_android.py +44 -0
  72. bleanser-0.1.20250119/src/bleanser/modules/sleepasandroid_android.py +24 -0
  73. bleanser-0.1.20250119/src/bleanser/modules/smscalls.py +84 -0
  74. bleanser-0.1.20250119/src/bleanser/modules/spotify.py +56 -0
  75. bleanser-0.1.20250119/src/bleanser/modules/spotifyexport.py +22 -0
  76. bleanser-0.1.20250119/src/bleanser/modules/stackexchange.py +52 -0
  77. bleanser-0.1.20250119/src/bleanser/modules/talon_android.py +23 -0
  78. bleanser-0.1.20250119/src/bleanser/modules/tiktok_android.py +28 -0
  79. bleanser-0.1.20250119/src/bleanser/modules/tinder_android.py +116 -0
  80. bleanser-0.1.20250119/src/bleanser/modules/twitter_android.py +144 -0
  81. bleanser-0.1.20250119/src/bleanser/modules/vk_android.py +167 -0
  82. bleanser-0.1.20250119/src/bleanser/modules/whatsapp_android.py +268 -0
  83. bleanser-0.1.20250119/src/bleanser/modules/xml_clean.py +9 -0
  84. bleanser-0.1.20250119/src/bleanser/py.typed +0 -0
  85. bleanser-0.1.20250119/src/bleanser/tests/common.py +69 -0
  86. bleanser-0.1.20250119/src/bleanser/tests/test_binary.py +45 -0
  87. bleanser-0.1.20250119/src/bleanser/tests/test_hypothesis.py +53 -0
  88. bleanser-0.1.20250119/src/bleanser.egg-info/PKG-INFO +45 -0
  89. bleanser-0.1.20250119/src/bleanser.egg-info/SOURCES.txt +91 -0
  90. bleanser-0.1.20250119/src/bleanser.egg-info/dependency_links.txt +1 -0
  91. bleanser-0.1.20250119/src/bleanser.egg-info/requires.txt +21 -0
  92. bleanser-0.1.20250119/src/bleanser.egg-info/top_level.txt +1 -0
  93. 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.