├── .coveragerc ├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── coverage.svg ├── mkdocs.yml ├── pages │ ├── css │ │ └── extra.css │ ├── demos │ │ ├── create.svg │ │ ├── edit.svg │ │ └── run.svg │ ├── enhancements.md │ ├── examples-rl │ │ ├── appmemdumper.md │ │ ├── bots-scheduler.md │ │ ├── git-web-recovery.png │ │ ├── malicious-macro-tester.md │ │ ├── pdf-password-bruteforce.png │ │ ├── recursive-compression.md │ │ ├── solitaire-cipher.md │ │ ├── solitaire-cipher.png │ │ ├── stegolsb.png │ │ ├── stegopit.png │ │ └── stegopvd.png │ ├── examples │ │ ├── demo.md │ │ ├── hotkeys.md │ │ ├── metadata.md │ │ ├── multi-level-debug.md │ │ ├── multi-level-help.md │ │ ├── simple-script.md │ │ ├── simple-tool.md │ │ ├── step.md │ │ ├── sudo.md │ │ └── timing.md │ ├── helpers.md │ ├── img │ │ ├── icon.png │ │ └── logo.png │ ├── index.md │ ├── internals.md │ ├── reporting.md │ ├── shaping.md │ ├── tsm.md │ ├── usage.md │ └── utility.md ├── requirements.txt └── scripts.list ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── src └── tinyscript │ ├── VERSION.txt │ ├── __conf__.py │ ├── __info__.py │ ├── __init__.py │ ├── __main__.py │ ├── argreparse.py │ ├── features │ ├── __init__.py │ ├── handlers.py │ ├── hotkeys.py │ ├── interact.py │ ├── loglib.py │ ├── notify │ │ ├── __init__.py │ │ ├── critical.png │ │ ├── debug.png │ │ ├── error.png │ │ ├── failure.png │ │ ├── info.png │ │ ├── success.png │ │ └── warning.png │ ├── progress.py │ ├── step.py │ └── timing.py │ ├── helpers │ ├── __init__.py │ ├── attack.py │ ├── classprop.py │ ├── common.py │ ├── compat.py │ ├── constants.py │ ├── data │ │ ├── __init__.py │ │ ├── transform │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ └── report.py │ │ ├── types │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── config.py │ │ │ ├── files.py │ │ │ ├── hash.py │ │ │ ├── network.py │ │ │ └── strings.py │ │ └── utils.py │ ├── decorators.py │ ├── dictionaries.py │ ├── docstring.py │ ├── expressions.py │ ├── fexec.py │ ├── inputs.py │ ├── layout.py │ ├── licenses.py │ ├── notify.py │ ├── parser.py │ ├── password.py │ ├── path.py │ ├── termsize.py │ ├── text.py │ └── timeout.py │ ├── parser.py │ ├── preimports │ ├── __init__.py │ ├── codep.py │ ├── ftools.py │ ├── hash.py │ ├── inspectp.py │ ├── itools.py │ ├── jsonp.py │ ├── log.py │ ├── pswd.py │ ├── rand.py │ ├── regex.py │ ├── stringp.py │ └── venv.py │ ├── report │ ├── __init__.py │ ├── base.py │ ├── default.css │ ├── objects.py │ └── report.py │ └── template.py └── tests ├── conftest.py ├── test_argreparse.py ├── test_conf.py ├── test_features_handlers.py ├── test_features_hotkeys.py ├── test_features_interact.py ├── test_features_loglib.py ├── test_features_notify.py ├── test_features_progress.py ├── test_features_step.py ├── test_features_timing.py ├── test_helpers_attack.py ├── test_helpers_classprop.py ├── test_helpers_common.py ├── test_helpers_compat.py ├── test_helpers_data_transform.py ├── test_helpers_data_types.py ├── test_helpers_data_utils.py ├── test_helpers_decorators.py ├── test_helpers_dictionaries.py ├── test_helpers_docstring.py ├── test_helpers_expressions.py ├── test_helpers_fexec.py ├── test_helpers_inputs.py ├── test_helpers_layout.py ├── test_helpers_licenses.py ├── test_helpers_parser.py ├── test_helpers_password.py ├── test_helpers_path.py ├── test_helpers_text.py ├── test_helpers_timeout.py ├── test_parser.py ├── test_preimports.py ├── test_preimports_code.py ├── test_preimports_ftools.py ├── test_preimports_getpass.py ├── test_preimports_hashlib.py ├── test_preimports_inspect.py ├── test_preimports_itools.py ├── test_preimports_json.py ├── test_preimports_logging.py ├── test_preimports_random.py ├── test_preimports_re.py ├── test_preimports_string.py ├── test_preimports_virtualenv.py ├── test_report.py ├── test_template.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | cover_pylib = false 3 | source = tinyscript 4 | omit = 5 | */site-packages/* 6 | src/tinyscript/__main__.py 7 | src/tinyscript/hotkeys.py 8 | src/tinyscript/warnings.py 9 | src/tinyscript/helpers/termsize.py 10 | tests/* 11 | 12 | [report] 13 | exclude_lines = 14 | pragma: no cover 15 | # __main__ logics 16 | if\s+__name__\s+==\s+(?P(?:[\'\"]))__main__(?P=q)\s+: 17 | # sudo when using 'initialize' 18 | if sudo and not is_admin(): 19 | # 'interact' module - remote interaction 20 | class ConsoleSocket 21 | class RemoteInteractiveConsole 22 | # exit tasks 23 | def __at_exit\(\)\: 24 | if DARWIN: 25 | if WINDOWS: 26 | raise NotImplementedError 27 | except NotImplementedError: 28 | def hotkeys(hotkeys, silent=True): 29 | super\(NewClass\, self\)\.__init__ 30 | # optional packages not installed or different platform or cumbersome to test 31 | if not hotkeys_enabled: 32 | def stdin_pipe\(\): 33 | Xlib.error.DisplayConnectionError 34 | def send_mail 35 | except KeyboardInterrupt: 36 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: build 5 | 6 | env: 7 | package: tinyscript 8 | 9 | on: 10 | push: 11 | branches: [ "main" ] 12 | pull_request: 13 | branches: [ "main" ] 14 | 15 | jobs: 16 | build: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest] 22 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install pandoc 30 | run: sudo apt-get install -y pandoc 31 | - name: Install ${{ env.package }} 32 | run: | 33 | python -m pip install --upgrade pip 34 | python -m pip install pytest pytest-cov pytest-pythonpath coverage 35 | pip install -r requirements.txt 36 | pip install . 37 | - name: Test ${{ env.package }} with pytest 38 | run: | 39 | pytest --cov=$package 40 | coverage: 41 | needs: build 42 | runs-on: ubuntu-latest 43 | env: 44 | cov_badge_path: docs/coverage.svg 45 | steps: 46 | - uses: actions/checkout@v3 47 | - name: Set up Python ${{ matrix.python-version }} 48 | uses: actions/setup-python@v4 49 | with: 50 | python-version: "3.12" 51 | - name: Install pandoc 52 | run: sudo apt-get install -y pandoc notification-daemon 53 | - name: Install ${{ env.package }} 54 | run: | 55 | python -m pip install --upgrade pip 56 | python -m pip install pytest pytest-cov pytest-pythonpath 57 | pip install -r requirements.txt 58 | pip install . 59 | - name: Make coverage badge for ${{ env.package }} 60 | run: | 61 | pip install genbadge[coverage] 62 | pytest --cov=$package --cov-report=xml 63 | genbadge coverage -i coverage.xml -o $cov_badge_path 64 | - name: Verify Changed files 65 | uses: tj-actions/verify-changed-files@v17 66 | id: changed_files 67 | with: 68 | files: ${{ env.cov_badge_path }} 69 | - name: Commit files 70 | if: steps.changed_files.outputs.files_changed == 'true' 71 | run: | 72 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 73 | git config --local user.name "github-actions[bot]" 74 | git add $cov_badge_path 75 | git commit -m "Updated coverage.svg" 76 | - name: Push changes 77 | if: steps.changed_files.outputs.files_changed == 'true' 78 | uses: ad-m/github-push-action@master 79 | with: 80 | github_token: ${{ secrets.github_token }} 81 | branch: ${{ github.ref }} 82 | deploy: 83 | runs-on: ubuntu-latest 84 | needs: coverage 85 | steps: 86 | - uses: actions/checkout@v3 87 | with: 88 | fetch-depth: 0 89 | - name: Check for version change 90 | uses: dorny/paths-filter@v2 91 | id: filter 92 | with: 93 | filters: | 94 | version: 95 | - '**/VERSION.txt' 96 | - if: steps.filter.outputs.version == 'true' 97 | name: Cleanup README 98 | run: | 99 | sed -ri 's/^(##*)\s*:.*:\s*/\1 /g' README.md 100 | awk '{if (match($0,"## Supporters")) exit; print}' README.md > README 101 | mv -f README README.md 102 | - if: steps.filter.outputs.version == 'true' 103 | name: Build ${{ env.package }} package 104 | run: python3 -m pip install --upgrade build && python3 -m build 105 | - if: steps.filter.outputs.version == 'true' 106 | name: Upload ${{ env.package }} to PyPi 107 | uses: pypa/gh-action-pypi-publish@release/v1 108 | with: 109 | password: ${{ secrets.PYPI_API_TOKEN }} 110 | verbose: true 111 | verify_metadata: false 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temp files 2 | *~ 3 | *.backup 4 | .DS_Store 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | .build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | reinstall.sh 27 | test.sh 28 | update.sh 29 | version.py 30 | 31 | var/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | MANIFEST 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | coverage/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .coveralls.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *,cover 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Project artifacts 70 | .idea 71 | .vagrant 72 | .test 73 | .pytest_cache 74 | .tinyscript-test.ini 75 | tmp 76 | TODO 77 | script.py 78 | tool.py 79 | .mypy_cache 80 | /*.txt 81 | /*.pdf 82 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.11" 7 | 8 | mkdocs: 9 | configuration: docs/mkdocs.yml 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /docs/coverage.svg: -------------------------------------------------------------------------------- 1 | coverage: 96.74%coverage96.74% -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_author: dhondta 2 | site_name: "Tinyscript - Devkit for quickly building CLI tools with Python" 3 | repo_url: https://github.com/dhondta/python-tinyscript 4 | copyright: Copyright © 2017-2023 Alexandre D'Hondt 5 | docs_dir: pages 6 | nav: 7 | - Introduction: index.md 8 | - Internals: internals.md 9 | - 'Enhanced modules': enhancements.md 10 | - 'Tool shaping': shaping.md 11 | - 'Report generation': reporting.md 12 | - 'Utility features': utility.md 13 | - Helpers: helpers.md 14 | - Usage: usage.md 15 | - 'Scripts management': tsm.md 16 | - Examples: 17 | - 'Simple script': examples/simple-script.md 18 | - 'Simple tool': examples/simple-tool.md 19 | - 'Metadata': examples/metadata.md 20 | - 'Privilege elevation': examples/sudo.md 21 | - 'Multi-level debugging': examples/multi-level-debug.md 22 | - 'Multi-level help': examples/multi-level-help.md 23 | - 'Playing a demo': examples/demo.md 24 | - 'Stepping execution': examples/step.md 25 | - 'Timing execution': examples/timing.md 26 | - 'Hotkeys': examples/hotkeys.md 27 | - 'Real-life examples': 28 | - 'AppmemDumper': examples-rl/appmemdumper.md 29 | - 'Bots Scheduler': examples-rl/bots-scheduler.md 30 | - 'Malicious Macro Tester': examples-rl/malicious-macro-tester.md 31 | - 'Recursive Compression': examples-rl/recursive-compression.md 32 | - 'Solitaire Cipher': examples-rl/solitaire-cipher.md 33 | extra_css: 34 | - css/extra.css 35 | extra: 36 | generator: false 37 | social: 38 | - icon: fontawesome/solid/paper-plane 39 | link: mailto:alexandre.dhondt@gmail.com 40 | name: Contact Alex 41 | - icon: fontawesome/brands/github 42 | link: https://github.com/dhondta 43 | name: Alex on GitHub 44 | - icon: fontawesome/brands/linkedin 45 | link: https://www.linkedin.com/in/alexandre-d-2ab2aa14/ 46 | name: Alex on LinkedIn 47 | - icon: fontawesome/brands/twitter 48 | link: https://twitter.com/alex_dhondt 49 | name: Alex on Twitter 50 | theme: 51 | name: material 52 | palette: 53 | - scheme: default 54 | toggle: 55 | icon: material/brightness-7 56 | name: Switch to dark mode 57 | - scheme: slate 58 | toggle: 59 | icon: material/brightness-4 60 | name: Switch to light mode 61 | logo: img/logo.png 62 | favicon: img/icon.png 63 | use_directory_urls: false 64 | markdown_extensions: 65 | - toc: 66 | permalink: true 67 | - admonition 68 | - pymdownx.details 69 | - pymdownx.superfences 70 | -------------------------------------------------------------------------------- /docs/pages/css/extra.css: -------------------------------------------------------------------------------- 1 | /* Full width (only works for some themes, including 'material') */ 2 | @media only screen and (min-width: 76.25em) { 3 | .md-main__inner { 4 | max-width: none; 5 | } 6 | .md-sidebar--primary { 7 | left: 0; 8 | } 9 | .md-sidebar--secondary { 10 | right: 0; 11 | margin-left: 0; 12 | -webkit-transform: none; 13 | transform: none; 14 | } 15 | } 16 | 17 | /* See https://github.com/mkdocs/mkdocs/wiki/MkDocs-Recipes */ 18 | /* Add Support for Checkbox Lists */ 19 | .task-list-item { 20 | list-style-type: none; 21 | } 22 | 23 | .task-list-item input { 24 | margin: 0 4px 0.25em -20px; 25 | vertical-align: middle; 26 | } 27 | -------------------------------------------------------------------------------- /docs/pages/examples-rl/git-web-recovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/examples-rl/git-web-recovery.png -------------------------------------------------------------------------------- /docs/pages/examples-rl/malicious-macro-tester.md: -------------------------------------------------------------------------------- 1 | # Malicious Macro Tester 2 | 3 | ## Description 4 | 5 | This CLI tool automates the classification of Office documents with macros using MaliciousMacroBot. It allows to analyze a folder of sample files and to generate a report in multiple output formats. 6 | 7 | ## Code 8 | 9 | See [this GitHub repository](https://github.com/dhondta/malicious-macro-tester). 10 | 11 | ## Help 12 | 13 | ```sh 14 | $ malicious-macro-tester -h 15 | usage: malicious-macro-tester [-d] [-f] [-l] [-q] [-r] [-s] [-u] 16 | [--api-key VT_KEY] 17 | [--output {es,html,json,md,pdf,xml}] [--send] 18 | [-h] [-v] 19 | FOLDER 20 | 21 | MaliciousMacroTester v2.4 22 | Author : Alexandre D'Hondt 23 | Copyright: © 2019 A. D'Hondt 24 | License : GNU Affero General Public License v3.0 25 | Reference: INFOM444 - Machine Learning - Hot Topic 26 | 27 | This tool uses MaliciousMacroBot to classify a list of samples as benign or 28 | malicious and provides a report. Note that it only works on an input folder 29 | and list every file to run it against mmbot. 30 | 31 | positional arguments: 32 | FOLDER folder with the samples to be tested OR 33 | pickle name if results are loaded with -l 34 | 35 | optional arguments: 36 | -d dump the VBA macros (default: False) 37 | -f filter only DOC and XLS files (default: False) 38 | -l load previous pickled results (default: False) 39 | -q do not display results report (default: False) 40 | -r when loading pickle, retry VirusTotal hashes with None results 41 | (default: False) 42 | -s pickle results to a file (default: False) 43 | -u when loading pickle, update VirusTotal results (default: False) 44 | --api-key VT_KEY VirusTotal API key (default: None) 45 | NB: key as a string or file path to the key 46 | --output {es,html,json,md,pdf,xml} 47 | report file format (default: None) 48 | --send send the data to ElasticSearch (default: False) 49 | NB: only applies to 'es' format 50 | the configuration is loaded with the following precedence: 51 | 1. ./elasticsearch.conf 52 | 2. /etc/elasticsearch/elasticsearch.conf 53 | 54 | extra arguments: 55 | -h, --help show this help message and exit 56 | -v, --verbose verbose mode (default: False) 57 | 58 | Usage examples: 59 | malicious-macro-tester my_samples_folder 60 | malicious-macro-tester my_samples_folder --api-key virustotal-key.txt -lr 61 | malicious-macro-tester my_samples_folder -lsrv --api-key 098fa24...be724a0 62 | malicious-macro-tester my_samples_folder -lf --output pdf 63 | malicious-macro-tester my_samples_folder --output es --sent 64 | 65 | ``` 66 | 67 | ## Execution 68 | 69 | ```session 70 | $ python malicious-macro-tester.py samples -vfqs --output xml 71 | 17:08:09 [INFO] Instantiating and initializing MaliciousMacroBot... 72 | 17:09:09 [INFO] Processing samples... 73 | 17:09:09 [DEBUG] MMBot: classifying 'file_003.xls'... 74 | 17:09:09 [DEBUG] MMBot: classifying 'file_001.doc'... 75 | 17:09:09 [DEBUG] MMBot: classifying 'file_005.xls'... 76 | 17:09:09 [DEBUG] MMBot: classifying 'file_000.doc'... 77 | 17:09:09 [DEBUG] MMBot: classifying 'file_004.xls'... 78 | 17:09:10 [DEBUG] MMBot: classifying 'file_002.doc'... 79 | 17:09:10 [INFO] Saving results to pickle... 80 | 17:09:10 [INFO] Parsing results... 81 | 17:09:10 [DEBUG] Generating the JSON report (text only)... 82 | 17:09:10 [DEBUG] Generating the XML report... 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/pages/examples-rl/pdf-password-bruteforce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/examples-rl/pdf-password-bruteforce.png -------------------------------------------------------------------------------- /docs/pages/examples-rl/recursive-compression.md: -------------------------------------------------------------------------------- 1 | # Recursive Compression 2 | 3 | ## Description 4 | 5 | This tool allows to recursively (de)compress nested archives according to various decompression algorithms, relying on the [`patool`](https://github.com/wummel/patool) module. 6 | 7 | ## Code 8 | 9 | See [this GitHub repository](https://github.com/dhondta/recursive-compression). 10 | 11 | ## Help 12 | 13 | ```session 14 | $ rec-comp -h 15 | usage: rec-comp [-c CHARSET] [-d] [-n NCHARS] [-r ROUNDS] [-h] [-p] [--stats] 16 | [--timings] [-v] 17 | files [files ...] 18 | 19 | RecComp v1.0 20 | Author : Alexandre D'Hondt 21 | Copyright: © 2019 A. D'Hondt 22 | License : GNU Affero General Public License v3.0 23 | 24 | This tool allows to recursively compress an archive relying on Patool, a Python 25 | library supporting various archive formats. 26 | 27 | Note: Password-protected compression is not supported. 28 | 29 | positional arguments: 30 | files files to be archived 31 | 32 | optional arguments: 33 | -c CHARSET character set of random archive name (default: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789) 34 | -d delete input files (default: False) 35 | -n NCHARS length of random archive name (default: 8) 36 | -r ROUNDS, --rounds ROUNDS 37 | number of compression rounds (default: 10) 38 | 39 | extra arguments: 40 | -h, --help show this help message and exit 41 | -p, --progress progress mode (default: False) 42 | -v, --verbose verbose mode (default: False) 43 | 44 | timing arguments: 45 | --stats display execution time stats at exit (default: False) 46 | --timings display time stats during execution (default: False) 47 | 48 | Usage examples: 49 | rec-comp file1 file2 file3 -r 10 50 | rec-comp file -c abcd -n 10 51 | rec-comp -p 52 | 53 | ``` 54 | 55 | ```session 56 | $ rec-decomp -h 57 | usage: rec-decomp [-d] [-p] [-h] [--stats] [--timings] [-v] archive 58 | 59 | RecDecomp v2.1 60 | Author : Alexandre D'Hondt 61 | Copyright: © 2019 A. D'Hondt 62 | License : GNU Affero General Public License v3.0 63 | Training : ZSIS CTF - Trivia - Shining (4 points) 64 | 65 | This tool allows to recursively decompress an archive relying on Patool, a 66 | Python library supporting various archive formats. 67 | 68 | Note: Password-protected compression is not supported yet. If the tool freezes 69 | while decompressing, it may be necessary to press enter to submit a blank 70 | password, which will stop decompression. 71 | 72 | positional arguments: 73 | archive input archive 74 | 75 | optional arguments: 76 | -d delete input archive (default: False) 77 | -p print resulting file, if possible (default: False) 78 | 79 | extra arguments: 80 | -h, --help show this help message and exit 81 | -v, --verbose verbose mode (default: False) 82 | 83 | timing arguments: 84 | --stats display execution time stats at exit (default: False) 85 | --timings display time stats during execution (default: False) 86 | 87 | Usage examples: 88 | rec-decomp archive.zip 89 | rec-decomp archive.zip -d 90 | 91 | ``` 92 | 93 | ## Execution 94 | 95 | ```session 96 | $ rec-comp file1 file2 file3 -r 10 -p 97 | 100%|██████████| 10/10 [00:05<00:00, 1.94it/s] 98 | 12:34:56 [INFO] Rounds: 10 99 | 12:34:56 [INFO] Archive: Vdpxp8Qy 100 | 101 | ``` 102 | 103 | ```session 104 | $ rec-decomp Vdpxp8Qy 105 | 12:34:56 [INFO] Rounds: 10 106 | 12:34:56 [INFO] Files : 107 | - file1 (8d5e08e1bbc49f59b208e0288e220ac0fc336ac0779852cb823c910ae03b5bc4) 108 | - file2 (9f07ec2f89cbec2696574d26238a2d876cfe1249909cc5de2f171ae9ede3e475) 109 | - file3 (60bf2a298af8b71b7fcc0e726c4f75d78c73949c9562cf0c1a2bbeadeeca8ee4) 110 | 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/pages/examples-rl/solitaire-cipher.md: -------------------------------------------------------------------------------- 1 | # Solitaire Cipher 2 | 3 | ## Description 4 | 5 | This tool implements Bruce Schneier's [Solitaire Cipher](https://www.schneier.com/academic/solitaire/). 6 | 7 | ## Code 8 | 9 | See [this Gist](https://gist.github.com/dhondta/1858f406fc55e5e5d440ff26432ad0a4). 10 | 11 | ## Help 12 | 13 | ```sh 14 | $ solitaire-cipher --help 15 | usage: solitaire-cipher [-r INI] [-w INI] [-h] [-v] {decrypt,encrypt} ... 16 | 17 | SolitaireCipher v1.1 18 | Author : Alexandre D'Hondt 19 | Copyright: © 2019 A. D'Hondt 20 | License : GNU Affero General Public License v3.0 21 | Reference: https://www.schneier.com/academic/solitaire/ 22 | 23 | This tool implements the Solitaire Encryption Algorithm of Bruce Schneier. 24 | 25 | positional arguments: 26 | {decrypt,encrypt} commands 27 | decrypt decrypt message 28 | encrypt encrypt message 29 | 30 | config arguments: 31 | -r INI, --read-config INI 32 | read args from a config file (default: None) 33 | NB: this overrides other arguments 34 | -w INI, --write-config INI 35 | write args to a config file (default: None) 36 | 37 | extra arguments: 38 | -h, --help show this help message and exit 39 | -v, --verbose verbose mode (default: False) 40 | 41 | Usage examples: 42 | solitaire-cipher encrypt "AAAAA AAAAA" -p my_super_secret -s 43 | solitaire-cipher decrypt "AAAAA AAAAA" -p my_super_secret -d deck.txt 44 | 45 | ``` 46 | 47 | ```sh 48 | $ solitaire-cipher encrypt --help 49 | usage: solitaire-cipher encrypt [-h] [-a A] [-b B] [-d DECK] -p PASSPHRASE 50 | [-o OUTPUT] [-s] 51 | message 52 | 53 | positional arguments: 54 | message message to be handled 55 | 56 | optional arguments: 57 | -a A joker A (default: 53) 58 | -b B joker B (default: 54) 59 | -d DECK deck file or list of integers (default: 1,2,...,53,54) 60 | -p PASSPHRASE passphrase (default: None) 61 | -o OUTPUT save the encoded deck to (default: deck.txt) 62 | -s shuffle the deck (default: False) 63 | 64 | extra arguments: 65 | -h, --help show this help message and exit 66 | 67 | ``` 68 | 69 | ```sh 70 | $ solitaire-cipher decrypt --help 71 | usage: solitaire-cipher decrypt [-h] [-a A] [-b B] [-d DECK] -p PASSPHRASE 72 | message 73 | 74 | positional arguments: 75 | message message to be handled 76 | 77 | optional arguments: 78 | -a A joker A (default: 53) 79 | -b B joker B (default: 54) 80 | -d DECK deck file or list of integers (default: 1,2,...,53,54) 81 | -p PASSPHRASE passphrase (default: None) 82 | 83 | extra arguments: 84 | -h, --help show this help message and exit 85 | 86 | ``` 87 | 88 | ## Execution 89 | 90 | ```sh 91 | $ solitaire-cipher encrypt "TEST" -s -p my_super_secret 92 | 12:34:56 [INFO] IWEJ 93 | 12:34:56 [INFO] 28,48,10,24,3,23,2,38,34,6,30,40,8,4,9,11,15,20,31,47,22,35,45,41,49,43,5,13,25,39,19,12,37,33,36,7,16,B,46,29,50,42,26,1,21,A,17,51,14,27,18,44,32,52 94 | 12:34:56 [INFO] Saved the encoded deck to 'deck.txt' 95 | 96 | ``` 97 | 98 | ```sh 99 | $ solitaire-cipher decrypt "IWEJ" -d deck.txt -p my_super_secret 100 | 12:34:56 [INFO] TEST 101 | 12:34:56 [INFO] 28,48,10,24,3,23,2,38,34,6,30,40,8,4,9,11,15,20,31,47,22,35,45,41,49,43,5,13,25,39,19,12,37,33,36,7,16,B,46,29,50,42,26,1,21,A,17,51,14,27,18,44,32,52 102 | 103 | ``` 104 | -------------------------------------------------------------------------------- /docs/pages/examples-rl/solitaire-cipher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/examples-rl/solitaire-cipher.png -------------------------------------------------------------------------------- /docs/pages/examples-rl/stegolsb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/examples-rl/stegolsb.png -------------------------------------------------------------------------------- /docs/pages/examples-rl/stegopit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/examples-rl/stegopit.png -------------------------------------------------------------------------------- /docs/pages/examples-rl/stegopvd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/examples-rl/stegopvd.png -------------------------------------------------------------------------------- /docs/pages/examples/demo.md: -------------------------------------------------------------------------------- 1 | # Demo Feature 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating the demonstration feature, adding an option to start a demo using one of the available examples defined in `__examples__`. 6 | 7 | !!! note "No example provided" 8 | 9 | If `__examples__` is not defined or is en empty list, the `--demo` will simply not be available, even if `add_demo` is `True` in the `initialize` function. 10 | 11 | ## Code 12 | 13 | ```python hl_lines="3 7" 14 | from tinyscript import * 15 | 16 | __examples__ = ["", "--test"] 17 | 18 | if __name__ == '__main__': 19 | parser.add_argument("--test", action="store_true", help="test argument") 20 | initialize(add_demo=True) 21 | logger.success("First example" if not args.test else "Second example") 22 | ``` 23 | 24 | ## Help 25 | 26 | ```sh 27 | $ python demo.py -h 28 | usage: python demo.py [--test] [--demo] [-h] [--help] [-v] 29 | 30 | ``` 31 | 32 | ## Execution 33 | 34 | ```sh hl_lines="1" 35 | $ python demo.py --demo 36 | 12:34:56 [SUCCESS] First example 37 | ``` 38 | 39 | ```sh hl_lines="1" 40 | $ python demo.py --demo 41 | 12:34:56 [SUCCESS] Second example 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/pages/examples/hotkeys.md: -------------------------------------------------------------------------------- 1 | # Hotkeys 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating the hotkeys feature. 6 | 7 | ## Code 8 | 9 | ```python hl_lines="3 4 5 6 7 9 10" 10 | from tinyscript import * 11 | 12 | HOTKEYS = ("default", { 13 | 'p': ("TEST", logger.info), 14 | # this overrides the default handler for 'q' (for exiting) 15 | 'q': lambda: q_handler(), 16 | }) 17 | 18 | def q_handler(): 19 | return "A computed string", logger.warning 20 | 21 | if __name__ == '__main__': 22 | initialize() 23 | while True: 24 | pass 25 | ``` 26 | 27 | ## Help 28 | 29 | ```sh 30 | $ python hotkeys.py -h 31 | usage: python hotkeys.py [-h] [--help] [-v] 32 | 33 | ``` 34 | 35 | ## Execution 36 | 37 | During this execution, the following keys are pressed: `p`, `q`, `l`, `i`, `Enter`, `i`, `y`, `Enter`. 38 | 39 | ```sh hl_lines="1" 40 | $ python hotkeys.py 41 | 12:34:56 [INFO] TEST 42 | 12:34:57 [WARNING] A computed string 43 | 12:34:58 [WARNING] A computed string 44 | Do you really want to interrupt ? {(y)es|(n)o} [n] 45 | 12:34:59 [WARNING] A computed string 46 | Do you really want to interrupt ? {(y)es|(n)o} [n] y 47 | 48 | ``` 49 | 50 | From the default handlers: 51 | 52 | - `l` displays the last log record again. 53 | - `i` asks for interrupting the execution. 54 | -------------------------------------------------------------------------------- /docs/pages/examples/metadata.md: -------------------------------------------------------------------------------- 1 | # Metadata 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating how to include metadata and how it is rendered in the help message. 6 | 7 | ## Code 8 | 9 | ```python hl_lines="3 4 5 6 7" 10 | from tinyscript import * 11 | 12 | __author__ = "John Doe" 13 | __email__ = "john.doe@example.com" 14 | __version__ = "1.0" 15 | __examples__ = [""] 16 | __doc__ = "This script prints a simple 'Hello world !'" 17 | 18 | if __name__ == '__main__': 19 | initialize() 20 | logger.success("Hello world !") 21 | ``` 22 | 23 | ## Help 24 | 25 | ```sh hl_lines="5 7" 26 | $ python metadata.py -h 27 | Tool 1.0 28 | Author : John Doe (john.doe@example.com) 29 | 30 | This script prints a simple 'Hello world !' 31 | 32 | usage: python metadata.py [-h] [--help] [-v] 33 | 34 | extra arguments: 35 | -h show usage message and exit 36 | --help show this help message and exit 37 | -v, --verbose verbose mode (default: False) 38 | 39 | Usage example: 40 | python metadata.py 41 | 42 | ``` 43 | 44 | ## Execution 45 | 46 | ```sh 47 | $ python metadata.py 48 | 12:34:56 [SUCCESS] Hello world ! 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/pages/examples/multi-level-debug.md: -------------------------------------------------------------------------------- 1 | # Multi-Level Debugging 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating the multi-level debug mode. 6 | 7 | ## Code 8 | 9 | ```python hl_lines="4" 10 | from tinyscript import * 11 | 12 | if __name__ == '__main__': 13 | initialize(multi_level_debug=True) 14 | logger.critical("This is always displayed") 15 | logger.error("This is always displayed") 16 | logger.warning("This is displayed with -v") 17 | logger.info("This is displayed with -vv") 18 | logger.debug("This is displayed with -vvv") 19 | ``` 20 | 21 | ## Help 22 | 23 | ```sh hl_lines="8 9" 24 | $ python multi-level-debug.py --help 25 | Tool 26 | 27 | usage: python multi-level-debug.py [-h] [--help] [-v] 28 | 29 | extra arguments: 30 | -h show usage message and exit 31 | --help show this help message and exit 32 | -v verbose level (default: 0) 33 | NB: -vvv is the highest verbosity level 34 | 35 | ``` 36 | 37 | ## Execution 38 | 39 | ```sh hl_lines="1" 40 | $ python multi-level-debug.py 41 | 12:34:56 [CRITICAL] This is always displayed 42 | 12:34:56 [ERROR] This is always displayed 43 | ``` 44 | 45 | ```sh hl_lines="1" 46 | $ python multi-level-debug.py -v 47 | 12:34:56 [CRITICAL] This is always displayed 48 | 12:34:56 [ERROR] This is always displayed 49 | 12:34:56 [WARNING] This is displayed with -v 50 | ``` 51 | 52 | ```sh hl_lines="1" 53 | $ python multi-level-debug.py -vv 54 | 12:34:56 [CRITICAL] This is always displayed 55 | 12:34:56 [ERROR] This is always displayed 56 | 12:34:56 [WARNING] This is displayed with -v 57 | 12:34:56 [INFO] This is displayed with -vv 58 | ``` 59 | 60 | ```sh hl_lines="1" 61 | $ python multi-level-debug.py -vvv 62 | 12:34:56 [CRITICAL] This is always displayed 63 | 12:34:56 [ERROR] This is always displayed 64 | 12:34:56 [WARNING] This is displayed with -v 65 | 12:34:56 [INFO] This is displayed with -vv 66 | 12:34:56 [DEBUG] This is displayed with -vvv 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/pages/examples/multi-level-help.md: -------------------------------------------------------------------------------- 1 | # Multi-Level Help 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating the multi-level help messages. 6 | 7 | ## Code 8 | 9 | ```python hl_lines="7 8 9 10" 10 | from tinyscript import * 11 | 12 | __doc__ = "Main description" 13 | __details__ = [ 14 | "First level of details", 15 | "Second level of details", 16 | ] 17 | 18 | if __name__ == '__main__': 19 | initialize() 20 | ``` 21 | 22 | ## Help 23 | 24 | ```sh hl_lines="1 4" 25 | $ python multi-level-help.py -h 26 | Tool 27 | 28 | Main description 29 | 30 | usage: ./tool.py [-h] [-v] 31 | 32 | extra arguments: 33 | -h show extended help message and exit (default: 0) 34 | NB: -hhh is the highest help detail level 35 | -v, --verbose verbose mode (default: False) 36 | 37 | ``` 38 | 39 | ```sh hl_lines="1 7" 40 | $ python multi-level-help.py -hh 41 | Tool 42 | [...] 43 | -v, --verbose verbose mode (default: False) 44 | 45 | 46 | First level of details 47 | 48 | ``` 49 | 50 | ```sh hl_lines="1 7 9" 51 | $ python multi-level-help.py -hh 52 | Tool 53 | [...] 54 | -v, --verbose verbose mode (default: False) 55 | 56 | 57 | First level of details 58 | 59 | Second level of details 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/pages/examples/simple-script.md: -------------------------------------------------------------------------------- 1 | # Simple Script 2 | 3 | ## Description 4 | 5 | Very basic script, with no particular feature used. 6 | 7 | ## Code 8 | 9 | ```python 10 | #!/usr/bin/python3 11 | # -*- coding: UTF-8 -*- 12 | from tinyscript import * 13 | 14 | 15 | if __name__ == '__main__': 16 | parser.add_argument("string", help="string to be displayed") 17 | initialize() 18 | logger.info(args.string) 19 | ``` 20 | 21 | ## Help 22 | 23 | ```sh 24 | $ python simple-script.py -h 25 | SimpleScript 26 | 27 | usage: simple-script [-h] [--help] [-v] string 28 | 29 | positional arguments: 30 | string string to be displayed 31 | 32 | extra arguments: 33 | -h show usage message and exit 34 | --help show this help message and exit 35 | -v, --verbose verbose mode (default: False) 36 | 37 | ``` 38 | 39 | ## Execution 40 | 41 | ```sh 42 | $ python simple-script.py "Hello World!" 43 | 12:34:56 [INFO] Hello World! 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/pages/examples/simple-tool.md: -------------------------------------------------------------------------------- 1 | # Simple Tool 2 | 3 | ## Description 4 | 5 | Very basic tool, using the demonstration feature when no argument is given. It also redefines a constant for tuning the logging. 6 | 7 | ## Code 8 | 9 | ```python 10 | #!/usr/bin/python3 11 | # -*- coding: UTF-8 -*- 12 | __author__ = "John Doe" 13 | __version__ = "1.0" 14 | __copyright__ = "AGPLv3 (http://www.gnu.org/licenses/agpl.html)" 15 | __reference__ = "John's Blog (http://blogsite.com/john/)" 16 | __doc__ = """ 17 | This tool is a simple example from the Tinyscript project. 18 | """ 19 | __examples__ = ["\"Hello World!\"", "\"Hello World!\" --critical"] 20 | 21 | # --------------------- IMPORTS SECTION --------------------- 22 | from tinyscript import * 23 | 24 | # -------------------- CONSTANTS SECTION -------------------- 25 | DATE_FORMAT = "%Y" 26 | 27 | # -------------------- FUNCTIONS SECTION -------------------- 28 | def hello(message, critical=False): 29 | (logger.info if not critical else logger.critical)(message) 30 | 31 | # ---------------------- MAIN SECTION ----------------------- 32 | if __name__ == '__main__': 33 | parser.add_argument("message", help="message to be displayed") 34 | parser.add_argument("--critical", action="store_true", help="critical message") 35 | initialize(noargs_action="demo") 36 | hello(args.message, args.critical) 37 | ``` 38 | 39 | ## Help 40 | 41 | ```sh 42 | $ python simple-tool.py -h 43 | SimpleTool v1.0 44 | Author: John Doe 45 | Reference: John's Blog (http://blogsite.com/john/) 46 | 47 | This tool is a simple example from the Tinyscript project. 48 | 49 | usage: python simple-tool.py [--critical] [-h] [--help] [-v] message 50 | 51 | positional arguments: 52 | message message to be displayed 53 | 54 | optional arguments: 55 | --critical critical message (default: False) 56 | 57 | extra arguments: 58 | -h show usage message and exit 59 | --help show this help message and exit 60 | -v, --verbose verbose mode (default: False) 61 | 62 | Usage examples: 63 | python simple-tool.py "Hello World!" 64 | 65 | ``` 66 | 67 | ## Execution 68 | 69 | ```sh 70 | $ python simple-tool.py 71 | 2018 [INFO] Hello World! 72 | 73 | $ python simple-tool.py 74 | 2018 [INFO] Hello World! 75 | 76 | $ python simple-tool.py 77 | 2018 [CRITICAL] Hello World! 78 | 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/pages/examples/step.md: -------------------------------------------------------------------------------- 1 | # Step Feature 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating the execution stepping feature. 6 | 7 | ## Code 8 | 9 | ```python hl_lines="7" 10 | from tinyscript import * 11 | 12 | if __name__ == '__main__': 13 | initialize(add_step=True) 14 | step("Pause 1") 15 | print("First computation") 16 | with Step("Pause 2"): 17 | print("Second computation") 18 | ``` 19 | 20 | ## Help 21 | 22 | ```sh 23 | $ python step.py -h 24 | usage: python step.py [-h] [--help] [--step] [-v] 25 | 26 | ``` 27 | 28 | ## Execution 29 | 30 | ```sh hl_lines="1" 31 | $ python step.py 32 | First computation 33 | Second computation 34 | 35 | ``` 36 | 37 | ```sh hl_lines="1" 38 | $ python step.py --step 39 | 12:34:56 [STEP] Pause 1 40 | Press enter to continue 41 | First computation 42 | 12:34:59 [STEP] Pause 2 43 | Press enter to continue 44 | Second computation 45 | 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/pages/examples/sudo.md: -------------------------------------------------------------------------------- 1 | # Privilege Escalation 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating running a script with sudo. 6 | 7 | ## Code 8 | 9 | ```python hl_lines="4" 10 | from tinyscript import * 11 | 12 | if __name__ == '__main__': 13 | initialize(sudo=True) 14 | logger.success("Do it as sudo !") 15 | ``` 16 | 17 | ## Help 18 | 19 | ```sh 20 | $ python sudo.py -h 21 | usage: python sudo.py [-h] [--help] [-v] 22 | 23 | ``` 24 | 25 | ## Execution 26 | 27 | ```sh hl_lines="2" 28 | $ python sudo.py 29 | [sudo] password for user: 30 | 12:34:56 [SUCCESS] Do it as sudo ! 31 | 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/pages/examples/timing.md: -------------------------------------------------------------------------------- 1 | # Timing Feature 2 | 3 | ## Description 4 | 5 | Very basic script demonstrating the execution timing feature. 6 | 7 | ## Code 8 | 9 | ```python hl_lines="7" 10 | from tinyscript import * 11 | 12 | if __name__ == '__main__': 13 | initialize(add_time=True) 14 | with Timer(timeout=1): 15 | while True: 16 | pass 17 | with Timer(timeout=2, fail_on_timeout=True): 18 | while True: 19 | pass 20 | ``` 21 | 22 | ## Help 23 | 24 | ```sh 25 | $ python step.py -h 26 | Tool 27 | 28 | usage: python tool.py [-h] [--help] [--stats] [--timings] [-v] 29 | 30 | extra arguments: 31 | -h show usage message and exit 32 | --help show this help message and exit 33 | -v, --verbose verbose mode (default: False) 34 | 35 | timing arguments: 36 | --stats display execution time stats at exit (default: False) 37 | --timings display time stats during execution (default: False) 38 | 39 | ``` 40 | 41 | ## Execution 42 | 43 | ```sh hl_lines="1" 44 | $ python tool.py --timings 45 | 12:34:56 [TIME] #0 46 | 12:34:57 [TIME] > Time elapsed: 1.00005912781 seconds 47 | 12:34:57 [TIME] > Time elapsed since execution start: 1.00027704239 seconds 48 | 12:34:57 [TIME] #1 49 | 12:34:59 [TIME] > Time elapsed: 2.00003600121 seconds 50 | Traceback (most recent call last): 51 | [...] 52 | tinyscript.helpers.timeout.TimeoutError: Timer expired 53 | 54 | ``` 55 | 56 | !!! note "Timer expiration" 57 | 58 | The `TimeoutError` exception is raised because of `fail_on_timeout=True` in the second timer. 59 | 60 | ```sh hl_lines="1" 61 | $ python tool.py --stats 62 | Traceback (most recent call last): 63 | [...] 64 | tinyscript.helpers.timeout.TimeoutError: Timer expired 65 | 12:34:59 [TIME] Total time: 3.00039601326 seconds 66 | #0 67 | > 1.000041008 seconds 68 | #1 69 | > 2.00003600121 seconds 70 | 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/pages/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/img/icon.png -------------------------------------------------------------------------------- /docs/pages/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/docs/pages/img/logo.png -------------------------------------------------------------------------------- /docs/pages/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Tinyscript aims to quickly prototype tools by sparing as much lines of code as possible and providing base features (especially useful for debugging or understanding the execution flow) like configured logging, preimports, stepping, timing and so forth. 4 | 5 | The idea is to make creating tools as easy as this: 6 | 7 | ```sh 8 | $ tinyscript new test && gedit test.py 9 | ``` 10 | 11 | Simply modifying the template to: 12 | 13 | ```python 14 | #!/usr/bin/env python 15 | # -*- coding: UTF-8 -*- 16 | from tinyscript import * 17 | 18 | __author__ = "John Doe" 19 | __version__ = "1.0" 20 | 21 | if __name__ == '__main__': 22 | parser.add_argument("string", help="string to be displayed") 23 | initialize() 24 | logger.info(args.string) 25 | ``` 26 | 27 | Will give the following: 28 | 29 | ```sh 30 | $ python test.py --help 31 | Test 1.0 32 | Author : John Doe 33 | 34 | usage: python test.py [-h] [--help] [-v] string 35 | 36 | positional arguments: 37 | string string to be displayed 38 | 39 | 40 | extra arguments: 41 | -h show usage message and exit 42 | --help show this help message and exit 43 | -v, --verbose verbose mode (default: False) 44 | 45 | $ python test.py "Hello World!" 46 | 01:02:03 [INFO] Hello World! 47 | ``` 48 | 49 | ----- 50 | 51 | ## Setup 52 | 53 | This library is available on [PyPi](https://pypi.python.org/pypi/tinyscript/) and can be simply installed using Pip: 54 | 55 | ```sh 56 | pip install --user tinyscript 57 | ``` 58 | 59 | or 60 | 61 | ```sh 62 | pip3 install --user tinyscript 63 | ``` 64 | 65 | ----- 66 | 67 | ## Rationale 68 | 69 | This library is born from the need of efficiently building tools without caring for redefining various things or rewriting/setting the same functionalities like logging or parsing of input arguments. 70 | 71 | In the meantime, I personnally used this library many times to create tools for my daily job or during cybersecurity or programming competitions and it proved very useful when dealing with rapid development. 72 | 73 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2<3.1.0 2 | mkdocs>=1.3.0 3 | mkdocs-bootswatch 4 | mkdocs-material 5 | mkdocs-rtd-dropdown 6 | pymdown-extensions 7 | -------------------------------------------------------------------------------- /docs/scripts.list: -------------------------------------------------------------------------------- 1 | # Scripts from author's Gists (source: https://raw.githubusercontent.com/dhondta/python-tinyscript/main/docs/scripts.list) 2 | https://gist.githubusercontent.com/dhondta/8b3c7d95b056cae3505df853a098fc4f/raw/audio-assembler.py 3 | https://gist.githubusercontent.com/dhondta/358393ce9ffed7b86254afd730174780/raw/clippercms-shell-uploader.py 4 | https://gist.githubusercontent.com/dhondta/c78062ac1a6be3122d8b3226896a231d/raw/craftcms-seomatic-ssti.py 5 | https://gist.githubusercontent.com/dhondta/5cae9533240471eac155bd51593af2e0/raw/doc-text-masker.py 6 | https://gist.githubusercontent.com/dhondta/0224d42a6f9dde00247ff8646f4e89aa/raw/evil-pickle-maker.py 7 | https://gist.githubusercontent.com/dhondta/54238677f1979c137a90a6da351f9337/raw/find-organization-by-oui-or-mac.py|oui 8 | https://gist.githubusercontent.com/dhondta/2e4946f791e5860bdb588d452b5b1570/raw/firefox_decrypt_modified.py 9 | https://gist.githubusercontent.com/dhondta/9a8027062ff770b2aa5d8422ddd78b57/raw/get-email-origin.py 10 | https://gist.githubusercontent.com/dhondta/7511710facb5eecc575e133ec60ed87c/raw/git-web-recovery.py 11 | https://gist.githubusercontent.com/dhondta/8937374f087f708c608bcacac431969f/raw/loose-comparison-input-generator.py 12 | https://gist.githubusercontent.com/dhondta/90a07d9d106775b0cd29bb51ffe15954/raw/paddinganograph.py 13 | https://gist.githubusercontent.com/dhondta/efe84a92e4dfae3b6c14932c73ab2577/raw/pdf-password-bruteforcer.py 14 | https://gist.githubusercontent.com/dhondta/f57dfde304905644ca5c43e48c249125/raw/pdf-preview-generator.py 15 | https://gist.githubusercontent.com/dhondta/f47b63d36decf151e5cf23abba613b63/raw/pta-downloader.py 16 | https://gist.githubusercontent.com/dhondta/1858f406fc55e5e5d440ff26432ad0a4/raw/solitaire-cipher.py 17 | https://gist.githubusercontent.com/dhondta/d2151c82dcd9a610a7380df1c6a0272c/raw/stegolsb.py 18 | https://gist.githubusercontent.com/dhondta/30abb35bb8ee86109d17437b11a1477a/raw/stegopit.py 19 | https://gist.githubusercontent.com/dhondta/feaf4f5fb3ed8d1eb7515abe8cde4880/raw/stegopvd.py 20 | https://gist.githubusercontent.com/dhondta/ca5fb748957b1ec6f13418ac41c94d5b/raw/stix-reports-to-pdf.py 21 | https://gist.githubusercontent.com/dhondta/f47b63d36decf151e5cf23abba613b63/raw/video-compressor.py 22 | https://gist.githubusercontent.com/dhondta/82a7919f8aafc1393c37c2d0f06b77e8/raw/word-list-filter.py|wlf 23 | https://gist.githubusercontent.com/dhondta/6c133993e870fae79845a0e84e5cf15d/raw/wp-log-parser.py 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | pythonpath = ["src"] 7 | 8 | [tool.setuptools.dynamic] 9 | version = {attr = "tinyscript.__info__.__version__"} 10 | 11 | [tool.setuptools.packages.find] 12 | where = ["src"] 13 | 14 | [tool.setuptools.package-data] 15 | "*" = ["*.css", "*.png", "*.txt"] 16 | 17 | [project] 18 | name = "tinyscript" 19 | authors = [ 20 | {name="Alexandre D'Hondt", email="alexandre.dhondt@gmail.com"}, 21 | ] 22 | description = "Devkit for quickly building CLI tools with Python" 23 | license = {file = "LICENSE"} 24 | keywords = ["python", "development", "programming", "cli", "devkit"] 25 | requires-python = ">=3.8,<4" 26 | classifiers = [ 27 | "Development Status :: 5 - Production/Stable", 28 | "Environment :: Console", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 31 | "Programming Language :: Python :: 3", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | ] 34 | dependencies = [ 35 | "argcomplete>=3.0.8", 36 | "asciistuff>=1.3.1", 37 | "codext>=1.15.4", 38 | "coloredlogs", 39 | "colorful", 40 | "dateparser>=1.1.8", 41 | "dicttoxml", 42 | "fonttools>=4.43.0", # SNYK-PYTHON-FONTTOOLS-6133203 43 | "ipaddress>=1.0.23", 44 | "json2html", 45 | "lazy_object_proxy>=1.9.0", 46 | "markdown2>=2.4.0", 47 | "netaddr", 48 | "netifaces", 49 | "packaging", 50 | "patchy", 51 | "pathlib2", 52 | "pip>=24.0", 53 | "plyer>=2.0.0", 54 | "pydyf>=0.11.0", 55 | "pygments>=2.8.1", 56 | "pyminizip", 57 | "pynput", 58 | "pypandoc", 59 | "pypiwin32; sys_platform=='windows'", 60 | "python-magic", 61 | "python-slugify", 62 | "pyyaml>=5.3.1", 63 | "requests>=2.32.2", 64 | "rich", 65 | "setuptools>=70.2.0", 66 | "terminaltables", 67 | "toml", 68 | "tqdm", 69 | "virtualenv>=20.26.3", 70 | "weasyprint>=64.1", 71 | "xmltodict", 72 | ] 73 | dynamic = ["version"] 74 | 75 | [project.readme] 76 | file = "README.md" 77 | content-type = "text/markdown" 78 | 79 | [project.urls] 80 | documentation = "https://python-tinyscript.readthedocs.io/en/latest/?badge=latest" 81 | homepage = "https://github.com/dhondta/python-tinyscript" 82 | issues = "https://github.com/dhondta/python-tinyscript/issues" 83 | repository = "https://github.com/dhondta/python-tinyscript" 84 | 85 | [project.scripts] 86 | tsm = "tinyscript.__main__:main" 87 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_paths = src 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argcomplete>=3.0.8 2 | asciistuff>=1.3.1 3 | bitstring==4.0.2 4 | codext>=1.15.4 5 | coloredlogs 6 | colorful 7 | dateparser>=1.1.8 8 | dicttoxml 9 | fonttools>=4.43.0 # SNYK-PYTHON-FONTTOOLS-6133203 10 | ipaddress>=1.0.23 11 | json2html 12 | lazy_object_proxy>=1.9.0 13 | markdown2>=2.4.0 14 | netaddr 15 | netifaces 16 | packaging 17 | patchy 18 | pathlib2 19 | pip>=24.0 20 | plyer>=2.0.0 21 | pygments>=2.8.1 22 | pyminizip 23 | pynput 24 | pypandoc 25 | pypiwin32; sys_platform=='windows' 26 | python-magic 27 | python-slugify 28 | pyyaml>=5.3.1 29 | requests>=2.32.2 30 | rich 31 | setuptools>=70.2.0 32 | terminaltables 33 | toml 34 | tqdm 35 | virtualenv>=20.26.3 36 | weasyprint>=60.2 37 | xmltodict 38 | # Snyk false alarms 39 | pillow>=10.2.0 # solved with asciistuff>=1.3.0 40 | urllib3>=2.2.2 # solved with requests>=2.32.2 41 | zipp>=3.19.1 42 | -------------------------------------------------------------------------------- /src/tinyscript/VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.30.21 2 | -------------------------------------------------------------------------------- /src/tinyscript/__conf__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Deprecation warnings, for use when no backward-compatibility provided. 3 | 4 | """ 5 | import builtins as bi 6 | import warnings 7 | from importlib import import_module 8 | from inspect import currentframe 9 | from lazy_object_proxy import Proxy 10 | from functools import wraps 11 | 12 | warnings.filterwarnings("always") 13 | 14 | 15 | def deprecate(old, new=None): 16 | def _warn(o, n): 17 | msg = "'%s' has been deprecated" % o 18 | if new is not None: 19 | msg += ", please use '%s' instead" % n 20 | warnings.warn(msg, DeprecationWarning) 21 | # react differently in function of the input 22 | if isinstance(old, type(lambda: 0)): 23 | n = old.__name__ 24 | def _old(old): 25 | @wraps(old) 26 | def _wrapper(*a, **kw): 27 | _warn(n, new) 28 | return old(*a, **kw) 29 | return _wrapper 30 | currentframe().f_back.f_globals[n] = _old(old) 31 | else: 32 | _warn(old, new) 33 | 34 | 35 | bi.deprecate = deprecate 36 | bi.warn = warnings.warn 37 | 38 | bi.lazy_object = Proxy 39 | 40 | 41 | def lazy_load_module(module, relative=None, alias=None, preload=None, postload=None): 42 | """ Lazily load a module. """ 43 | alias = alias or module 44 | glob = currentframe().f_back.f_globals 45 | def _load(): 46 | if callable(preload): 47 | preload() 48 | glob[alias] = glob[module] = m = import_module(*((module, ) if relative is None else ("." + module, relative))) 49 | m.__name__ = alias or module 50 | if callable(postload): 51 | try: 52 | postload() 53 | except TypeError: 54 | postload(m) 55 | return m 56 | glob[alias] = glob[module] = m = Proxy(_load) 57 | return m 58 | bi.lazy_load_module = lazy_load_module 59 | 60 | 61 | def lazy_load_object(name, load_func, preload=None, postload=None): 62 | """ Lazily load an object. """ 63 | glob = currentframe().f_back.f_globals 64 | def _load(): 65 | if callable(preload): 66 | preload() 67 | glob[name] = o = load_func() 68 | try: 69 | o._instance = o 70 | except (AttributeError, TypeError): 71 | pass 72 | if callable(postload): 73 | try: 74 | postload() 75 | except TypeError: 76 | postload(o) 77 | return o 78 | glob[name] = o = Proxy(_load) 79 | return o 80 | bi.lazy_load_object = lazy_load_object 81 | 82 | -------------------------------------------------------------------------------- /src/tinyscript/__info__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Tinyscript package information. 3 | 4 | """ 5 | import os 6 | from datetime import datetime 7 | 8 | __y = str(datetime.now().year) 9 | __s = "2017" 10 | 11 | __author__ = "Alexandre D'Hondt" 12 | __copyright__ = "© {} A. D'Hondt".format([__y, __s + "-" + __y][__y != __s]) 13 | __email__ = "alexandre.dhondt@gmail.com" 14 | __license__ = "GPLv3+ (https://www.gnu.org/licenses/gpl-3.0.fr.html)" 15 | __source__ = "https://github.com/dhondta/python-tinyscript" 16 | 17 | with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f: 18 | __version__ = f.read().strip() 19 | 20 | -------------------------------------------------------------------------------- /src/tinyscript/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Initialization of Tinyscript package. 3 | 4 | """ 5 | # NB: disabled as too expensive to load while almost not used ; if the error mentioned hereafter appears again, this 6 | # will be addressed in a different way 7 | # this avoids an AssertionError due to pip being loaded before setuptools ; 8 | # see https://github.com/pypa/setuptools/issues/3044 9 | #import setuptools 10 | 11 | from .__conf__ import * 12 | 13 | from warnings import filterwarnings 14 | filterwarnings("ignore", "Setuptools is replacing distutils.") 15 | 16 | from .__info__ import __author__, __copyright__, __license__, __version__ 17 | 18 | from .features import * 19 | from .helpers import * 20 | from .parser import * 21 | from .preimports import * 22 | 23 | from .features import __features__ as _features 24 | from .helpers import __features__ as _helpers 25 | from .parser import __features__ as _parser 26 | from .preimports import __features__ as _preimports 27 | 28 | 29 | ts.__author__ = __author__ 30 | ts.__copyright__ = __copyright__ 31 | ts.__license__ = __license__ 32 | ts.__version__ = __version__ 33 | 34 | 35 | __all__ = _features + _helpers + _parser + _preimports 36 | 37 | -------------------------------------------------------------------------------- /src/tinyscript/features/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module containing Tinyscript's features. 3 | 4 | """ 5 | from .handlers import * 6 | from .hotkeys import * 7 | from .interact import * 8 | from .loglib import * 9 | from .notify import * 10 | from .progress import * 11 | from .step import * 12 | from .timing import * 13 | 14 | 15 | from .handlers import __features__ as _handlers 16 | from .loglib import __features__ as _loglib 17 | 18 | 19 | __features__ = _handlers + _loglib 20 | 21 | -------------------------------------------------------------------------------- /src/tinyscript/features/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining handlers and exit hook. 3 | 4 | """ 5 | import sys 6 | from signal import getsignal, signal, SIG_IGN, SIGINT, SIGTERM 7 | 8 | from ..helpers.constants import WINDOWS 9 | 10 | 11 | __features__ = ["at_exit", "at_graceful_exit", "at_interrupt", "at_terminate", "DisableSignals"] 12 | __all__ = ["_hooks"] + __features__ 13 | 14 | 15 | class DisableSignals(object): 16 | """ Context manager that disable signal handlers. 17 | 18 | :param signals: list of signal identifiers 19 | :param fail: whether execution should fail or not when a bad signal ID is encountered 20 | """ 21 | def __init__(self, *signals, **kwargs): 22 | self.__handlers = {} 23 | for s in signals: 24 | try: 25 | self.__handlers[s] = getsignal(s) 26 | except ValueError as e: 27 | if kwargs.get('fail', False): 28 | raise e 29 | 30 | def __enter__(self): 31 | for s in self.__handlers.keys(): 32 | signal(s, SIG_IGN) 33 | 34 | def __exit__(self, exc_type, exc_val, exc_tb): 35 | for s, h in self.__handlers.items(): 36 | signal(s, h) 37 | 38 | 39 | # https://stackoverflow.com/questions/9741351/how-to-find-exit-code-or-reason-when-atexit-callback-is-called-in-python 40 | class ExitHooks(object): 41 | sigint_actions = ["confirm", "continue", "exit"] 42 | def __init__(self): 43 | self.__sigint_action = "exit" 44 | self._orig_exit = sys.exit 45 | self.code = None 46 | self.exception = None 47 | sys.exit = self.exit 48 | self.resume() 49 | 50 | def exit(self, code=0): 51 | self.code = code 52 | self._orig_exit(code) 53 | 54 | def pause(self): 55 | self.state = "PAUSED" 56 | while self.state == "PAUSED": continue 57 | 58 | def quit(self, code=0): 59 | from ..helpers.inputs import user_input 60 | if self.__sigint_action == "confirm" and \ 61 | user_input("Do you really want to interrupt execution ?", ["(Y)es", "(N)o"], "y", style="bold") == "yes": 62 | self.__sigint_action = "exit" 63 | if self.state != "INTERRUPTED" or self.__sigint_action == "exit": 64 | self.exit(code) 65 | self.resume() 66 | 67 | def resume(self): 68 | self.state = "RUNNING" 69 | 70 | @property 71 | def sigint_action(self): 72 | return self.__sigint_action 73 | 74 | @sigint_action.setter 75 | def sigint_action(self, value): 76 | if value not in self.sigint_actions: 77 | raise ValueError("Bad interrupt action ; should be one of {}".format("|".join(self.sigint_actions))) 78 | self.__sigint_action = value 79 | 80 | _hooks = ExitHooks() 81 | 82 | 83 | def __interrupt_handler(*args): 84 | """ Interruption handler. 85 | 86 | :param signal: signal number 87 | :param stack: stack frame 88 | :param code: exit code 89 | """ 90 | _hooks.state = "INTERRUPTED" 91 | _hooks.quit(0) 92 | # bind to interrupt signal (Ctrl+C) 93 | signal(SIGINT, __interrupt_handler) 94 | 95 | 96 | def __pause_handler(*args): 97 | """ Execution pause handler. """ 98 | _hooks.pause() 99 | if not WINDOWS: 100 | from signal import siginterrupt, SIGUSR1 101 | # bind to user-defined signal 102 | signal(SIGUSR1, __pause_handler) 103 | siginterrupt(SIGUSR1, False) 104 | 105 | 106 | def __terminate_handler(*args): 107 | """ Termination handler. 108 | 109 | :param signal: signal number 110 | :param stack: stack frame 111 | :param code: exit code 112 | """ 113 | _hooks.state = "TERMINATED" 114 | _hooks.quit(0) 115 | # bind to terminate signal 116 | signal(SIGTERM, __terminate_handler) 117 | 118 | 119 | at_exit = lambda: None 120 | at_graceful_exit = lambda: None 121 | at_interrupt = lambda: None 122 | at_terminate = lambda: None 123 | 124 | -------------------------------------------------------------------------------- /src/tinyscript/features/hotkeys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining hotkeys. 3 | 4 | """ 5 | from os import getpid, kill 6 | from signal import SIGINT, SIGTERM 7 | 8 | from .handlers import _hooks 9 | from ..helpers.constants import WINDOWS 10 | from ..helpers.inputs import confirm, hotkeys 11 | from ..preimports import logging 12 | 13 | 14 | __all__ = ["set_hotkeys"] 15 | 16 | 17 | HOTKEYS = None 18 | 19 | 20 | def __confirm_sig(prompt, sig): 21 | def _wrapper(): 22 | if not WINDOWS: 23 | from signal import SIGUSR1 24 | kill(getpid(), SIGUSR1) 25 | if confirm(prompt): 26 | kill(getpid(), sig) 27 | else: 28 | #FIXME: SIGUSR1 does not exist in Windows ; find a way to pause execution while prompting 29 | while not confirm(prompt): 30 | continue 31 | _hooks.resume() 32 | return _wrapper 33 | 34 | 35 | BASE_HOTKEYS = { 36 | 'i': __confirm_sig("Do you really want to interrupt ?", SIGINT), 37 | 'l': logging.lastLogRecord, 38 | 'q': __confirm_sig("Do you really want to quit ?", SIGTERM), 39 | } 40 | 41 | 42 | def set_hotkeys(glob): 43 | """ This function registers the hotkeys got from the global scope. 44 | 45 | :param glob: main script's global scope dictionary reference 46 | """ 47 | k = glob.get('HOTKEYS', HOTKEYS) 48 | # case 1: no hotkey to be configured 49 | if k is None: 50 | return 51 | # case 2: only the default hotkeys 52 | elif k == "default": 53 | return hotkeys(BASE_HOTKEYS) 54 | # case 3: only user-defined keys 55 | elif isinstance(k, dict): 56 | return hotkeys(k) 57 | # case 4: default hotkeys and user-defined keys mix 58 | elif isinstance(k, tuple) and len(k) == 2 and "default" in k: 59 | r = {} 60 | for hk in k: 61 | if hk == "default": 62 | hk = BASE_HOTKEYS 63 | for key, actions in hk.items(): 64 | r[key] = actions 65 | return hotkeys(r) 66 | raise ValueError("Invalid HOTKEYS dictionary") 67 | 68 | -------------------------------------------------------------------------------- /src/tinyscript/features/interact.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining interactive mode logic. 3 | 4 | """ 5 | import readline 6 | import socket 7 | import sys 8 | from code import compile_command, interact as base_interact, InteractiveConsole as BaseInteractiveConsole 9 | 10 | 11 | __all__ = ["set_interact_items"] 12 | 13 | 14 | def set_interact_items(glob): 15 | """ This function prepares the interaction items for inclusion in main script's global scope. 16 | 17 | :param glob: main script's global scope dictionary reference 18 | """ 19 | a, l = glob['args'], glob['logger'] 20 | enabled = getattr(a, a._collisions.get("interact") or "interact", False) 21 | if enabled: 22 | readline.parse_and_bind('tab: complete') 23 | 24 | # InteractiveConsole as defined in the code module, but handling a banner using the logging of tinyscript 25 | class InteractiveConsole(BaseInteractiveConsole, object): 26 | def __init__(self, banner=None, namespace=None, filename='', exitmsg=None): 27 | if enabled: 28 | self.banner = banner 29 | self.exitmsg = exitmsg 30 | ns = glob 31 | ns.update(namespace or {}) 32 | super(InteractiveConsole, self).__init__(locals=ns, filename=filename) 33 | 34 | def __enter__(self): 35 | if enabled and self.banner is not None: 36 | l.interact(self.banner) 37 | return self 38 | 39 | def __exit__(self, *args): 40 | if enabled and self.exitmsg is not None: 41 | l.interact(self.exitmsg) 42 | 43 | def interact(self, *args, **kwargs): 44 | if enabled: 45 | super(InteractiveConsole, self).interact(*args, **kwargs) 46 | 47 | glob['InteractiveConsole'] = InteractiveConsole 48 | 49 | def interact(banner=None, readfunc=None, namespace=None, exitmsg=None): 50 | if enabled: 51 | if banner is not None: 52 | l.interact(banner) 53 | ns = glob 54 | ns.update(namespace or {}) 55 | base_interact(readfunc=readfunc, local=ns) 56 | if exitmsg is not None: 57 | l.interact(exitmsg) 58 | 59 | glob['interact'] = interact 60 | 61 | glob['compile_command'] = compile_command if enabled else lambda *a, **kw: None 62 | 63 | # ConsoleSocket for handling duplicating std*** to a socket for the RemoteInteractiveConsole 64 | host = getattr(a, a._collisions.get("host") or "host", None) 65 | port = getattr(a, a._collisions.get("port") or "port", None) 66 | 67 | # custom socket, for handling the bindings of stdXXX through a socket 68 | class ConsoleSocket(socket.socket): 69 | def readline(self, nbytes=2048): 70 | return self.recv(nbytes) 71 | 72 | def write(self, *args, **kwargs): 73 | return self.send(*args, **kwargs) 74 | 75 | # RemoteInteractiveConsole as defined in the code module, but handling interaction through a socket 76 | class RemoteInteractiveConsole(InteractiveConsole): 77 | def __init__(self, *args, **kwargs): 78 | if enabled: 79 | # open a socket 80 | self.socket = ConsoleSocket() 81 | self.socket.connect((str(host), port)) 82 | # save STDIN, STDOUT and STDERR 83 | self.__stdin = sys.stdin 84 | self.__stdout = sys.stdout 85 | self.__stderr = sys.stderr 86 | # rebind STDIN, STDOUT and STDERR to the socket 87 | sys.stdin = sys.stdout = sys.stderr = self.socket 88 | # now initialize the interactive console 89 | super(RemoteInteractiveConsole, self).__init__(*args, **kwargs) 90 | 91 | def __exit__(self, *args): 92 | if enabled: 93 | super(RemoteInteractiveConsole, self).__exit__(*args) 94 | self.socket.close() 95 | self.close() 96 | 97 | def close(self): 98 | if enabled: 99 | # restore STDIN, STDOUT and STDERR 100 | sys.stdin = self.__stdin 101 | sys.stdout = self.__stdout 102 | sys.stderr = self.__stderr 103 | 104 | glob['RemoteInteractiveConsole'] = RemoteInteractiveConsole 105 | 106 | -------------------------------------------------------------------------------- /src/tinyscript/features/loglib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining logging-related constants and objects. 3 | 4 | """ 5 | import coloredlogs 6 | 7 | from ..preimports import logging 8 | 9 | 10 | __features__ = ["LOG_FORMAT", "DATE_FORMAT", "TIME_MILLISECONDS", "logger"] 11 | __all__ = ["coloredlogs", "configure_logger"] + __features__ 12 | 13 | 14 | DATE_FORMAT = '%H:%M:%S' 15 | LOG_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 16 | TIME_MILLISECONDS = False 17 | 18 | 19 | # add a custom level beneath DEBUG 20 | logging.addLogLevel("detail", "green", 5, False) 21 | # add a custom log level for interactive mode 22 | logging.addLogLevel("interact", "cyan", 100) 23 | # add a custom log level for stepping 24 | logging.addLogLevel("step", "cyan", 101) 25 | # add a custom log level for timing 26 | logging.addLogLevel("time", "magenta", 102) 27 | # add a custom success log level 28 | logging.addLogLevel("success", "green", 103) 29 | # add a custom failure log level 30 | logging.addLogLevel("failure", "red", 104) 31 | 32 | 33 | # setup a default logger for allowing logging before initialize() is called 34 | logger = logging.getLogger("main") 35 | logger.setLevel(1) 36 | logger.addHandler(logging.InterceptionHandler()) 37 | coloredlogs.DEFAULT_LOG_FORMAT = LOG_FORMAT 38 | coloredlogs.DEFAULT_DATE_FORMAT = DATE_FORMAT 39 | coloredlogs.install(logger=logger) 40 | 41 | 42 | def configure_logger(glob, multi_level, relative=False, logfile=None, syslog=False): 43 | """ Logger configuration function for setting either a simple debug mode or a multi-level one. 44 | 45 | :param glob: globals dictionary 46 | :param multi_level: boolean telling if multi-level debug is to be considered 47 | :param relative: use relative time for the logging messages 48 | :param logfile: log file to be saved (None means do not log to file) 49 | :param syslog: enable logging to /var/log/syslog 50 | """ 51 | _l = logging 52 | levels = [_l.ERROR, _l.WARNING, _l.INFO, _l.DEBUG] if multi_level else [_l.INFO, _l.DEBUG] 53 | try: 54 | verbose = min(int(glob['args'].verbose), 3) 55 | except AttributeError: 56 | verbose = 0 57 | glob['args']._debug_level = dl = levels[verbose] 58 | glob['args']._debug_syslog = syslog 59 | glob['args']._debug_logfile = logfile 60 | glob['logger'] = logger 61 | # create the "last record" logger, used for reminding the last log record, i.e. with a shortcut key 62 | lastrec = logging.getLogger("__last_record__") 63 | kw = {'fmt': "\r" + glob.pop('LOG_FORMAT', LOG_FORMAT), 'datefmt': glob.pop('DATE_FORMAT', DATE_FORMAT)} 64 | h = lastrec.handlers[0] if len(lastrec.handlers) == 1 else logging.StreamHandler() 65 | if len(lastrec.handlers) != 1: 66 | for old_h in lastrec.handlers: 67 | lastrec.removeHandler(old_h) 68 | lastrec.addHandler(h) 69 | h.setFormatter(logging.Formatter(*kw.values())) 70 | lastrec.setLevel(1) 71 | coloredlogs.install(1, logger=lastrec, **kw) 72 | logging.configLogger(logger, dl, syslog=syslog, stream=logfile, relative=relative, 73 | milliseconds=glob.get('TIME_MILLISECONDS', TIME_MILLISECONDS), **kw) 74 | 75 | -------------------------------------------------------------------------------- /src/tinyscript/features/notify/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining the notify feature. 3 | 4 | """ 5 | from os.path import dirname, isfile, join 6 | 7 | from ...helpers.notify import notify 8 | from ...helpers.constants import LINUX 9 | from ...helpers.data.types import folder_exists, positive_int 10 | from ...preimports import logging 11 | 12 | 13 | __all__ = ["set_notify_items"] 14 | 15 | 16 | def set_notify_items(glob): 17 | """ 18 | This function prepares the notify items for inclusion in main script's global scope. 19 | 20 | :param glob: main script's global scope dictionary reference 21 | """ 22 | a = glob['args'] 23 | enabled = getattr(a, a._collisions.get("notify") or "notify", False) 24 | appname = glob.get('__banner__', glob.get('__script__', "my-app")) 25 | timeout = positive_int(glob.get('NOTIFICATION_TIMEOUT', 5), zero=False) 26 | icon_path = folder_exists(glob.get('NOTIFICATION_ICONS_PATH', dirname(__file__))) 27 | level = positive_int(glob.get('NOTIFICATION_LEVEL', logging.SUCCESS)) 28 | 29 | class NotificationHandler(logging.Handler): 30 | def emit(self, record): 31 | title = f"{appname}[{record.name}]:" if record.name != "main" else appname 32 | icon = record.levelname.lower() 33 | ipath = join(icon_path, icon) + ".png" 34 | if isfile(ipath): 35 | icon = ipath 36 | notify(title, record.msg, appname, icon, timeout, title + " " + record.levelname) 37 | 38 | if enabled and not any(type(h) is NotificationHandler for h in glob['logger'].handlers): 39 | nh = NotificationHandler() 40 | nh.setLevel(level) 41 | glob['logger'].addHandler(nh) 42 | glob['notify'] = notify 43 | 44 | -------------------------------------------------------------------------------- /src/tinyscript/features/notify/critical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/src/tinyscript/features/notify/critical.png -------------------------------------------------------------------------------- /src/tinyscript/features/notify/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/src/tinyscript/features/notify/debug.png -------------------------------------------------------------------------------- /src/tinyscript/features/notify/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/src/tinyscript/features/notify/error.png -------------------------------------------------------------------------------- /src/tinyscript/features/notify/failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/src/tinyscript/features/notify/failure.png -------------------------------------------------------------------------------- /src/tinyscript/features/notify/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/src/tinyscript/features/notify/info.png -------------------------------------------------------------------------------- /src/tinyscript/features/notify/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/src/tinyscript/features/notify/success.png -------------------------------------------------------------------------------- /src/tinyscript/features/notify/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhondta/python-tinyscript/2b9abf0533c993bd8ccdf546eb75f91b1a18e3db/src/tinyscript/features/notify/warning.png -------------------------------------------------------------------------------- /src/tinyscript/features/progress.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining progress mode logic. 3 | 4 | """ 5 | lazy_load_module("tqdm") 6 | 7 | 8 | __all__ = ["set_progress_items"] 9 | 10 | 11 | def set_progress_items(glob): 12 | """ This function prepares the progress items for inclusion in main script's global scope. 13 | 14 | :param glob: main script's global scope dictionary reference 15 | """ 16 | a = glob['args'] 17 | enabled = getattr(a, a._collisions.get("progress") or "progress", False) 18 | # Progress manager, for providing an interface to tqdm progress bar class 19 | class __ProgressManager(object): 20 | """ Simple progress bar manager, relying on tqdm module. """ 21 | def __init__(self): 22 | c = a._collisions 23 | self._tqdm = None 24 | 25 | def __getattr__(self, name): 26 | if enabled: 27 | try: 28 | return self.__getattribute__(name) 29 | except AttributeError: 30 | pass 31 | if hasattr(tqdm.tqdm, name) and self._tqdm is not None: 32 | return getattr(self._tqdm, name) 33 | raise AttributeError("ProgressManager instance has no attribute '{}'".format(name)) 34 | 35 | def range(self, *args, **kwargs): 36 | """ Dummy alias to trange. """ 37 | if enabled: 38 | self._tqdm = tqdm.trange(*args, **kwargs) 39 | return self._tqdm 40 | 41 | def start(self, *args, **kwargs): 42 | if enabled: 43 | self.stop() 44 | self._tqdm = tqdm.tqdm(*args, **kwargs) 45 | return self._tqdm 46 | 47 | def stop(self): 48 | """ Closing method. """ 49 | if enabled: 50 | if self._tqdm is not None: 51 | self._tqdm.close() 52 | self._tqdm = None 53 | glob['progress_manager'] = manager = __ProgressManager() 54 | # shortcut function to range-based progress bar 55 | def progressbar(*args, **kwargs): 56 | """ Range-based progress bar relying on tqdm. """ 57 | try: 58 | iter(args[0]) 59 | return manager.start(*args, **kwargs) 60 | except TypeError as te: 61 | return manager.range(*args, **kwargs) 62 | glob['progressbar'] = progressbar 63 | 64 | -------------------------------------------------------------------------------- /src/tinyscript/features/step.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining stepping mode logic. 3 | 4 | """ 5 | from ..helpers.inputs import std_input 6 | from ..preimports import logging 7 | 8 | 9 | __all__ = ["set_step_items"] 10 | 11 | 12 | COLOR = logging.STEP_COLOR 13 | 14 | 15 | def set_step_items(glob): 16 | """ This function prepares the stepping items for inclusion in main script's global scope. 17 | 18 | :param glob: main script's global scope dictionary reference 19 | """ 20 | a = glob['args'] 21 | l = glob['logger'] 22 | enabled = getattr(a, a._collisions.get("step") or "step", False) 23 | # Step context manager, for defining a block of code that can be paused at its start and end 24 | class Step(object): 25 | def __init__(self, message=None, at_end=False): 26 | self.message = message 27 | self.at_end = at_end 28 | 29 | def __enter__(self): 30 | if enabled: 31 | if self.message: 32 | l.step(self.message) 33 | if not self.at_end: 34 | std_input("Press enter to continue", ["bold", COLOR]) 35 | return self 36 | 37 | def __exit__(self, *args): 38 | if enabled and self.at_end: 39 | std_input("Press enter to continue", ["bold", COLOR]) 40 | glob['Step'] = Step 41 | # stepping function, for stopping the execution and displaying a message if any defined 42 | def step(message=None): 43 | if enabled: 44 | if message: 45 | l.step(message) 46 | std_input("Press enter to continue", ["bold", COLOR]) 47 | glob['step'] = step 48 | 49 | -------------------------------------------------------------------------------- /src/tinyscript/features/timing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining benchmark mode logic. 3 | 4 | """ 5 | import signal 6 | import time 7 | from errno import ETIME 8 | from os import strerror 9 | 10 | from .loglib import logger 11 | from ..helpers.constants import WINDOWS 12 | 13 | 14 | __all__ = ["set_time_items"] 15 | 16 | TO_MSG = strerror(ETIME) 17 | 18 | 19 | def set_time_items(glob): 20 | """ This function prepares the benchmark items for inclusion in main script's global scope. 21 | 22 | :param glob: main script's global scope dictionary reference 23 | """ 24 | a = glob['args'] 25 | l = glob['logger'] 26 | # Time manager, for keeping track of collected times 27 | class __TimeManager(object): 28 | """ Simple time manager, using time module. """ 29 | def __init__(self): 30 | c = a._collisions 31 | self._stats = getattr(a, c.get("stats") or "stats", False) 32 | self._timings = getattr(a, c.get("timings") or "timings", False) 33 | self.enabled = self._stats or self._timings 34 | self.last = self.start = time.time() 35 | self.times = [] 36 | 37 | def stats(self): 38 | end = time.time() 39 | b = "" 40 | for d, s, e in self.times: 41 | b += "\n{}\n> {} seconds".format(d, e - s) 42 | l.time("Total time: {} seconds{}".format(end - self.start, b)) 43 | glob['time_manager'] = manager = __TimeManager() 44 | # private function to keep time measure in the time manager 45 | def _take_time(start=None, descr=None): 46 | t = manager.last = time.time() 47 | if start is not None and descr is not None: 48 | manager.times.append((descr, float(start), float(t))) 49 | return t - (start or 0) 50 | # Time context manager, for easily benchmarking a block of code 51 | class Timer(object): 52 | def __init__(self, description=None, message=TO_MSG, timeout=None, fail_on_timeout=False, precision=True): 53 | self.fail = fail_on_timeout 54 | self.id = len(manager.times) 55 | self.descr = "#" + str(self.id) + (": " + (description or "")).rstrip(": ") 56 | self.message = message 57 | self.precision = precision 58 | self.timeout = timeout 59 | 60 | def __enter__(self): 61 | if manager.enabled: 62 | if self.timeout is not None: 63 | if WINDOWS: 64 | logger.warning("signal.SIGALRM does not exist in Windows ; timeout parameter won't work.") 65 | else: 66 | signal.signal(signal.SIGALRM, self._handler) 67 | signal.alarm(self.timeout) 68 | if manager._timings and self.descr: 69 | l.time(self.descr) 70 | self.start = _take_time() 71 | if self.precision: 72 | self.startp = time.perf_counter() 73 | return self 74 | 75 | def __exit__(self, exc_type, exc_value, exc_traceback): 76 | if manager.enabled: 77 | if self.precision: 78 | dt = time.perf_counter() - self.startp 79 | l.time("> Precise time elapsed: %.6f seconds"% dt) 80 | manager.times.append(("", 0, dt)) 81 | d = _take_time(self.start, self.descr) 82 | if manager._timings: 83 | l.time("> Time elapsed: %.2f seconds"% d) 84 | if self.timeout is not None: 85 | if not self.fail and exc_type is TimeoutError: 86 | return True # this allows to let the execution continue 87 | # implicitly returns None ; this lets the exception be raised 88 | 89 | def _handler(self, signum, frame): 90 | raise TimeoutError(self.message) 91 | 92 | glob['Timer'] = Timer 93 | # timing function for getting a measure from the start of the execution 94 | def get_time(message=None, start=manager.start): 95 | if manager._timings: 96 | l.time("> {}: {} seconds".format(message or "Time elapsed since execution start", _take_time(start))) 97 | glob['get_time'] = get_time 98 | # timing function for getting a measure since the last one 99 | def get_time_since_last(message=None): 100 | get_time(message or "Time elapsed since last measure", manager.last) 101 | glob['get_time_since_last'] = get_time_since_last 102 | 103 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module containing various helper functions. 3 | 4 | """ 5 | from pprint import pprint 6 | from types import ModuleType 7 | 8 | from .attack import * 9 | from .classprop import * 10 | from .common import * 11 | from .compat import * 12 | from .constants import * 13 | from .data import * 14 | from .decorators import * 15 | from .dictionaries import * 16 | from .docstring import * 17 | from .expressions import * 18 | from .fexec import * 19 | from .inputs import * 20 | from .layout import * 21 | from .licenses import * 22 | from .notify import * 23 | from .parser import * 24 | from .password import * 25 | from .path import * 26 | from .termsize import * 27 | from .text import * 28 | from .timeout import * 29 | 30 | from .attack import __features__ as _attack 31 | from .classprop import __features__ as _clsprop 32 | from .common import __features__ as _common 33 | from .compat import __features__ as _compat 34 | from .constants import __features__ as _csts 35 | from .data import __features__ as _data 36 | from .decorators import __features__ as _dec 37 | from .dictionaries import __features__ as _dict 38 | from .docstring import __features__ as _docs 39 | from .expressions import __features__ as _expr 40 | from .fexec import __features__ as _fexec 41 | from .inputs import __features__ as _inputs 42 | from .layout import __features__ as _layout 43 | from .licenses import __features__ as _lic 44 | from .notify import __features__ as _notify 45 | from .parser import __features__ as _parser 46 | from .password import __features__ as _pswd 47 | from .path import __features__ as _path 48 | from .termsize import __features__ as _tsize 49 | from .text import __features__ as _text 50 | from .timeout import __features__ as _to 51 | 52 | 53 | __helpers__ = _attack + _common + _data + _dec + _dict + _docs + _expr + _fexec + _inputs + _layout + _lic + _notify + \ 54 | _parser + _path + _pswd + _tsize + _text + _to 55 | 56 | ts = ModuleType("ts", """ 57 | Tinyscript helpers 58 | ~~~~~~~~~~~~~~~~~~ 59 | 60 | The `ts` module contains various helper functions that can be very useful 61 | to not reinvent the wheel, including: 62 | 63 | - Common utility functions 64 | 65 | Bruteforce generator, customized with minimum and maximum lengths and an 66 | alphabet. A dummy process execution function is also available. 67 | 68 | - Data-related functions 69 | 70 | This encompasses argparse argument types and data transformation functions 71 | i.e. from and to bin/int/hex. 72 | 73 | - Useful decorators 74 | 75 | I.e. for trying something while choosing a different action if the 76 | execution fails (e.g. simply passing, warning the user or interrupting the 77 | program). 78 | 79 | - Input functions 80 | 81 | This relates to functions expecting user inputs like a 'pause' function, 82 | a 'confirm' function (with a customizable message and set of answers), a 83 | 'user_input' function with styling, ... 84 | 85 | - Path-related helpers 86 | 87 | Some useful path functions, enhanced from pathlib2.Path's one. It adds 88 | various methods to Path and provides helpers for mirroring a path or 89 | creating a temporary one. 90 | 91 | - Timeout items 92 | 93 | This provides a timeout decorator and a timeout context manager. 94 | 95 | - Others 96 | 97 | Namely license-related functions, terminal size get function, ... 98 | """) 99 | for h in __helpers__: 100 | setattr(ts, h, globals()[h]) 101 | 102 | __all__ = __features__ = ["colored", "pprint", "ts"] + _clsprop + _compat + _csts 103 | 104 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/classprop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Class property. 3 | 4 | """ 5 | __all__ = __features__ = ["classproperty"] 6 | 7 | 8 | def classproperty(f): 9 | if not isinstance(f, (classmethod, staticmethod)): 10 | f = classmethod(f) 11 | return ClassProperty(f) 12 | 13 | 14 | # inspired from: https://stackoverflow.com/questions/5189699/how-to-make-a-class-property 15 | class ClassProperty(object): 16 | """ ClassProperty class for implementing @classproperty. """ 17 | def __init__(self, fget=None, fset=None, doc=None): 18 | self.fget = fget 19 | self.fset = fset 20 | self.__doc__ = doc or getattr(fget, "__doc__", None) 21 | 22 | def __get__(self, obj, objtype=None): 23 | objtype = objtype or type(obj) 24 | return self.fget.__get__(obj, objtype)() 25 | 26 | def setter(self, f): 27 | if not isinstance(f, (classmethod, staticmethod)): 28 | f = classmethod(f) 29 | self.fset = f 30 | return self 31 | 32 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Formerly common Python2/3 compatibility functions, now left for backward-compatibility. 3 | 4 | """ 5 | __all__ = __features__ = ["b", "binary_type", "byteindex", "execfile", "ensure_binary", "ensure_str", "iterbytes", 6 | "reduce", "string_types", "text_type", "u"] 7 | 8 | 9 | binary_type = bytes 10 | integer_types = (int, ) 11 | string_types = (str, ) 12 | text_type = u = str 13 | 14 | # see: http://python3porting.com/problems.html 15 | byteindex = lambda d, i=None: d[i] 16 | 17 | 18 | def b(s): 19 | try: 20 | return s.encode("latin-1") 21 | except: 22 | pass 23 | try: 24 | return s.encode("utf-8") 25 | except: 26 | pass 27 | return s 28 | 29 | 30 | def ensure_binary(s, encoding='utf-8', errors='strict'): 31 | if isinstance(s, str): 32 | return s.encode(encoding, errors) 33 | elif isinstance(s, bytes): 34 | return s 35 | else: 36 | raise TypeError("not expecting type '%s'" % type(s)) 37 | 38 | 39 | def ensure_str(s, encoding='utf-8', errors='strict'): 40 | if isinstance(s, bytes): 41 | try: 42 | return s.decode(encoding, errors) 43 | except: 44 | return s.decode("latin-1") 45 | elif not isinstance(s, (str, bytes)): 46 | raise TypeError("not expecting type '%s'" % type(s)) 47 | return s 48 | 49 | 50 | def execfile(source, globals=None, locals=None): 51 | with open(source) as f: 52 | content = f.read() 53 | exec(content, globals, locals) 54 | 55 | 56 | def iterbytes(text): 57 | """ Bytes iterator. If a string is provided, it will automatically be converted to bytes. """ 58 | if isinstance(text, str): 59 | text = b(text) 60 | for c in text: 61 | yield c 62 | 63 | 64 | _initial_missing = object() 65 | 66 | def reduce(function, sequence, initial=_initial_missing, stop=None): 67 | """ Similar to functools.reduce, but with a stop condition. 68 | reduce(function, sequence[, initial, stop]) -> value """ 69 | it = iter(sequence) 70 | try: 71 | value = next(it) if initial is _initial_missing else initial 72 | except StopIteration: 73 | raise TypeError("reduce() of empty sequence with no initial value") from None 74 | for element in it: 75 | v = function(value, element) 76 | if stop and stop(v): 77 | break 78 | value = v 79 | return value 80 | 81 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Common constants. 3 | 4 | """ 5 | import sys 6 | from getpass import getuser 7 | from locale import getlocale 8 | from platform import system 9 | 10 | 11 | __all__ = __features__ = ["DARWIN", "LINUX", "WINDOWS"] 12 | DARWIN = system() == "Darwin" 13 | LINUX = system() == "Linux" 14 | WINDOWS = system() == "Windows" 15 | 16 | __all__ += ["ADMIN", "USER"] 17 | USER = getuser() 18 | ADMIN = USER == ["root", "Administrator"][WINDOWS] 19 | 20 | __all__ += ["ENCODING", "LANGUAGE"] 21 | LANGUAGE, ENCODING = getlocale() 22 | 23 | __all__ += ["IPYTHON", "JUPYTER", "JYTHON", "PYPY", "TTY"] 24 | JYTHON = sys.platform.startswith("java") 25 | PYPY = hasattr(sys, "pypy_version_info") 26 | TTY = sys.stdout.isatty() 27 | try: 28 | __IPYTHON__ 29 | IPYTHON = JUPYTER = True 30 | except NameError: 31 | IPYTHON = JUPYTER = False 32 | 33 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/data/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Subpackage containing data-related helper functions. 3 | 4 | """ 5 | from .transform import * 6 | from .types import * 7 | from .utils import * 8 | 9 | from .transform import __features__ as _transform 10 | from .types import __features__ as _types 11 | from .utils import __features__ as _utils 12 | 13 | 14 | __all__ = __features__ = _transform + _types + _utils 15 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/data/transform/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Subpackage containing data transformation functions. 3 | 4 | """ 5 | from .common import * 6 | from .report import * 7 | 8 | from .common import __features__ as _common 9 | from .report import __features__ as _report 10 | 11 | 12 | __all__ = __features__ = _common + _report 13 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/data/transform/report.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Report data transformation functions. 3 | 4 | """ 5 | from ....preimports import inspect, re 6 | 7 | lazy_load_module("dicttoxml") 8 | lazy_load_module("json2html", alias="j2h") 9 | lazy_load_module("xmltodict") 10 | 11 | 12 | __all__ = __features__ = ["dict2html", "dict2xml", "json2html", "json2xml", "report2objects", "xml2dict", "xml2json"] 13 | 14 | 15 | json2html = dict2html = lazy_object(lambda: j2h.json2html.convert) 16 | json2xml = dict2xml = lazy_object(lambda: dicttoxml.dicttoxml) 17 | xml2json = xml2dict = lazy_object(lambda: xmltodict.parse) 18 | 19 | 20 | def report2objects(text, header_sep=None, footer_sep=None): 21 | """ Convert a raw text report (i.e. WPScan-like) to Tinyscript report objects. """ 22 | glob = inspect.currentframe().f_back.f_globals 23 | o = glob.get('Report', list)() 24 | if header_sep: 25 | parts = re.split("[" + re.escape(header_sep) + "]{10,}", text) 26 | if len(parts) > 1: 27 | header = parts.pop(0).strip() 28 | while len(parts) > 0 and header == "": 29 | header = parts.pop(0).strip() 30 | if header != "": 31 | Header = glob.get('Header', None) 32 | o.append(("Header", header) if Header is None else Header(header)) 33 | text = parts[0] 34 | if footer_sep: 35 | parts = re.split("[" + re.escape(footer_sep) + "]{10,}", text) 36 | if len(parts) > 1: 37 | footer = parts.pop().strip() 38 | while len(parts) > 0 and footer == "": 39 | footer = parts.pop().strip() 40 | if footer != "": 41 | Footer = glob.get('Footer', None) 42 | o.append(("Footer", footer) if Footer is None else Footer(footer)) 43 | text = parts[0] 44 | blocks = list(re.split(r"(?:\r?\n){2,}", text)) 45 | for i, block in enumerate(blocks): 46 | block = block.strip() 47 | lines = re.split(r"\r?\n", block) 48 | if len(lines) == 1: 49 | if re.match(r"\[.\]\s", block): 50 | Subsection = glob.get('Subsection', None) 51 | o.append(("Subsection", block[4:]) if Subsection is None else Subsection(block[4:])) 52 | else: 53 | Section = glob.get('Section', None) 54 | o.append(("Section", block) if Section is None else Section(block)) 55 | else: 56 | Text = glob.get('Text', None) 57 | o.append(("Text", block) if Text is None else Text(block)) 58 | return o 59 | 60 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/data/types/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Subpackage containing type validation functions and argument types. 3 | 4 | """ 5 | from .common import * 6 | from .config import * 7 | from .files import * 8 | from .hash import * 9 | from .network import * 10 | from .strings import * 11 | 12 | from .common import __features__ as _common 13 | from .config import __features__ as _config 14 | from .files import __features__ as _files 15 | from .hash import __features__ as _hash 16 | from .network import __features__ as _network 17 | from .strings import __features__ as _strings 18 | 19 | 20 | __all__ = __features__ = _common + _config + _files + _hash + _network + _strings 21 | 22 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/data/types/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Config-related checking functions and argument types. 3 | 4 | """ 5 | import configparser as ini 6 | 7 | for _m in ["json", "toml", "yaml"]: 8 | lazy_load_module(_m) 9 | 10 | 11 | __all__ = __features__ = [] 12 | 13 | 14 | # config check functions 15 | __all__ += ["is_ini", "is_ini_file", "is_json", "is_json_file", "is_toml", "is_toml_file", "is_yaml", "is_yaml_file"] 16 | is_ini = lambda c: __check_file(c, "ini", False, False) is not None 17 | is_ini_file = lambda c: __check_file(c, "ini", False) is not None 18 | is_json = lambda c: __check_file(c, "json", False, False) is not None 19 | is_json_file = lambda c: __check_file(c, "json", False) is not None 20 | is_toml = lambda c: __check_file(c, "toml", False, False) is not None 21 | is_toml_file = lambda c: __check_file(c, "toml", False) is not None 22 | is_yaml = lambda c: __check_file(c, "yaml", False, False) is not None 23 | is_yaml_file = lambda c: __check_file(c, "yaml", False) is not None 24 | 25 | 26 | # config argument types 27 | __all__ += ["ini_config", "ini_file", "json_config", "json_file", "toml_config", "toml_file", "yaml_config", 28 | "yaml_file"] 29 | 30 | 31 | def __check_file(c, ctype, fail=True, is_file=True): 32 | try: 33 | if ctype == "ini": 34 | cfg = ini.ConfigParser() 35 | if is_file: 36 | if len(cfg.read(c)) == 0: 37 | raise ValueError("Config file does not exist") 38 | else: 39 | cfg.read_string(str(c)) 40 | elif ctype in ["json", "toml"]: 41 | m = globals()[ctype] 42 | if is_file: 43 | with open(c, 'rt') as f: 44 | cfg = m.load(f) 45 | else: 46 | cfg = m.loads(c) 47 | elif ctype == "yaml": 48 | if is_file: 49 | with open(c, 'rb') as f: 50 | c = f.read() 51 | cfg = yaml.safe_load(c) 52 | return cfg 53 | except Exception as e: 54 | if fail: 55 | raise ValueError("Bad {} input config ({})".format(ctype, e)) 56 | ini_config = ini_file = lambda c: __check_file(c, "ini") 57 | json_config = json_file = lambda c: __check_file(c, "json") 58 | toml_config = toml_file = lambda c: __check_file(c, "toml") 59 | yaml_config = yaml_file = lambda c: __check_file(c, "yaml") 60 | ini_config.__name__ = ini_file.__name__ = "INI file" 61 | json_config.__name__ = json_file.__name__ = "JSON file" 62 | toml_config.__name__ = toml_file.__name__ = "TOML file" 63 | yaml_config.__name__ = yaml_file.__name__ = "YAML file" 64 | 65 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/data/types/hash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Hash-related checking functions and argument types. 3 | 4 | """ 5 | from ....preimports import re 6 | 7 | 8 | __all__ = __features__ = [] 9 | 10 | 11 | # hash check functions 12 | __all__ += ["is_hash", "is_md5", "is_sha1", "is_sha224", "is_sha256", 13 | "is_sha384", "is_sha512"] 14 | is_hash = lambda h: any(__check_hash(h, a, False) is not None for a in \ 15 | HASH_LEN.keys()) 16 | is_md5 = lambda h: __check_hash(h, "md5", False) is not None 17 | is_sha1 = lambda h: __check_hash(h, "sha1", False) is not None 18 | is_sha224 = lambda h: __check_hash(h, "sha224", False) is not None 19 | is_sha256 = lambda h: __check_hash(h, "sha256", False) is not None 20 | is_sha384 = lambda h: __check_hash(h, "sha384", False) is not None 21 | is_sha512 = lambda h: __check_hash(h, "sha512", False) is not None 22 | 23 | 24 | # hash-related argument types 25 | __all__ += ["any_hash", "md5_hash", "sha1_hash", "sha224_hash", "sha256_hash", 26 | "sha512_hash"] 27 | HASH_LEN = {'md5': 32, 'sha1': 40, 'sha224': 56, 'sha256': 64, 'sha384': 96, 28 | 'sha512': 128} 29 | 30 | 31 | def __check_hash(s, algo, fail=True): 32 | l = HASH_LEN[algo] 33 | if re.match(r"(?i)^[a-f0-9]{%d}$" % l, s) is None: 34 | if fail: 35 | raise ValueError("Bad {} hash".format(algo)) 36 | return 37 | return s 38 | md5_hash = lambda h: __check_hash(h, "md5") 39 | sha1_hash = lambda h: __check_hash(h, "sha1") 40 | sha224_hash = lambda h: __check_hash(h, "sha224") 41 | sha256_hash = lambda h: __check_hash(h, "sha256") 42 | sha384_hash = lambda h: __check_hash(h, "sha384") 43 | sha512_hash = lambda h: __check_hash(h, "sha512") 44 | md5_hash.__name__ = "MD5 hash" 45 | sha1_hash.__name__ = "SHA1 hash" 46 | sha224_hash.__name__ = "SHA224 hash" 47 | sha256_hash.__name__ = "SHA256 hash" 48 | sha384_hash.__name__ = "SHA384 hash" 49 | sha512_hash.__name__ = "SHA512 hash" 50 | 51 | 52 | def any_hash(h): 53 | if not any(__check_hash(h, a, False) is not None for a in HASH_LEN.keys()): 54 | raise ValueError("Bad hash") 55 | return h 56 | any_hash.__name__ = "arbitrary hash" 57 | 58 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/data/types/strings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """String-related checking functions and argument types. 3 | 4 | """ 5 | from ....preimports import ast, re, string 6 | 7 | 8 | __all__ = __features__ = [] 9 | 10 | 11 | def _str2list(s): 12 | """ Convert string to list if input is effectively a string. """ 13 | # if already a list, simply return it, otherwise ensure input is a string 14 | if isinstance(s, list): 15 | return s 16 | else: 17 | s = str(s) 18 | # remove heading and trailing brackets 19 | if s[0] == '[' and s[-1] == ']' or s[0] == '(' and s[-1] == ')': 20 | s = s[1:-1] 21 | # then parse list elements from the string 22 | l = [] 23 | for i in s.split(","): 24 | i = i.strip() 25 | try: 26 | l.append(ast.literal_eval(i)) 27 | except Exception: 28 | l.append(i) 29 | return l 30 | 31 | 32 | def _is_from_alph(s, a, t): 33 | if is_str(s): 34 | val = str_contains(a, t) 35 | try: 36 | val(s) 37 | return True 38 | except ValueError: 39 | pass 40 | return False 41 | 42 | 43 | # various string-related check functions 44 | __all__ += ["is_str", "is_bytes", "is_digits", "is_letters", "is_lowercase", "is_printable", "is_punctuation", 45 | "is_uppercase"] 46 | is_str = lambda s: isinstance(s, str) 47 | is_bytes = lambda s: isinstance(s, bytes) 48 | is_digits = lambda s, t=1.0: _is_from_alph(s, string.digits, t) 49 | is_letters = lambda s, t=1.0: _is_from_alph(s, string.ascii_letters, t) 50 | is_lowercase = lambda s, t=1.0: _is_from_alph(s, string.ascii_lowercase, t) 51 | is_printable = lambda s, t=1.0: _is_from_alph(s, string.printable, t) 52 | is_punctuation = lambda s, t=1.0: _is_from_alph(s, string.punctuation, t) 53 | is_uppercase = lambda s, t=1.0: _is_from_alph(s, string.ascii_uppercase, t) 54 | is_str.__name__ = "string" 55 | is_bytes.__name__ = "bytes" 56 | is_digits.__name__ = "digits" 57 | is_letters.__name__ = "letters" 58 | is_lowercase.__name__ = "lowercase string" 59 | is_printable.__name__ = "printable string" 60 | is_punctuation.__name__ = "punctuation" 61 | is_uppercase.__name__ = "uppercase string" 62 | 63 | # various data format check functions 64 | __all__ += ["is_bin", "is_hex"] 65 | is_bin = lambda b: is_str(b) and all(set(_).difference(set("01")) == set() for _ in re.split(r"\W+", b)) or \ 66 | isinstance(b, (list, set, tuple)) and all(str(x) in "01" for x in b) 67 | is_hex = lambda h: is_str(h) and len(h) % 2 == 0 and set(h.lower()).difference(set("0123456789abcdef")) == set() 68 | is_bin.__name__ = "binary string" 69 | is_hex.__name__ = "hexadecimal string" 70 | 71 | # some other common check functions 72 | __all__ += ["is_long_opt", "is_short_opt"] 73 | is_long_opt = lambda o: is_str(o) and re.match(r"^--[a-z]+(-[a-z]+)*$", o, re.I) 74 | is_short_opt = lambda o: is_str(o) and re.match(r"^-[a-z]$", o, re.I) 75 | 76 | # another useful check function 77 | __all__ += ["is_regex", "regular_expression"] 78 | is_regex = lambda s: __regex(s, False) is not None 79 | 80 | 81 | def __regex(regex, fail=True): 82 | """ Regular expression validation. """ 83 | try: 84 | re.sre_parse.parse(regex) 85 | return regex 86 | except re.sre_parse.error: 87 | if fail: 88 | raise ValueError("Bad regular expression") 89 | regular_expression = lambda s: __regex(s) 90 | regular_expression.__name__ = "regular expression" 91 | 92 | 93 | # -------------------- STRING FORMAT ARGUMENT TYPES -------------------- 94 | __all__ += ["str_contains", "str_matches"] 95 | 96 | 97 | def str_contains(alphabet, threshold=1.0): 98 | """ Counts the characters of a string and determines, given an alphabet, if the string has enough valid characters. 99 | """ 100 | if not 0.0 < threshold <= 1.0: 101 | raise ValueError("Bad threshold (should be between 0 and 1)") 102 | def _validation(s): 103 | p = sum(int(c in alphabet) for c in s) / float(len(s)) 104 | if p < threshold: 105 | raise ValueError("Input string does not contain enough items from the given alphabet ({:.2f}%)" 106 | .format(p * 100)) 107 | return s 108 | _validation.__name__ = "string contained" 109 | return _validation 110 | 111 | 112 | def str_matches(pattern, flags=0): 113 | """ Applies the given regular expression to a string argument. """ 114 | def _validation(s): 115 | if re.match(pattern, s, flags) is None: 116 | raise ValueError("Input string does not match the given regex") 117 | return s 118 | _validation.__name__ = "string match" 119 | return _validation 120 | 121 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/docstring.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """ Module for transforming an instance's docstring to a metadata dictionary as shown in the following example: 3 | 4 | class Example(object): 5 | \""" 6 | This is a test multi-line long 7 | description. 8 | 9 | This is a first comment. 10 | 11 | Author: John Doe 12 | (john.doe@example.com) 13 | Version: 1.0 14 | Comments: 15 | - subcomment 1 16 | - subcomment 2 17 | 18 | Something: lorem ipsum 19 | paragraph 20 | 21 | This is a second comment, 22 | a multi-line one. 23 | \""" 24 | [...] 25 | 26 | >>> parse_docstring(Example) 27 | {'author': 'John Doe (john.doe@example.com)', 28 | 'comments': ['This is a first comment.', 29 | ('subcomment 1', 'subcomment 2'), 30 | 'This is a second comment, a multi-line one.'], 31 | 'description': 'This is a test multi-line long description.', 32 | 'something': 'lorem ipsum paragraph', 33 | 'version': '1.0'} 34 | """ 35 | from ..preimports import re 36 | 37 | 38 | __all__ = __features__ = ["parse_docstring"] 39 | 40 | 41 | def parse_docstring(something): 42 | """ Parse the docstring of the given object. """ 43 | metadata = {} 44 | if not isinstance(something, str): 45 | if not hasattr(something, "__doc__") or something.__doc__ is None: 46 | return metadata 47 | something = something.__doc__ 48 | # function for registering the key-value pair in the dictionary of metadata 49 | def setkv(key, value): 50 | if key is not None: 51 | key = key.lower().replace(" ", "_") 52 | if value == "": 53 | return 54 | else: 55 | if value.startswith("-"): # do not consider '*' 56 | value = tuple(map(lambda s: s.strip(), value.split("-")[1:])) 57 | # free text (either the description or a comment) 58 | if key is None: 59 | metadata.setdefault("comments", []) 60 | if metadata.get("description") is None: 61 | metadata["description"] = value 62 | else: 63 | metadata["comments"].append(value) 64 | # when comments field is explicitly set 65 | elif key == "comments": 66 | metadata["comments"].append(value) 67 | # convert each option to a tuple 68 | elif key == "options": 69 | metadata.setdefault("options", []) 70 | if not isinstance(value, (list, tuple)): 71 | value = [value] 72 | for v in value: 73 | metadata["options"].append(tuple(map(lambda s: s.strip(), v.split("|")))) 74 | # key-value pair 75 | else: 76 | metadata[key] = value 77 | # parse trying to get key-values first, then full text 78 | for p in re.split(r"\n\s*\n", something): 79 | field, text = None, "" 80 | for l in p.splitlines(): 81 | kv = list(map(lambda s: s.strip(), l.split(":", 1))) 82 | if len(kv) == 1: 83 | # unwrap and unindent lines of the text 84 | text = (text + " " + kv[0]).strip() 85 | else: 86 | # a key-value pair is present 87 | if kv[0] != field: 88 | setkv(field, text) 89 | field, text = kv 90 | setkv(field, text) 91 | return metadata 92 | 93 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/layout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """CLI layout objects. 3 | 4 | """ 5 | from .termsize import get_terminal_size 6 | 7 | for _m in ["terminaltables", "textwrap"]: 8 | lazy_load_module(_m) 9 | 10 | 11 | __all__ = __features__ = ["BorderlessTable", "NameDescription"] 12 | 13 | 14 | def __init_tables(): 15 | class _NoBorder(terminaltables.AsciiTable): 16 | """ AsciiTable with no border. """ 17 | def __init__(self, *args, **kwargs): 18 | super(_NoBorder, self).__init__(*args, **kwargs) 19 | self.outer_border = False 20 | self.inner_column_border = False 21 | self.inner_heading_row_border = False 22 | 23 | def __str__(self): 24 | t = self.table 25 | return "\n" + t + "\n" if len(t) > 0 else "" 26 | 27 | class BorderlessTable(_NoBorder): 28 | """ Custom table with no border. """ 29 | def __init__(self, data, title=None, title_ul_char="=", header_ul_char="-", header=True, indent=3): 30 | if len(data) == 0: 31 | raise ValueError("Invalid data ; should be a non-empty") 32 | self.data = data 33 | if data is None or not isinstance(data, list) or not all(isinstance(r, list) for r in data): 34 | raise ValueError("Invalid data ; should be a non-empty list of lists") 35 | if header: 36 | # add a row with underlining for the header row 37 | data.insert(1, [len(_) * header_ul_char for _ in data[0]]) 38 | if len(data) < 2: 39 | raise ValueError("Invalid data ; should be a non-empty") 40 | # now insert an indentation column 41 | if (indent or 0) > 0: 42 | for row in data: 43 | row.insert(0, max(0, indent - 3) * " ") 44 | # then initialize the AsciiTable 45 | super(BorderlessTable, self).__init__(data) 46 | # wrap the text for every column that has a width above the average 47 | n_cols, sum_w, c_widths = len(self.column_widths), sum(self.column_widths), [] 48 | try: 49 | width, _ = get_terminal_size() 50 | except TypeError: 51 | width = 80 52 | width -= n_cols * 2 # take cell padding into account 53 | max_w = round(float(width) / n_cols) 54 | rem = width - sum(w for w in self.column_widths if w <= max_w) 55 | c_widths = [w if w <= max_w else max(round(float(w) * width / sum_w), max_w) for w in self.column_widths] 56 | if sum(c_widths) >= width: 57 | c_widths[c_widths.index(max(c_widths))] -= sum(c_widths) - width 58 | for row in self.table_data: 59 | for i, v in enumerate(row): 60 | if len(str(v)) > 0: 61 | row[i] = "\n".join(textwrap.wrap(str(v), c_widths[i])) 62 | # configure the title 63 | self.title_ = title # define another title to format it differently 64 | self.title_ul_char = title_ul_char 65 | 66 | def __str__(self): 67 | return self.table 68 | 69 | @property 70 | def table(self): 71 | t = self.title_ 72 | s = ("\n{}\n{}\n".format(t, len(t) * self.title_ul_char) if t is not None else "") + "\n{}\n" 73 | return s.format(super(BorderlessTable, self).table) 74 | 75 | class NameDescription(_NoBorder): 76 | """ Row for displaying a name-description pair, with details if given. """ 77 | indent = 4 78 | 79 | def __init__(self, name, descr, details=None, nwidth=16): 80 | # compute the name column with to a defined width 81 | n = "{n: <{w}}".format(n=name, w=nwidth) 82 | # then initialize the AsciiTable, adding an empty column for indentation 83 | super(NameDescription, self).__init__([[" " * max(0, self.indent - 3), n, ""]]) 84 | # now wrap the text of the last column 85 | max_w = self.column_max_width(-1) 86 | self.table_data[0][2] = "\n".join(textwrap.wrap(descr, max_w)) 87 | self._details = details 88 | 89 | def __str__(self): 90 | s = super(NameDescription, self).__str__() 91 | if self._details: 92 | s += str(NameDescription(" ", self._details, nwidth=self.indent-2)) 93 | return s 94 | 95 | return _NoBorder, BorderlessTable, NameDescription 96 | _NoBorder, BorderlessTable, NameDescription = lazy_object(__init_tables) 97 | 98 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/licenses.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Common software licenses. 3 | 4 | Source: https://help.github.com/en/articles/licensing-a-repository 5 | """ 6 | from datetime import datetime 7 | 8 | 9 | __all__ = __features__ = ["copyright", "license", "list_licenses"] 10 | 11 | 12 | # source: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository 13 | LICENSES = { 14 | 'afl-3.0': "Academic Free License v3.0", 15 | 'agpl-3.0': "GNU Affero General Public License v3.0", 16 | 'apache-2.0': "Apache license 2.0", 17 | 'artistic-2.0': "Artistic license 2.0", 18 | 'bsd-2-clause': "BSD 2-clause \"Simplified\" license", 19 | 'bsd-3-clause': "BSD 3-clause \"New\" or \"Revised\" license", 20 | 'bsd-3-clause-clear': "BSD 3-clause Clear license", 21 | 'bsl-1.0': "Boost Software License 1.0", 22 | 'cc': "Creative Commons license family", 23 | 'cc-by-4.0': "Creative Commons Attribution 4.0", 24 | 'cc-by-sa-4.0': "Creative Commons Attribution Share Alike 4.0", 25 | 'cc0-1.0': "Creative Commons Zero v1.0 Universal", 26 | 'ecl-2.0': "Educational Community License v2.0", 27 | 'epl-1.0': "Eclipse Public License 1.0", 28 | 'eupl-1.1': "European Union Public License 1.1", 29 | 'gpl': "GNU General Public License family", 30 | 'gpl-2.0': "GNU General Public License v2.0", 31 | 'gpl-3.0': "GNU General Public License v3.0", 32 | 'isc': "ISC", 33 | 'lgpl': "GNU Lesser General Public License family", 34 | 'lgpl-2.1': "GNU Lesser General Public License v2.1", 35 | 'lgpl-3.0': "GNU Lesser General Public License v3.0", 36 | 'lppl-1.3c': "LaTeX Project Public License v1.3c", 37 | 'mit': "MIT", 38 | 'mpl-2.0': "Mozilla Public License 2.0", 39 | 'ms-pl': "Microsoft Public License", 40 | 'ncsa': "University of Illinois/NCSA Open Source License", 41 | 'ofl-1.1': "SIL Open Font License 1.1", 42 | 'osl-3.0': "Open Software License 3.0", 43 | 'postgresql': "PostgreSQL License", 44 | 'unlicense': "The Unlicense", 45 | 'wtfpl': "Do What The F*ck You Want To Public License", 46 | 'zlib': "zLib License", 47 | } 48 | 49 | 50 | def copyright(text, start=None, end=None): 51 | """ Make the copyright field value from the given text. """ 52 | year = datetime.now().year 53 | if end is None: 54 | end = year 55 | if isinstance(start, int) and isinstance(end, int) and start < end: 56 | year = "{}-{}".format(start, end) 57 | return "© {} {}".format(year, text) if not text.startswith("©") else text 58 | 59 | 60 | def license(name, null=False): 61 | """ Get the full license description matching the given short name. """ 62 | return LICENSES.get(str(name)) or ["Invalid license", None][null] 63 | 64 | 65 | def list_licenses(): 66 | """ Get the list of all supported licenses by short name. """ 67 | return list(LICENSES.keys()) 68 | 69 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Utility functions for retrieving a parser and subparsers from a tool. 3 | 4 | """ 5 | from shutil import which 6 | 7 | from .path import Path, PythonPath, TempPath 8 | from ..argreparse import ArgumentParser, ProxyArgumentParser 9 | 10 | 11 | __all__ = __features__ = ["get_parser", "get_parsers"] 12 | 13 | 14 | def get_parser(tool, logger=None, **kwargs): 15 | for p in get_parsers(tool, logger=logger, **kwargs).values(): 16 | if p.name == ArgumentParser.name: 17 | return p 18 | 19 | 20 | def get_parsers(tool, logger=None, **kwargs): 21 | tmp = TempPath(length=16) 22 | if isinstance(tool, str): 23 | tool = Path(which(tool), expand=True) 24 | # copy the target tool to modify it so that its parser tree can be retrieved 25 | ntool = tool.copy(tmp.joinpath(f"_{tool.basename}.py")) 26 | ntool.write_text(ntool.read_text().replace("if __name__ == '__main__':", f"{kwargs.pop('cond', '')}\ndef main():") \ 27 | .replace("if __name__ == \"__main__\":", "def main():") \ 28 | .replace("initialize(", "return parser\n initialize(") \ 29 | .rstrip("\n") + "\n\nif __name__ == '__main__':\n main()\n") 30 | ntool.chmod(0o755) 31 | # populate the real parser and add information arguments 32 | try: 33 | __parsers = {PythonPath(ntool).module.main(): ArgumentParser(**kwargs)} 34 | except Exception as e: 35 | if logger: 36 | logger.critical(f"Parser retrieval failed for tool: {tool.basename}") 37 | logger.error(f"Source ({ntool}):\n{ntool.read_text()}") 38 | logger.exception(e) 39 | from sys import exit 40 | exit(1) 41 | # now import the populated list of parser calls from within the tinyscript.parser module 42 | from tinyscript.argreparse import parser_calls 43 | global parser_calls 44 | # proxy parser to real parser recursive conversion function 45 | def __proxy_to_real_parser(value): 46 | """ Source: tinyscript.parser """ 47 | if isinstance(value, ProxyArgumentParser): 48 | return __parsers[value] 49 | elif isinstance(value, (list, tuple)): 50 | return [__proxy_to_real_parser(_) for _ in value] 51 | return value 52 | # now iterate over the registered calls 53 | pairs = [] 54 | for proxy_parser, method, args, kwargs, proxy_subparser in parser_calls: 55 | kw_category = kwargs.get('category') 56 | real_parser = __parsers[proxy_parser] 57 | args = (__proxy_to_real_parser(v) for v in args) 58 | kwargs = {k: __proxy_to_real_parser(v) for k, v in kwargs.items()} 59 | # NB: when initializing a subparser, 'category' kwarg gets popped 60 | real_subparser = getattr(real_parser, method)(*args, **kwargs) 61 | if real_subparser is not None: 62 | __parsers[proxy_subparser] = real_subparser 63 | if not isinstance(real_subparser, str): 64 | real_subparser._parent = real_parser 65 | real_subparser.category = kw_category # reattach category 66 | tmp.remove() 67 | ArgumentParser.reset() 68 | return __parsers 69 | 70 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/termsize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Multi-platform terminal size get function. 3 | 4 | Source: https://gist.githubusercontent.com/jtriley/1108174/raw/6ec4c846427120aa342912956c7f717b586f1ddb/terminalsize.py 5 | """ 6 | from ..preimports import os, platform, struct, subprocess 7 | 8 | 9 | __author__ = "Justin Riley (https://gist.github.com/jtriley)" 10 | 11 | 12 | __all__ = __features__ = ["get_terminal_size"] 13 | 14 | 15 | def get_terminal_size(): 16 | """ getTerminalSize() 17 | - get width and height of console 18 | - works on linux,os x,windows,cygwin(windows) 19 | originally retrieved from: 20 | http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python 21 | """ 22 | current_os = platform.system() 23 | tuple_xy = None 24 | if current_os == 'Windows': 25 | tuple_xy = _get_terminal_size_windows() 26 | if tuple_xy is None: 27 | tuple_xy = _get_terminal_size_tput() 28 | # needed for window's python in cygwin's xterm! 29 | if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'): 30 | tuple_xy = _get_terminal_size_linux() 31 | return tuple_xy or (80, 40) 32 | 33 | 34 | def _get_terminal_size_windows(): 35 | try: 36 | from ctypes import windll, create_string_buffer 37 | # stdin handle is -10 38 | # stdout handle is -11 39 | # stderr handle is -12 40 | h = windll.kernel32.GetStdHandle(-12) 41 | csbi = create_string_buffer(22) 42 | res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) 43 | if res: 44 | bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy = struct.unpack("hhhhHhhhhhh", csbi.raw) 45 | sizex = right - left + 1 46 | sizey = bottom - top + 1 47 | return sizex, sizey 48 | except: 49 | pass 50 | 51 | 52 | def _get_terminal_size_tput(): 53 | # get terminal width 54 | # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window 55 | try: 56 | cols = int(subprocess.check_call(["tput", "cols"])) 57 | rows = int(subprocess.check_call(["tput", "lines"])) 58 | return cols, rows 59 | except: 60 | pass 61 | 62 | 63 | def _get_terminal_size_linux(): 64 | def ioctl_gwinsz(fd): 65 | try: 66 | import fcntl 67 | import termios 68 | cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) 69 | return cr 70 | except: 71 | pass 72 | 73 | cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2) 74 | if not cr: 75 | try: 76 | fd = os.open(os.ctermid(), os.O_RDONLY) 77 | cr = ioctl_gwinsz(fd) 78 | os.close(fd) 79 | except: 80 | pass 81 | if not cr: 82 | try: 83 | cr = (os.environ['LINES'], os.environ['COLUMNS']) 84 | except: 85 | return None 86 | return int(cr[1]), int(cr[0]) 87 | 88 | 89 | if __name__ == "__main__": 90 | print("width = {}, height = {}".format(*get_terminal_size())) 91 | 92 | -------------------------------------------------------------------------------- /src/tinyscript/helpers/timeout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Timeout-related functions. 3 | 4 | """ 5 | from contextlib import contextmanager 6 | 7 | from .constants import WINDOWS 8 | from ..preimports import signal 9 | 10 | 11 | __all__ = __features__ = ["timeout", "Timeout"] 12 | 13 | 14 | class Timeout(object): 15 | """ Timeout context manager. 16 | 17 | :param seconds: number of seconds before raising the timeout 18 | :param message: custom message to be displayed 19 | :param stop: whether the execution must be stopped in case of timeout 20 | """ 21 | def __init__(self, seconds=10, message=None, stop=False): 22 | self.message = message 23 | self.seconds = seconds 24 | self.stop = stop 25 | 26 | def __enter__(self): 27 | if WINDOWS: 28 | raise NotImplementedError("signal.SIGALRM does not exist in Windows") 29 | else: 30 | signal.signal(signal.SIGALRM, self._handler) 31 | signal.alarm(self.seconds) 32 | return self 33 | 34 | def __exit__(self, exc_type, exc_value, exc_traceback): 35 | if WINDOWS: 36 | raise NotImplementedError("signal.SIGALRM does not exist in Windows") 37 | else: 38 | signal.signal(signal.SIGALRM, signal.SIG_IGN) 39 | signal.alarm(0) 40 | return not self.stop 41 | 42 | def _handler(self, signum, frame): 43 | raise TimeoutError(self.message or "Execution timeout") 44 | 45 | 46 | def timeout(seconds=10, message=None, stop=False): 47 | """ Decorator for applying the Timeout context manager to a function. """ 48 | def _wrapper1(f): 49 | def _wrapper2(*a, **kw): 50 | with Timeout(seconds, message, stop) as to: 51 | return f(*a, **kw) 52 | return _wrapper2 53 | return _wrapper1 54 | 55 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining the list of preimports. 3 | 4 | """ 5 | import lazy_object_proxy 6 | from importlib import import_module, reload 7 | 8 | 9 | __all__ = __features__ = ["import_module"] 10 | __all__ += ["__imports__", "load", "reload"] 11 | 12 | __imports__ = { 13 | 'bad': [], 14 | 'enhanced': { 15 | 'code': "codep", 16 | 'functools': "ftools", 17 | 'getpass': "pswd", 18 | 'hashlib': "hash", 19 | 'inspect': "inspectp", 20 | 'itertools': "itools", 21 | 'json': "jsonp", 22 | 'logging': "log", 23 | 'random': "rand", 24 | 're': "regex", 25 | 'string': "stringp", 26 | 'virtualenv': "venv", 27 | }, 28 | 'standard': [ 29 | "argparse", 30 | "ast", 31 | "base64", 32 | "binascii", 33 | "codext", 34 | "collections", 35 | "colorful", 36 | "configparser", 37 | "ctypes", 38 | "fileinput", 39 | "os", 40 | "platform", 41 | "shlex", 42 | "shutil", 43 | "signal", 44 | "string", 45 | "struct", 46 | "subprocess", 47 | "sys", 48 | "time", 49 | "types", 50 | "uuid", 51 | ], 52 | 'optional': [ 53 | "bs4", 54 | "fs", 55 | "loremipsum", 56 | "requests", 57 | ] 58 | } 59 | 60 | 61 | def _load_preimports(*extras, lazy=True): 62 | """ 63 | This loads the list of modules to be preimported in the global scope. 64 | 65 | :param extra: additional modules 66 | :return: list of successfully imported modules, list of failures 67 | """ 68 | i = __imports__ 69 | for module, enhanced in i['enhanced'].copy().items(): 70 | # these modules are used somewhere in the imported code anyway, hence laziness makes no sense 71 | load(module, enhanced, lazy=module in ["inspect", "logging"] or lazy) 72 | # handle specific classes to be added to the global namespace 73 | if module == "virtualenv": 74 | cls = ["PipPackage", "VirtualEnv"] 75 | for c in cls: 76 | globals()[c] = lazy_object_proxy.Proxy(lambda: getattr(module, c)) 77 | __features__.extend(cls) 78 | for module in i['standard'] + list(extras): 79 | load(module, lazy=lazy) 80 | for module in i['optional']: 81 | load(module, optional=True, lazy=lazy) 82 | 83 | 84 | def load(module, tsmodule=None, optional=False, lazy=True): 85 | """ 86 | This loads a module and, in case of failure, appends it to a list of bad 87 | imports or not if it is required or optional. 88 | 89 | :param module: module name 90 | :param tsmodule: Tinyscript root submodulen (in case of internal import) 91 | :param optional: whether the module is optional or not 92 | :param lazy: lazily load the module using a Proxy object 93 | """ 94 | global __features__, __imports__ 95 | m, tsmodule = globals().get(module), (module, ) if tsmodule is None else ("." + tsmodule, "tinyscript.preimports") 96 | if m is not None: # already imported 97 | __features__.append(module) 98 | return m 99 | def _load(): 100 | try: 101 | m = import_module(*tsmodule) 102 | if len(tsmodule) == 2: 103 | m = getattr(m, module) 104 | globals()[module] = m 105 | m.__name__ = module 106 | return m 107 | except ImportError: 108 | if module in __features__: 109 | __features__.remove(module) 110 | if not optional and module not in __imports__['bad']: 111 | __imports__['bad'].append(module) 112 | for k, x in __imports__.items(): 113 | if k != 'bad' and module in x: 114 | x.remove(module) if isinstance(x, list) else x.pop(module) 115 | __features__.append(module) 116 | globals()[module] = m = lazy_object_proxy.Proxy(_load) if lazy else _load() 117 | 118 | 119 | _load_preimports() 120 | 121 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/ftools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for enhancing functools preimport. 3 | 4 | """ 5 | import functools 6 | 7 | 8 | # source: https://stackoverflow.com/questions/6394511/python-functools-wraps-equivalent-for-classes 9 | def wraps_cls(cls): 10 | """ functools.wraps equivalent for classes. """ 11 | class _Wrapper(cls): 12 | def __init__(self, wrapped, assignents=functools.WRAPPER_ASSIGNMENTS): 13 | self.__wrapped__ = wrapped 14 | for attr in assignents: 15 | setattr(self, attr, getattr(wrapped, attr)) 16 | super().__init__(wrapped) 17 | 18 | def __repr__(self): 19 | return repr(self.__wrapped__) 20 | return _Wrapper 21 | functools.wraps_cls = wraps_cls 22 | 23 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/hash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for enhancing hashlib preimport. 3 | 4 | """ 5 | import hashlib 6 | from os.path import expanduser, isfile 7 | 8 | from ..helpers.compat import b 9 | 10 | 11 | def hash_file(filename, algo="sha256"): 12 | """ This extends hashlib's hashing function to hash a file per block. 13 | 14 | :param filename: name of the file to be hashed 15 | :return: ALGO(file) 16 | """ 17 | h = hashlib.new(algo) 18 | with open(filename, 'rb') as f: 19 | while True: 20 | data = f.read(h.block_size) 21 | if not data: 22 | break 23 | h.update(data) 24 | return h.hexdigest() 25 | hashlib.hash_file = hash_file 26 | 27 | 28 | # this binds new file hashing functions to the hashlib for each existing hash algorithm 29 | for algo in [x for x in hashlib.__dict__.keys()]: 30 | try: 31 | h = hashlib.new(algo) 32 | h.update(b"") 33 | def _hash_file(a): 34 | def _wrapper(f): 35 | return hash_file(f, a) 36 | return _wrapper 37 | setattr(hashlib, "{}_file".format(algo), _hash_file(algo)) 38 | except ValueError: # unsupported hash type 39 | pass 40 | 41 | 42 | class LookupTable(dict): 43 | """ Lookup table class for password cracking. 44 | 45 | :param dict_path: path of the dictionary file to be loaded 46 | :param algorithm: the hash algorithm to be used 47 | :param ratio: ratio of value to be hashed in the lookup table (by default, every value is considered but, i.e. 48 | with a big wordlist, a ratio of 2/3/4/5/... can be used in order to limit the memory load) 49 | :param dict_filter: function aimed to filter the words from the dictionary (e.g. only alphanumeric) 50 | :param prefix: prefix to be prepended to passwords (e.g. a salt) 51 | :param suffix: suffix to be appended to passwords (e.g. a salt) 52 | """ 53 | def __init__(self, dict_path, algorithm="md5", ratio=1., dict_filter=None, prefix=None, suffix=None): 54 | dict_path = expanduser(dict_path) 55 | if not isfile(dict_path): 56 | raise ValueError("Bad dictionary file path") 57 | if algorithm not in hashlib.algorithms_available: 58 | raise ValueError("Bad hashing algorithm") 59 | if not isinstance(ratio, float) or ratio <= 0. or ratio > 1.: 60 | raise ValueError("Bad ratio") 61 | h = lambda x: getattr(hashlib, algorithm)(b(prefix or "") + b(x) + b(suffix or "")).hexdigest() 62 | with open(dict_path) as f: 63 | self.filtered = 0 64 | m = round(1 / float(ratio)) 65 | for i, l in enumerate(f): 66 | if (i - self.filtered) % m == 0: 67 | l = l.strip() 68 | if dict_filter is None or hasattr(dict_filter, '__call__') and dict_filter(l): 69 | self[h(l)] = l 70 | else: 71 | self.filtered += 1 72 | hashlib.LookupTable = LookupTable 73 | 74 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/inspectp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for enhancing inspect preimport. 3 | 4 | """ 5 | import inspect 6 | import sys 7 | 8 | 9 | def getcallermodule(): 10 | """ Return the module the caller belongs to. """ 11 | return inspect.getmodule(inspect.currentframe().f_back) 12 | inspect.getcallermodule = getcallermodule 13 | 14 | 15 | def getmainframe(): 16 | """ Return __main__'s frame object. """ 17 | return getparentframe(__name__="__main__") or inspect.currentframe() 18 | inspect.getmainframe = getmainframe 19 | 20 | 21 | def getmainglobals(): 22 | """ Return __main__'s globals. """ 23 | return getmainframe().f_globals 24 | inspect.getmainglobals = getmainglobals 25 | 26 | 27 | def getmainmodule(): 28 | """ Return __main__'s frame object. """ 29 | return inspect.getmodule(getmainframe()) 30 | inspect.getmainmodule = getmainmodule 31 | 32 | 33 | def getparentframe(**kwargs): 34 | """ Return the frame object of the first one having kwargs in its globals. """ 35 | frame = inspect.stack()[0][0] 36 | while frame is not None: 37 | frame = frame.f_back 38 | if frame and all(frame.f_globals.get(k) == v for k, v in kwargs.items()): 39 | break 40 | return frame 41 | inspect.getparentframe = getparentframe 42 | 43 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/itools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for enhancing itertools preimport. 3 | 4 | """ 5 | import itertools 6 | from functools import update_wrapper, wraps 7 | 8 | from ..helpers.data.types import is_generator, is_generatorfunc 9 | 10 | 11 | __generator_inputs = {} 12 | 13 | 14 | class NonResettableGeneratorException(Exception): 15 | pass 16 | itertools.NonResettableGeneratorException = NonResettableGeneratorException 17 | 18 | 19 | def __product_lazy(*generators): 20 | """ This recursive function allows to yield an output of the cartesian product of iterables and generators. """ 21 | gen = (_ for _ in generators[0]) if not is_generator(generators[0]) else reset(generators[0], True) 22 | for i in gen: 23 | if len(generators) == 1: 24 | yield [i] 25 | continue 26 | for j in __product_lazy(*generators[1:]): 27 | yield [i] + j 28 | 29 | 30 | def product_lazy(*items, **kwargs): 31 | """ This function can use iterables and generators to generate and output as itertools.product but fully lazy. 32 | 33 | :param items: list of iterables and/or generators 34 | :param repeat: number of times items should be repeated 35 | """ 36 | repeat = kwargs.get('repeat', 1) 37 | if repeat < 0: 38 | raise ValueError("Repeat must be a positive integer") 39 | elif repeat == 0: 40 | # even if 0 occurrence must be generated, trigger the generators 41 | for i in items: 42 | if is_generator(i): 43 | next(i) 44 | yield () 45 | else: 46 | new_items = [] 47 | for n in range(repeat): 48 | for i in items: 49 | if n > 0 and is_generator(i): 50 | # make n different instances of the same generator 51 | f, args, kwargs = __generator_inputs[i] 52 | i = f(*args, **kwargs) 53 | __generator_inputs[i] = f, args, kwargs 54 | new_items.append(i) 55 | for out in __product_lazy(*new_items): 56 | yield tuple(out) 57 | itertools.product2 = product_lazy 58 | 59 | 60 | def reset(g, keep=False): 61 | """ This function resets a generator to its initial state as of the registered arguments. """ 62 | try: 63 | f, args, kwargs = __generator_inputs.pop(g) if not keep else __generator_inputs[g] 64 | return resettable(f)(*args, **kwargs) 65 | except KeyError: 66 | raise NonResettableGeneratorException("Cannot reset this generator") 67 | itertools.reset = reset 68 | 69 | 70 | def resettable(f): 71 | """ This decorator registers for a generator instance its function and arguments so that it can be reset. """ 72 | if not is_generatorfunc(f): 73 | raise ValueError("The input function does not produce a generator") 74 | @wraps(f) 75 | def _wrapper(*args, **kwargs): 76 | g = f(*args, **kwargs) 77 | __generator_inputs[g] = (f, args, kwargs) 78 | return g 79 | return _wrapper 80 | itertools.resettable = resettable 81 | 82 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/jsonp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for enhancing json preimport. 3 | 4 | """ 5 | import json 6 | 7 | from ..helpers import ensure_str 8 | 9 | 10 | _CACHE = dict() 11 | 12 | 13 | def dumpc(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, 14 | separators=None, default=None, sort_keys=False, **kw): 15 | """ Serialize ``obj`` as a JSON formatted stream to ``fp`` (a ``.write()``-supporting file-like object. """ 16 | comments = _CACHE.get(id(obj), {}) 17 | indent = comments.get('indent', indent) 18 | s = json.dumps(obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, 19 | allow_nan=allow_nan, cls=cls, indent=indent, separators=separators, default=default, 20 | sort_keys=sort_keys, **kw) 21 | if indent: 22 | s, lines = "", s.split("\n") 23 | for l in lines: 24 | try: 25 | ws, c = comments.get('body', {}).get(l.strip().rstrip(",")) 26 | s += f"{l}{' '*ws}#{c}\n" 27 | except TypeError: 28 | s += f"{l}\n" 29 | s = "\n".join(f"{' '*ws}#{c}" for ws, c in comments.get('header', [])) + s 30 | fp.write(s.encode() if 'b' in fp.mode else s) 31 | json.dumpc = dumpc 32 | 33 | 34 | def loadc(fp, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, 35 | **kw): 36 | """ Deserialize ``fp`` (a ``.read()``-supporting file-like object containing a JSON document with comments) to a 37 | Python object. """ 38 | s, comments, header, indent = [], {}, True, None 39 | # collect comments from the header then from the body ; keep track of indentation 40 | for l in ensure_str(fp.read()).split("\n"): 41 | i = len(l) - len(l.lstrip()) 42 | if i > 0: 43 | indent = i if indent is None else min(indent, i) 44 | try: 45 | l, c = l.split("#", 1) 46 | ws = len(l) - len(l.rstrip()) 47 | except ValueError: 48 | c = None 49 | if header: 50 | if l.strip() == "": 51 | if c: 52 | comments.setdefault('header', []) 53 | comments['header'].append((ws, c.rstrip())) 54 | continue 55 | else: 56 | header = False 57 | s.append(l) 58 | if c: 59 | comments.setdefault('body', {}) 60 | comments['body'][l.strip().rstrip(",")] = (ws, c.rstrip()) 61 | comments['indent'] = indent 62 | # now parse the comment-free JSON 63 | obj = json.loads("\n".join(s), cls=cls, object_hook=object_hook, parse_float=parse_float, parse_int=parse_int, 64 | parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw) 65 | _CACHE[id(obj)] = comments 66 | return obj 67 | json.loadc = loadc 68 | 69 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/pswd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """Module for enhancing getpass preimport. It adds a function that prompts for a password that must be compliant with a 3 | simple password policy. 4 | """ 5 | 6 | import getpass 7 | 8 | from .log import bindLogger 9 | from ..helpers.password import getpass as _getpass 10 | 11 | 12 | @bindLogger 13 | def __getcompliantpass(prompt="Password: ", stream=None, policy=None, once=False): 14 | """ This function allows to enter a password enforced through a password policy able to check for the password 15 | length, characters set and presence in the given wordlists. 16 | 17 | :param prompt: prompt text 18 | :param stream: a writable file object to display the prompt (defaults to the tty or to sys.stderr if not available) 19 | :param policy: password policy to be considered 20 | :param once: ask for a password only once 21 | :return: policy-compliant password 22 | """ 23 | pwd = None 24 | while pwd is None: 25 | logger.debug("Special conjunction characters are stripped") 26 | try: 27 | pwd = _getpass(prompt, stream, policy).strip() 28 | except ValueError as exc: 29 | if hasattr(exc, "errors"): 30 | for e in exc.errors: 31 | logger.error(e) 32 | pwd = None 33 | else: 34 | raise 35 | except KeyboardInterrupt: 36 | print("") 37 | break 38 | if once: 39 | break 40 | return pwd 41 | getpass.getcompliantpass = __getcompliantpass 42 | 43 | -------------------------------------------------------------------------------- /src/tinyscript/preimports/stringp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for enhancing string preimport. 3 | 4 | """ 5 | import re 6 | import string 7 | 8 | from ..helpers.termsize import get_terminal_size 9 | 10 | 11 | def _natural_key(text): 12 | """ Key computation function for considering keys in a natural way. 13 | 14 | :param text: text to be used for computing the key 15 | """ 16 | tokens = [] 17 | for s in re.split(r"(\d+|\D+)", text): 18 | tokens.append(int(s) if s.isdigit() else s.lower()) 19 | return tokens 20 | string.natural_key = _natural_key 21 | 22 | 23 | def sort_natural(strings): 24 | """ Simple function to sort a list of strings with numbers inside. 25 | 26 | :param strings: list of strings 27 | """ 28 | strings.sort(key=_natural_key) 29 | string.sort_natural = sort_natural 30 | 31 | 32 | def sorted_natural(lst): 33 | """ Simple function to return a sorted list of strings with numbers inside. 34 | 35 | :param strings: list of strings 36 | :return: list of strings sorted based on numbers inside 37 | """ 38 | return sorted(lst, key=_natural_key) 39 | string.sorted_natural = sorted_natural 40 | 41 | 42 | def shorten(string, length=None, end="..."): 43 | """ Simple string shortening function for user-friendlier display. 44 | 45 | :param string: the string to be shortened 46 | :param length: maximum length of the string 47 | """ 48 | if length is None: 49 | ts = get_terminal_size() 50 | length = ts[0] if ts is not None else 40 51 | if not isinstance(length, int): 52 | raise ValueError("Invalid length '{}'".format(length)) 53 | return string if len(string) <= length else string[:length-len(end)] + end 54 | string.shorten = shorten 55 | 56 | -------------------------------------------------------------------------------- /src/tinyscript/report/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for making a report from markdown/HTML to PDF or other individual 3 | elements to various formats. 4 | 5 | """ 6 | from .objects import * 7 | from .report import * 8 | 9 | from .objects import __features__ as _objects 10 | from .report import __features__ as _report 11 | 12 | 13 | __features__ = _objects 14 | __all__ = _report + __features__ 15 | 16 | -------------------------------------------------------------------------------- /src/tinyscript/report/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Module for defining common functions for report elements. 3 | 4 | """ 5 | 6 | __all__ = ["output", "Element"] 7 | 8 | 9 | def output(f): 10 | """ This decorator allows to choose to return an output as text or to save it to a file. """ 11 | from functools import wraps 12 | f._output = True # used in tinyscript.parser 13 | @wraps(f) 14 | def _wrapper(self, *args, **kwargs): 15 | from os.path import exists, splitext 16 | s2f = kwargs.pop('save_to_file', False) 17 | r = f(self, *args, **kwargs) 18 | if not s2f or r is None: 19 | return r 20 | if isinstance(r, dict): 21 | if f.__name__ == "json": 22 | from json import dumps 23 | r = dumps(r, indent=kwargs.get('indent', 2)) 24 | elif f.__name__ == "yaml": 25 | from yaml import dump 26 | r = dump(r, indent=kwargs.get('indent', 2), width=kwargs.get('width', 0)) 27 | filename = "{}.{}".format(self.filename, f.__name__) 28 | while exists(filename): 29 | name, ext = splitext(filename) 30 | try: 31 | name, i = name.split('-') 32 | i = int(i) + 1 33 | except ValueError: 34 | i = 2 35 | filename = "{}-{}".format(name, i) + ext 36 | with open(filename, 'w') as out: 37 | out.write(r) 38 | return _wrapper 39 | 40 | 41 | class Element(object): 42 | """ This class is used to give a common type to report elements. """ 43 | _style = {} 44 | id = 0 45 | 46 | def __init__(self, **kwargs): 47 | self.name = kwargs.get('name', self.__class__.__name__.lower()) 48 | self.css = "" 49 | for k, v in kwargs.items(): 50 | if k in ['color', 'size', 'style']: 51 | self._style[k] = v 52 | self._newline = "\n" 53 | 54 | def __repr__(self): 55 | return "<{}: {}>".format(self.__class__.__name__, self.name) 56 | 57 | def _set_indent(self, indent): 58 | return ("", "") if indent is None else (indent * " ", "\n") 59 | 60 | @property 61 | def data(self): 62 | from inspect import stack 63 | output_format = stack()[1][3] # get the calling output format method name from the stack 64 | return Element.format_data(self._data, output_format) 65 | 66 | @data.setter 67 | def data(self, data): 68 | self._data = data 69 | 70 | @property 71 | def style(self): 72 | r = "" 73 | for s, k in zip(["font-size:%spx", "font-style:%s", "color:%s"], ['size', 'style', 'color']): 74 | if self._style.get(k): 75 | r += s % str(self._style[k]) + ";" 76 | return r 77 | 78 | @output 79 | def csv(self, sep=','): 80 | return "" 81 | 82 | @output 83 | def html(self, indent=4): 84 | return "" 85 | 86 | @output 87 | def json(self, indent=2): 88 | return {self.name: self.data} 89 | 90 | @output 91 | def md(self): 92 | return "" 93 | rst = md 94 | 95 | @output 96 | def xml(self, indent=2): 97 | return ("<%(name)s>{0}{1}{0}" % self.__dict__).format(self._newline, str(self.data)) 98 | 99 | @output 100 | def yaml(self, indent=2): 101 | return self.json(indent=indent) 102 | 103 | @staticmethod 104 | def format_data(data, fmt): 105 | if isinstance(data, Element): 106 | return getattr(data, fmt, lambda: str(data))() 107 | if isinstance(data, (list, set, tuple)): 108 | t = type(data) 109 | data = list(data) 110 | for i, subdata in enumerate(data): 111 | data[i] = Element.format_data(subdata, fmt) 112 | data = t(data) 113 | elif isinstance(data, dict): 114 | for k, subdata in data.items(): 115 | data[k] = Element.format_data(subdata, fmt) 116 | return data 117 | 118 | -------------------------------------------------------------------------------- /src/tinyscript/report/default.css: -------------------------------------------------------------------------------- 1 | h1,h3{ 2 | line-height:1 3 | } 4 | address,blockquote,dfn,em{ 5 | font-style:italic 6 | } 7 | html{ 8 | font-size:100.01% 9 | } 10 | body{ 11 | font-size:75%; 12 | color:#222; 13 | background:#fff; 14 | font-family:\"Helvetica Neue\",Arial,Helvetica,sans-serif 15 | } 16 | h1,h2,h3,h4,h5,h6{ 17 | font-weight:400; 18 | color:#111 19 | } 20 | h1{ 21 | font-size:3em; 22 | margin-bottom:.5em 23 | } 24 | h2{ 25 | font-size:2em; 26 | margin-bottom:.75em 27 | } 28 | h3{ 29 | font-size:1.5em; 30 | margin-bottom:1em 31 | } 32 | h4{ 33 | font-size:1.2em; 34 | line-height:1.25; 35 | margin-bottom:1.25em 36 | } 37 | h5,h6{ 38 | font-size:1em; 39 | font-weight:700 40 | } 41 | h5{ 42 | margin-bottom:1.5em 43 | } 44 | h1 img,h2 img,h3 img,h4 img,h5 img,h6 img{ 45 | margin:0 46 | } 47 | p{ 48 | margin:0 0 1.5em 49 | } 50 | .left{ 51 | float:left!important 52 | } 53 | p .left{ 54 | margin:1.5em 1.5em 1.5em 0; 55 | padding:0 56 | } 57 | .right{ 58 | float:right!important 59 | } 60 | p .right{ 61 | margin:1.5em 0 1.5em 1.5em; 62 | padding:0 63 | } 64 | address,dl{ 65 | margin:0 0 1.5em 66 | } 67 | a:focus,a:hover{ 68 | color:#09f 69 | } 70 | a{ 71 | color:#06c; 72 | text-decoration:underline 73 | } 74 | .quiet,blockquote,del{ 75 | color:#666 76 | } 77 | blockquote{ 78 | margin:1.5em 79 | } 80 | dfn,dl dt,strong,th{ 81 | font-weight:700 82 | } 83 | sub,sup{ 84 | line-height:0 85 | } 86 | abbr,acronym{ 87 | border-bottom:1px dotted #666 88 | } 89 | pre{ 90 | margin:1.5em 0; 91 | white-space:pre 92 | } 93 | code,pre,tt{ 94 | font:1em 'andale mono','lucida console',monospace; 95 | line-height:1.5 96 | } 97 | li ol,li ul{ 98 | margin:0 99 | } 100 | ol,ul{ 101 | margin:0 1.5em 1.5em 0; 102 | padding-left:1.5em 103 | } 104 | ul{ 105 | list-style-type:disc 106 | } 107 | ol{ 108 | list-style-type:decimal 109 | } 110 | dd{ 111 | margin-left:1.5em 112 | } 113 | table{ 114 | margin-bottom:1.4em; 115 | width:100% 116 | } 117 | thead th{ 118 | background:#c3d9ff 119 | } 120 | caption,td,th{ 121 | padding:4px 10px 4px 5px 122 | } 123 | tbody tr.even td,tbody tr:nth-child(even) td{ 124 | background:#e5ecf9 125 | } 126 | tfoot{ 127 | font-style:italic 128 | } 129 | caption{ 130 | background:#eee 131 | } 132 | .small{ 133 | font-size:.8em; 134 | margin-bottom:1.875em; 135 | line-height:1.875em 136 | } 137 | .large{ 138 | font-size:1.2em; 139 | line-height:2.5em; 140 | margin-bottom:1.25em 141 | } 142 | .hide{ 143 | display:none 144 | } 145 | .loud{ 146 | color:#000 147 | } 148 | .highlight{ 149 | background:#ff0 150 | } 151 | .added{ 152 | background:#060; 153 | color:#fff 154 | } 155 | .removed{ 156 | background:#900; 157 | color:#fff 158 | } 159 | .first{ 160 | margin-left:0; 161 | padding-left:0 162 | } 163 | .last{ 164 | margin-right:0; 165 | padding-right:0 166 | } 167 | .top{ 168 | margin-top:0; 169 | padding-top:0 170 | } 171 | .bottom{ 172 | margin-bottom:0; 173 | padding-bottom:0 174 | } 175 | -------------------------------------------------------------------------------- /src/tinyscript/template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import re 3 | 4 | 5 | __all__ = ["new"] 6 | 7 | 8 | TARGETS = { 9 | 'pybots.HTTPBot': """ 10 | with HTTPBot("...", verbose=args.verbose) as bot: 11 | bot.get() 12 | """, 13 | 'pybots.JSONBot': """ 14 | with JSONBot("...", verbose=args.verbose) as bot: 15 | bot.get() 16 | """, 17 | 'pybots.TCPBot': """ 18 | with TCPBot("...", 1234, verbose=args.verbose) as bot: 19 | bot.send_receive("...") 20 | """ 21 | } 22 | TEMPLATE = """#!/usr/bin/python3 23 | # -*- coding: UTF-8 -*- 24 | {target}from tinyscript import * 25 | # TODO: fill in imports 26 | 27 | 28 | __author__ = "John Doe" 29 | __email__ = "john.doe@example.com" 30 | __version__ = "1.0" 31 | __copyright__ = "J. Doe" 32 | __license__ = "agpl-3.0" 33 | #__reference__ = "" 34 | #__source__ = "" 35 | #__training__ = "" 36 | # TODO: fill in the docstring 37 | __doc__ = \"\"\" 38 | This tool ... 39 | \"\"\" 40 | # TODO: fill in examples 41 | __examples__ = [""] 42 | 43 | 44 | if __name__ == '__main__': 45 | # TODO: add arguments 46 | initialize() 47 | # TODO: write logic here{base} 48 | """ 49 | 50 | 51 | def new(name, target=None): 52 | """ Function for creating a template script. 53 | 54 | :param name: name of the new script/tool 55 | :param target: type of script to be created, a value among TARGETS' keys 56 | """ 57 | with open("{}.py".format(name), 'w') as f: 58 | target_imp = "" if target is None else "from {} import {}\n".format(*target.split('.')) 59 | f.write(TEMPLATE.format(base=TARGETS.get(target) or "", target=target_imp)) 60 | 61 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import pytest 3 | 4 | from utils import remove 5 | 6 | 7 | @pytest.fixture(scope="session", autouse=True) 8 | def clear_files_teardown(): 9 | yield None 10 | for f in [".test-script.py", ".tinyscript-test.ini", "report.pdf", "test-script.py"]: 11 | remove(f) 12 | 13 | -------------------------------------------------------------------------------- /tests/test_conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Root module's __conf__.py tests. 3 | 4 | """ 5 | from tinyscript import * 6 | 7 | from utils import * 8 | 9 | 10 | class TestConf(TestCase): 11 | def test_deprecation(self): 12 | self.assertWarns(DeprecationWarning, deprecate, "test", "test2") 13 | f = lambda x: x 14 | f.__name__ = "test" 15 | deprecate(f, "test2") 16 | self.assertWarns(DeprecationWarning, test, "ok") 17 | 18 | def test_lazy_loading(self): 19 | lazy_load_module("gc", preload=lambda: True, postload=lambda m: True) 20 | self.assertIsNotNone(gc.DEBUG_LEAK) 21 | lazy_load_object("test", lambda: "ok", preload=lambda: True, postload=lambda m: True) 22 | self.assertIsNotNone(test.center(20)) 23 | 24 | -------------------------------------------------------------------------------- /tests/test_features_handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Handlers module assets' tests. 3 | 4 | """ 5 | import threading 6 | 7 | from tinyscript import * 8 | from tinyscript.features.handlers import * 9 | from tinyscript.features.handlers import signal, SIGINT, SIGTERM, _hooks, __interrupt_handler as ih, \ 10 | __pause_handler as ph, __terminate_handler as th 11 | 12 | from utils import * 13 | 14 | 15 | FILE = tmpf() 16 | SCRIPT1 = """from tinyscript import * 17 | def at_{0}(): 18 | with open("{1}", 'w+') as f: 19 | f.write("{2}") 20 | initialize() 21 | {3}""" 22 | SCRIPT2 = """from tinyscript import * 23 | def at_{0}(): 24 | with open("{1}", 'w+') as f: 25 | f.write("{2}") 26 | initialize() 27 | with DisableSignals(signal.{2}) as _: 28 | {3}""" 29 | SIGNALS = { 30 | 'interrupt': "SIGINT", 31 | 'terminate': "SIGTERM", 32 | } 33 | FILE2 = tmpf("handler-result", "txt") 34 | 35 | 36 | def exec_pause(self): 37 | while _hooks.state != "PAUSED": 38 | sleep(.1) 39 | self.assertEqual(_hooks.state, "PAUSED") 40 | _hooks.resume() 41 | 42 | 43 | def exec_script(handler, template): 44 | s = SIGNALS.get(handler) 45 | t = [f"os.kill(os.getpid(), signal.{s})", ""][s is None] 46 | with open(FILE, 'w+') as f: 47 | f.write(template.format(handler, FILE2, s, t)) 48 | p = subprocess.Popen(["python3", FILE]) 49 | p.wait() 50 | try: 51 | with open(FILE2) as f: 52 | out = f.read().strip() 53 | remove(FILE2) 54 | return out 55 | except IOError as e: 56 | pass 57 | 58 | 59 | class TestHandlers(TestCase): 60 | def _test_handler(self, h): 61 | self.assertEqual(exec_script(h, SCRIPT1), SIGNALS.get(h)) 62 | 63 | @classmethod 64 | def tearDownClass(self): 65 | remove(FILE) 66 | 67 | def test_disable_handlers(self): 68 | with DisableSignals(SIGINT): 69 | self.assertIsNone(exec_script("interrupt", SCRIPT2)) 70 | self.assertIsNone(exec_script("terminate", SCRIPT2)) 71 | self.assertRaises(ValueError, DisableSignals, 123456, fail=True) 72 | 73 | def test_interrupt_handler(self): 74 | self.assertIs(at_interrupt(), None) 75 | #FIXME: signals seem not to work anymore in Python 3.12 76 | if sys.version_info.major == 3 and sys.version_info.minor < 12: 77 | self._test_handler("interrupt") 78 | _hooks.sigint_action = "confirm" 79 | temp_stdout(self) 80 | temp_stdin(self, "\n") 81 | self.assertRaises(SystemExit, ih) 82 | _hooks.sigint_action = "continue" 83 | self.assertIsNone(ih()) 84 | with self.assertRaises(ValueError): 85 | _hooks.sigint_action = "BAD_ACTION" 86 | self.assertIsNotNone(_hooks.sigint_action) 87 | _hooks.sigint_action = "exit" 88 | 89 | def test_pause_handler(self): 90 | #FIXME: test once the feature for pausing execution is developed 91 | if WINDOWS: 92 | logger.warning("Pause-related features are not implemented for Windows") 93 | else: 94 | from tinyscript.features.handlers import SIGUSR1 95 | self.assertIsNot(signal(SIGUSR1, ph), None) 96 | self.assertEqual(_hooks.state, "RUNNING") 97 | t = threading.Thread(target=exec_pause, args=(self, )) 98 | t.start() 99 | ph() 100 | t.join() 101 | self.assertEqual(_hooks.state, "RUNNING") 102 | 103 | def test_terminate_handler(self): 104 | self.assertIs(at_terminate(), None) 105 | #FIXME: signals seem not to work anymore in Python 3.12 106 | if sys.version_info.major == 3 and sys.version_info.minor < 12: 107 | self._test_handler("terminate") 108 | 109 | def test_private_handlers(self): 110 | self.assertRaises(SystemExit, ih) 111 | self.assertIsNot(signal(SIGINT, ih), None) 112 | self.assertRaises(SystemExit, th) 113 | self.assertIsNot(signal(SIGTERM, th), None) 114 | self.assertRaises(SystemExit, _hooks.exit) 115 | 116 | -------------------------------------------------------------------------------- /tests/test_features_hotkeys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Keystrokes module assets' tests. 3 | 4 | """ 5 | from time import sleep 6 | from tinyscript.features.hotkeys import set_hotkeys 7 | from tinyscript.helpers.inputs import _keyboard 8 | 9 | from utils import * 10 | 11 | 12 | class TestHotkeys(TestCase): 13 | def test_set_hotkeys(self): 14 | try: # do not check with 'if _keyboard is None:' ; _keyboard's type will be lazy_proxy_object.Proxy 15 | _keyboard.press 16 | except AttributeError: # 'NoneType' object has no attribute 'press' 17 | return 18 | temp_stdout(self) 19 | set_hotkeys({}) 20 | set_hotkeys({'HOTKEYS': "default"}) 21 | sleep(.1) 22 | _keyboard.press("a") 23 | sleep(.1) 24 | _keyboard.press("l") 25 | sleep(.1) 26 | set_hotkeys({'HOTKEYS': {'l': "TEST"}}) 27 | sleep(.1) 28 | _keyboard.press("l") 29 | sleep(.1) 30 | set_hotkeys({'HOTKEYS': ("default", {'l': "TEST"})}) 31 | sleep(.1) 32 | _keyboard.press("l") 33 | sleep(.1) 34 | self.assertRaises(ValueError, set_hotkeys, {'HOTKEYS': "BAD"}) 35 | 36 | -------------------------------------------------------------------------------- /tests/test_features_interact.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Interaction module assets' tests. 3 | 4 | """ 5 | from tinyscript.features.interact import set_interact_items 6 | 7 | from utils import * 8 | 9 | 10 | args.interact = True 11 | set_interact_items(globals()) 12 | 13 | 14 | class TestInteraction(TestCase): 15 | def test_interact_setup(self): 16 | g = globals().keys() 17 | self.assertTrue(args.interact) 18 | self.assertIn("interact", g) 19 | self.assertIn("compile_command", g) 20 | self.assertIn("InteractiveConsole", g) 21 | self.assertIn("RemoteInteractiveConsole", g) 22 | 23 | def test_local_interaction(self): 24 | temp_stdout(self) 25 | temp_stdin(self, "\n") 26 | self.assertIs(interact(), None) 27 | temp_stdin(self, "\n") 28 | self.assertIs(interact("test"), None) 29 | temp_stdin(self, "\n") 30 | self.assertIs(interact(exitmsg="test"), None) 31 | 32 | def test_local_interactive_console(self): 33 | temp_stdout(self) 34 | temp_stdin(self, "\n") 35 | with InteractiveConsole("test") as console: 36 | pass 37 | temp_stdin(self, "\n") 38 | with InteractiveConsole(exitmsg="test") as console: 39 | pass 40 | temp_stdin(self, "\n") 41 | with InteractiveConsole() as console: 42 | console.interact() 43 | 44 | -------------------------------------------------------------------------------- /tests/test_features_loglib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Logging module assets' tests. 3 | 4 | """ 5 | from tinyscript.features.loglib import * 6 | 7 | from utils import * 8 | 9 | 10 | class TestLogging(TestCase): 11 | def setUp(self): 12 | args.verbose = True 13 | configure_logger(globals(), False) 14 | 15 | def tearDown(self): 16 | args.verbose = False 17 | configure_logger(globals(), False) 18 | 19 | def test_base_logger(self): 20 | temp_stdout(self) 21 | self.assertIsNot(logger, None) 22 | self.assertIs(logger.info("test"), None) 23 | self.assertIs(logger.warning("test"), None) 24 | self.assertIs(logger.error("test"), None) 25 | self.assertIs(logger.critical("test"), None) 26 | self.assertIs(logger.debug("test"), None) 27 | 28 | def test_enhanced_logger(self): 29 | temp_stdout(self) 30 | configure_logger(globals(), False, True) 31 | self.assertIs(logger.success("test"), None) 32 | self.assertIs(logger.failure("test"), None) 33 | self.assertIs(logger.time("test"), None) 34 | self.assertIs(logger.step("test"), None) 35 | self.assertIs(logger.interact("test"), None) 36 | self.assertIsNot(logging.RelativeTimeColoredFormatter().format(FakeLogRecord()), None) 37 | 38 | def test_multi_level_debug(self): 39 | temp_stdout(self) 40 | args.verbose = 3 41 | configure_logger(globals(), True) 42 | self.assertIs(logger.debug("test"), None) 43 | delattr(args, "verbose") 44 | configure_logger(globals(), True) 45 | self.assertIs(logger.critical("test"), None) 46 | 47 | -------------------------------------------------------------------------------- /tests/test_features_notify.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Notification feature module assets' tests. 3 | 4 | """ 5 | from tinyscript import * 6 | from tinyscript.features.notify import set_notify_items 7 | 8 | from utils import * 9 | 10 | 11 | NOTIFICATION_TIMEOUT = 1 12 | NOTIFICATION_LEVEL = 10 # logging.DEBUG 13 | 14 | 15 | class TestNotify(TestCase): 16 | @classmethod 17 | def setUpClass(cls): 18 | # collect ProxyArgumentParser object reference from the scope of tinyscript to reuse it afterwards (as it will 19 | # be overwritten with the reference of a real parser each time initialize(...) is called) 20 | cls.argv = sys.argv[1:] # backup input arguments 21 | sys.argv[1:] = ["--notify"] 22 | initialize() 23 | 24 | @classmethod 25 | def tearDownClass(cls): 26 | sys.argv[1:] = cls.argv # restore input arguments 27 | for h in logger.handlers[:]: 28 | if h.__class__.__name__ == "NotificationHandler": 29 | logger.handlers.remove(h) 30 | 31 | def test_notify_setup(self): 32 | g = globals().keys() 33 | self.assertTrue(args.notify) 34 | self.assertIn("notify", g) 35 | 36 | def test_notifications(self): 37 | logger.info("test message") 38 | logger.warning("test message") 39 | logger.error("test message") 40 | g = globals() 41 | g['NOTIFICATION_TIMEOUT'] = "BAD" 42 | self.assertRaises(ValueError, set_notify_items, g) 43 | g['NOTIFICATION_TIMEOUT'] = 1 44 | g['NOTIFICATION_LEVEL'] = -1 45 | self.assertRaises(ValueError, set_notify_items, g) 46 | g['NOTIFICATION_LEVEL'] = 10 47 | g['NOTIFICATION_ICONS_PATH'] = "/path/does/not/exist" 48 | self.assertRaises(ValueError, set_notify_items, g) 49 | 50 | -------------------------------------------------------------------------------- /tests/test_features_progress.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Progress management module assets' tests. 3 | 4 | """ 5 | from tinyscript.features.progress import set_progress_items 6 | 7 | from utils import * 8 | 9 | 10 | args.progress = True 11 | set_progress_items(globals()) 12 | 13 | 14 | class TestProgress(TestCase): 15 | def test_progress_setup(self): 16 | g = globals().keys() 17 | self.assertTrue(args.progress) 18 | self.assertIn("progress_manager", g) 19 | 20 | def test_progress_manager(self): 21 | p = progress_manager 22 | temp_stdout(self) 23 | p.start(total=10) 24 | self.assertIsNotNone(p._tqdm) 25 | p.update(5) 26 | p.update(10) 27 | p.stop() 28 | self.assertIsNone(p._tqdm) 29 | 30 | def test_progress_bar(self): 31 | for i in progressbar(10): 32 | continue 33 | for i in progressbar([1, 2, 3]): 34 | continue 35 | 36 | -------------------------------------------------------------------------------- /tests/test_features_step.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Stepping module assets' tests. 3 | 4 | """ 5 | from tinyscript.features.step import set_step_items 6 | 7 | from utils import * 8 | 9 | 10 | args.step = True 11 | set_step_items(globals()) 12 | 13 | 14 | class TestStepping(TestCase): 15 | def test_step_setup(self): 16 | g = globals().keys() 17 | self.assertTrue(args.step) 18 | self.assertIn("step", g) 19 | self.assertIn("Step", g) 20 | 21 | def test_step_object(self): 22 | temp_stdout(self) 23 | temp_stdin(self, "\n") 24 | with Step("test"): 25 | pass 26 | temp_stdin(self, "\n") 27 | with Step(): 28 | pass 29 | temp_stdin(self, "\n") 30 | with Step(at_end=True): 31 | pass 32 | 33 | def test_step_function(self): 34 | temp_stdout(self) 35 | temp_stdin(self, "\n") 36 | self.assertIs(step(), None) 37 | temp_stdin(self, "\n") 38 | self.assertIs(step("test"), None) 39 | 40 | -------------------------------------------------------------------------------- /tests/test_features_timing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Timing module assets' tests. 3 | 4 | """ 5 | import time 6 | 7 | from tinyscript.features.timing import set_time_items 8 | 9 | from utils import * 10 | 11 | 12 | args.stats = True 13 | args.timings = True 14 | set_time_items(globals()) 15 | 16 | 17 | class TestTiming(TestCase): 18 | def test_step_setup(self): 19 | g = globals().keys() 20 | self.assertTrue(args.stats) 21 | self.assertTrue(args.timings) 22 | self.assertIn("get_time", g) 23 | self.assertIn("get_time_since_last", g) 24 | self.assertIn("Timer", g) 25 | 26 | def test_time_manager(self): 27 | with Timer() as t: 28 | pass 29 | self.assertFalse(time_manager.stats()) 30 | 31 | def test_timer_object(self): 32 | if WINDOWS: 33 | logger.warning("Timeout-related features are not implemented for Windows") 34 | else: 35 | temp_stdout(self) 36 | with self.assertRaises(TimeoutError): 37 | with Timer(timeout=1, fail_on_timeout=True) as timer: 38 | self.assertTrue(timer.fail) 39 | self.assertTrue(timer.descr) 40 | self.assertTrue(timer.message) 41 | self.assertTrue(timer.start) 42 | self.assertEqual(timer.timeout, 1) 43 | self.assertRaises(TimeoutError, timer._handler, None, None) 44 | time.sleep(2) 45 | with Timer(timeout=1): 46 | time.sleep(2) 47 | 48 | def test_timing_functions(self): 49 | temp_stdout(self) 50 | self.assertFalse(get_time()) 51 | self.assertFalse(get_time("test")) 52 | self.assertFalse(get_time_since_last()) 53 | self.assertFalse(get_time_since_last("test")) 54 | 55 | -------------------------------------------------------------------------------- /tests/test_helpers_attack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Attack helper functions' tests. 3 | 4 | """ 5 | from tinyscript.helpers.attack import * 6 | 7 | from utils import remove, TestCase 8 | 9 | 10 | class TestHelpersAttack(TestCase): 11 | def test_attack_functions(self): 12 | self.assertEqual(list(bruteforce(2, "ab")), ["a", "b", "aa", "ab", "ba", "bb"]) 13 | self.assertEqual(list(bruteforce(2, "ab", repeat=False)), ["a", "b", "ab", "ba"]) 14 | self.assertRaises(ValueError, list, bruteforce(-1)) 15 | self.assertRaises(ValueError, list, bruteforce(2, minlen=-1)) 16 | self.assertRaises(ValueError, list, bruteforce(1, minlen=3)) 17 | DICT = ".test-dictionary-attack.txt" 18 | with open(DICT, 'wt') as f: 19 | f.write("password\ntest") 20 | self.assertEqual(list(dictionary(DICT)), ['password', 'test']) 21 | self.assertEqual(list(dictionary(DICT, rules="i,sta[0]")), ['password', 'Password0', 'test', 'Test0']) 22 | remove(DICT) 23 | self.assertEqual(sorted(list(bruteforce(3, "abc"))), sorted(list(bruteforce_re(r"[a-c]{1,3}")))) 24 | self.assertRaises(ValueError, list, bruteforce_re(1234)) 25 | for i in range(1, 5): 26 | self.assertEqual(len(list(bruteforce_pin(i))), 10 ** i) 27 | self.assertRaises(ValueError, list, bruteforce_pin(0)) 28 | 29 | def test_mask_string_expansion(self): 30 | self.assertIsNotNone(expand_mask("???c?(abc)")) 31 | self.assertRaises(ValueError, expand_mask, "?(") 32 | self.assertRaises(ValueError, expand_mask, "?()") 33 | self.assertRaises(ValueError, expand_mask, "?(v()") 34 | self.assertIsNotNone(expand_mask("??(v()")) 35 | self.assertRaises(ValueError, expand_mask, "?z") 36 | self.assertEqual(expand_mask("?z", {'z': "xyz"}), ["xyz"]) 37 | self.assertEqual(expand_mask("?v"), ["aeiouy"]) 38 | self.assertEqual(expand_mask("?v", {'v': "abc"}), ["abc"]) 39 | self.assertEqual(list(bruteforce_mask("ab?l", {'l': "cde"})), ["abc", "abd", "abe"]) 40 | self.assertEqual(list(bruteforce_mask(["a", "b", "cde"])), ["abc", "abd", "abe"]) 41 | g = bruteforce_mask(12345) 42 | self.assertIsNotNone(g) 43 | self.assertRaises(ValueError, list, g) 44 | 45 | def test_rule_parsing(self): 46 | self.assertTrue(list(parse_rule("icrstu"))) 47 | self.assertTrue(list(parse_rule("p[TEST]a[123]"))) 48 | for g in [parse_rule("rz"), parse_rule("z[test]"), parse_rule("p[test]]"), parse_rule("p[[test]")]: 49 | self.assertIsNotNone(g) 50 | self.assertRaises(ValueError, list, g) 51 | 52 | -------------------------------------------------------------------------------- /tests/test_helpers_classprop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Classproperty tests. 3 | 4 | """ 5 | from tinyscript.helpers.classprop import * 6 | 7 | from utils import TestCase 8 | 9 | 10 | class UselessClass(object): 11 | _val1 = None 12 | 13 | def __init__(self): 14 | self._val2 = None 15 | 16 | @classproperty 17 | def val1(cls): 18 | return cls._val1 19 | 20 | @val1.setter 21 | def val1(cls, value): 22 | cls._val1 = value 23 | 24 | @property 25 | def val2(self): 26 | return self._val2 27 | 28 | @val2.setter 29 | def val2(self, value): 30 | self._val2 = value 31 | 32 | @classproperty 33 | @classmethod 34 | def val3(cls): 35 | return cls._val1 36 | 37 | 38 | class TestHelpersClassProp(TestCase): 39 | def test_classproperty_feature(self): 40 | S1, S2, S3 = "OK1", "OK2", "OK3" 41 | self.assertIsNone(UselessClass.val1) 42 | self.assertIsNone(UselessClass.val3) 43 | self.assertIsInstance(UselessClass.val2, property) 44 | UselessClass.val1 = S1 45 | self.assertEqual(UselessClass.val1, S1) 46 | t1 = UselessClass() 47 | self.assertIsNone(t1.val2) 48 | t1.val2 = S2 49 | self.assertEqual(t1.val2, S2) 50 | self.assertIsInstance(UselessClass.val2, property) 51 | t1.val1 = S2 52 | self.assertEqual(t1.val1, S2) 53 | self.assertEqual(UselessClass.val1, S1) 54 | t2 = UselessClass() 55 | t2.val2 = S3 56 | self.assertEqual(t2.val2, S3) 57 | t2.val1 = S2 58 | self.assertEqual(t2.val1, S2) 59 | self.assertEqual(UselessClass.val1, S1) 60 | 61 | -------------------------------------------------------------------------------- /tests/test_helpers_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Common utility functions' tests. 3 | 4 | """ 5 | import lazy_object_proxy 6 | from datetime import datetime 7 | from pytz import timezone 8 | from tinyscript.helpers.common import * 9 | 10 | from utils import remove, TestCase 11 | 12 | 13 | DUMMY_CONST = None 14 | 15 | 16 | def _init_ospathexists(): 17 | from os.path import exists 18 | return exists 19 | 20 | 21 | def _postload(o): 22 | global DUMMY_CONST 23 | DUMMY_CONST = "OK" 24 | 25 | 26 | class TestHelpersCommon(TestCase): 27 | def test_common_utility_functions(self): 28 | self.assertRaises(TypeError, range2) 29 | self.assertRaises(TypeError, range2, ()) 30 | self.assertRaises(TypeError, range2, 1, 2, 3, 4) 31 | self.assertEqual(list(range2(2)), [0.0, 1.0]) 32 | self.assertEqual(list(range2(0, .5)), [0.0]) 33 | self.assertEqual(len(range2(0, .5)), 1) 34 | r = range2(0, .5, .1) 35 | self.assertEqual(repr(r), "range(0.0, 0.5, 0.1)") 36 | self.assertEqual(list(r), [0.0, 0.1, 0.2, 0.3, 0.4]) 37 | self.assertEqual(r.count(.3), 1) 38 | self.assertEqual(r.count(.5), 0) 39 | self.assertEqual(r.index(.2), 2) 40 | self.assertRaises(ValueError, r.index, 1.1) 41 | self.assertEqual(human_readable_size(123456), "121KB") 42 | self.assertRaises(ValueError, human_readable_size, "BAD") 43 | self.assertRaises(ValueError, human_readable_size, -1) 44 | self.assertIsNotNone(is_admin()) 45 | self.assertEqual(xor("this is a test", " "), "THIS\x00IS\x00A\x00TEST") 46 | self.assertEqual(list(strings("this is a \x00 test")), ["this is a ", " test"]) 47 | FILE, CONTENT = ".test_strings", b"this is a \x00 test" 48 | with open(FILE, 'wb') as f: 49 | f.write(CONTENT) 50 | self.assertIsNone(xor_file(FILE, " ")) 51 | self.assertIsNone(xor_file(FILE, " ")) 52 | with open(FILE, 'rb') as f: 53 | self.assertEqual(f.read(), CONTENT) 54 | self.assertEqual(list(strings_from_file(FILE)), ["this is a ", " test"]) 55 | remove(FILE) 56 | tz = timezone("Europe/London") 57 | self.assertEqual(dateparse("2008"), datetime(2008, datetime.now(tz).month, datetime.now(tz).day)) 58 | def test_func(): 59 | pass 60 | self.assertTrue(repr(test_func).startswith(" 1000), 990) 27 | self.assertEqual(reduce(lambda a, b: a+b, l, 20, stop=lambda x: x > 1000), 938) 28 | 29 | -------------------------------------------------------------------------------- /tests/test_helpers_data_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Data utility functions' tests. 3 | 4 | """ 5 | from tinyscript.helpers.data.utils import * 6 | from tinyscript.helpers.data.utils import PAD 7 | 8 | from utils import TestCase 9 | 10 | 11 | class TestHelpersDataUtils(TestCase): 12 | def test_common_utility_functions(self): 13 | BIN = "01010" 14 | STR = "test" 15 | HEX = "deadbeefc0f3b1ac00" 16 | self.assertEqual(pad(STR, "\x00"), STR + 4 * "\x00") 17 | self.assertEqual(pad(STR, ">\x00"), 4 * "\x00" + STR) 18 | self.assertEqual(unpad(pad(STR, "\x00"), "\x00"), STR) 19 | self.assertEqual(pad(BIN, "bit"), BIN + "100") 20 | self.assertEqual(unpad(pad(BIN, "bit"), "bit"), BIN) 21 | self.assertRaises(ValueError, pad, STR, "000") 22 | self.assertRaises(ValueError, pad, "0101", PAD[0]) 23 | self.assertRaises(ValueError, unpad, "0101", PAD[0]) 24 | self.assertRaises(ValueError, pad, "", None, -1) 25 | self.assertRaises(ValueError, unpad, "", None, -1) 26 | for padding in PAD: 27 | for l in range(5, 15): 28 | self.assertEqual(unpad(pad(STR, padding, l), padding, l), STR) 29 | self.assertEqual(unpad(pad(HEX, padding, l), padding, l), HEX) 30 | self.assertEqual(unpad(pad(HEX, padding, l, True), padding, l, True), HEX) 31 | self.assertEqual(entropy(STR), 1.5) 32 | self.assertEqual(entropy(STR), entropy(list(map(ord, STR)))) 33 | self.assertRaises(TypeError, entropy, 1) 34 | self.assertRaises(TypeError, entropy, pad) 35 | self.assertEqual(entropy_bits(STR), 19) 36 | self.assertRaises(TypeError, entropy_bits, 1) 37 | self.assertRaises(TypeError, entropy_bits, pad) 38 | self.assertEqual(entropy_bits("This is a test !"), 103) 39 | 40 | -------------------------------------------------------------------------------- /tests/test_helpers_decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Decorators' tests. 3 | 4 | """ 5 | from tinyscript.helpers.decorators import * 6 | 7 | from utils import * 8 | 9 | 10 | class GoodClass(object): 11 | pass 12 | 13 | 14 | class BadClass(object): 15 | pass 16 | 17 | 18 | @applicable_to(GoodClass) 19 | class EmptyMixin(object): 20 | pass 21 | 22 | 23 | class UselessClass(GoodClass, EmptyMixin): 24 | test = "test" 25 | 26 | def __init__(self): 27 | self.logger = logger 28 | 29 | def __exit__(self, *args): 30 | pass 31 | 32 | @try_or_die("TRY_OR_DIE message", extra_info="test") 33 | def func1(self): 34 | raise Exception("something wrong happened") 35 | 36 | @try_and_pass() 37 | def func2(self): 38 | raise Exception("something wrong happened") 39 | 40 | @try_and_warn("TRY_OR_WARN message", trace=True, extra_info="test") 41 | def func3(self): 42 | raise Exception("something wrong happened") 43 | 44 | 45 | @failsafe 46 | def func(): 47 | raise Exception("something wrong happened") 48 | 49 | 50 | @try_or_die("TRY_OR_DIE message", extra_info="test") 51 | def func1(): 52 | raise Exception("something wrong happened") 53 | 54 | @try_and_pass() 55 | def func2(): 56 | raise Exception("something wrong happened") 57 | 58 | @try_and_warn("TRY_OR_WARN message", trace=True, extra_info="test") 59 | def func3(): 60 | raise Exception("something wrong happened") 61 | 62 | 63 | class UselessClass2(BadClass, EmptyMixin): 64 | pass 65 | 66 | 67 | class TestHelpersDecorators(TestCase): 68 | def test_applicable_to(self): 69 | self.assertIsNotNone(UselessClass()) 70 | self.assertRaises(Exception, UselessClass2) 71 | 72 | def test_try_decorators(self): 73 | temp_stdout(self) 74 | t = UselessClass() 75 | self.assertRaises(SystemExit, t.func1) 76 | self.assertIsNone(t.func2()) 77 | self.assertIsNone(t.func3()) 78 | self.assertIsNone(func()) 79 | #self.assertRaises(Exception, func1) 80 | self.assertIsNone(func2()) 81 | self.assertIsNone(func3()) 82 | 83 | -------------------------------------------------------------------------------- /tests/test_helpers_docstring.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Dictionary assets' tests. 3 | 4 | """ 5 | from tinyscript.helpers.docstring import * 6 | 7 | from utils import * 8 | 9 | 10 | class _Example1(object): 11 | pass 12 | 13 | 14 | class _Example2(object): 15 | """ 16 | This is a test multi-line long 17 | description. 18 | 19 | This is a first comment. 20 | 21 | Author: John Doe 22 | (john.doe@example.com) 23 | Version: 1.0 24 | Comments: 25 | - subcomment 1 26 | - subcomment 2 27 | 28 | Options: 29 | - test | str 30 | - test2 | int 31 | 32 | Something: lorem ipsum 33 | paragraph 34 | 35 | This is a second comment, 36 | a multi-line one. 37 | 38 | Options: test3 | list 39 | """ 40 | pass 41 | 42 | _info = { 43 | 'author': "John Doe (john.doe@example.com)", 44 | 'comments': [ 45 | "This is a first comment.", 46 | ("subcomment 1", "subcomment 2"), 47 | "This is a second comment, a multi-line one.", 48 | ], 49 | 'description': "This is a test multi-line long description.", 50 | 'options': [ 51 | ('test', 'str'), 52 | ('test2', 'int'), 53 | ('test3', 'list'), 54 | ], 55 | 'something': "lorem ipsum paragraph", 56 | 'version': "1.0", 57 | } 58 | 59 | 60 | class TestHelpersDocstring(TestCase): 61 | def test_parse_docstring(self): 62 | self.assertEqual(parse_docstring(_Example1), {}) 63 | self.assertEqual(parse_docstring(_Example2.__doc__), _info) 64 | self.assertEqual(parse_docstring(_Example2), _info) 65 | 66 | -------------------------------------------------------------------------------- /tests/test_helpers_expressions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Expression evaluation functions' tests. 3 | 4 | """ 5 | from tinyscript.helpers.expressions import * 6 | from tinyscript.helpers.expressions import BL_BUILTINS 7 | 8 | from utils import TestCase 9 | 10 | 11 | EXPRESSIONS = [ 12 | "True", 13 | "[x for x in '123']", 14 | "list(x for x in [1,2,3])", 15 | ] 16 | 17 | 18 | class TestHelpersExpressions(TestCase): 19 | def test_ast_nodes_evaluation(self): 20 | self.assertIsNotNone(eval_ast_nodes(*EXPRESSIONS)) 21 | 22 | def test_expression_evaluations(self): 23 | # simple expressions (including list comprehensions and generators) 24 | for e in EXPRESSIONS: 25 | self.assertIsNotNone(eval2(e)) 26 | # missing names 27 | self.assertIsNotNone(eval2("test + 1", {'test': 1})) 28 | self.assertRaises(NameError, eval2, "test + 1") 29 | # native blacklist of names 30 | for n in BL_BUILTINS: 31 | try: 32 | self.assertRaises(NameError, eval2, "%s('True')" % n) 33 | except SyntaxError: 34 | pass # occurs with n="exec" under Python 2 35 | # code objects 36 | self.assertRaises(TypeError, eval2, "test", {'test': compile("None", "", "exec")}) 37 | # common attacks (triggers ValueError: node 'Attribute' is not allowed) 38 | self.assertRaises(ValueError, eval2, "__import__('subprocess').getoutput('id')") 39 | self.assertRaises(ValueError, eval2, "().__class__.__base__.__subclasses__()") 40 | 41 | def test_free_variables_evaluation(self): 42 | self.assertEqual(eval_free_variables("test + 1", **{'test': 1}), []) 43 | self.assertEqual(eval_free_variables(EXPRESSIONS[1]), ["x"]) 44 | 45 | -------------------------------------------------------------------------------- /tests/test_helpers_fexec.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Execution functions' tests. 3 | 4 | """ 5 | from tinyscript import logging, shutil 6 | from tinyscript.helpers.constants import LINUX 7 | from tinyscript.helpers.fexec import * 8 | from tinyscript.helpers.path import Path, TempPath 9 | 10 | from utils import remove, TestCase 11 | 12 | 13 | logger = logging.getLogger("test-exec-log") 14 | 15 | 16 | TEST_SH = """#!/bin/bash 17 | while true; do sleep .1; echo "."; >&2 echo "PATTERN$(echo $((1 + $RANDOM % 3)))"; done""" 18 | 19 | 20 | @process 21 | def test1(): 22 | pass 23 | 24 | 25 | @thread 26 | def test2(): 27 | pass 28 | 29 | 30 | class TestHelpersFexec(TestCase): 31 | @classmethod 32 | def setUpClass(cls): 33 | global SH, TPATH 34 | TPATH = TempPath(prefix="tinyscript-test_", length=8) 35 | if LINUX: 36 | SH = TPATH.joinpath("test.sh") 37 | SH.write_text(TEST_SH) 38 | 39 | @classmethod 40 | def tearDownClass(cls): 41 | TPATH.remove() 42 | 43 | def test_execution_functions(self): 44 | if LINUX: 45 | self.assertIsNotNone(execute("id")) 46 | self.assertIsNotNone(execute("sleep 10", timeout=1)) 47 | self.assertIsNotNone(execute("sleep 10", shell=True, timeout=1)) 48 | self.assertRaises(Exception, execute, "sleep 10", timeout=1, reraise=True) 49 | self.assertIsNotNone(execute(Path(shutil.which("id")))) 50 | self.assertIsNotNone(execute_and_log("id")) 51 | self.assertIsNotNone(execute_and_log(["id"], shell=True)) 52 | self.assertIsNotNone(execute_and_log("id 123456789", logger=logging.getLogger("test-exec-log-2"))) 53 | out, err, retc = execute_and_kill("id", patterns=["TEST"]) 54 | self.assertIsNotNone(out) 55 | self.assertEqual(retc, 0) 56 | out, err, retc = execute_and_kill(["/bin/bash", str(SH)], patterns=["PATTERN1"]) 57 | self.assertIn(b"PATTERN1", err) 58 | self.assertNotEqual(retc, 0) 59 | self.assertIsNotNone(filter_bin("cat", "id", "netstat", "whoami")) 60 | self.assertIsNotNone(test1()) 61 | self.assertIsNotNone(test2()) 62 | self.assertIsNone(processes_clean()) 63 | self.assertIsNone(threads_clean()) 64 | self.assertEqual(apply([lambda x: x+1, lambda x: x+2], (1, )), [2, 3]) 65 | 66 | -------------------------------------------------------------------------------- /tests/test_helpers_layout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """CLI layout objects' tests. 3 | 4 | """ 5 | from tinyscript.helpers.layout import * 6 | 7 | from utils import TestCase 8 | 9 | 10 | class TestHelpersLayoutObjects(TestCase): 11 | def test_borderless_objects(self): 12 | nd = NameDescription("test", "test argument") 13 | self.assertNotIn("\n\n", str(nd)) 14 | nd = NameDescription("test", "test argument", "test details") 15 | self.assertIn("\n\n", str(nd)) 16 | data = [["h1", "h2"], ["v1", "v2"]] 17 | bt1 = BorderlessTable(data, header=False) 18 | self.assertIsNotNone(str(bt1)) 19 | self.assertNotIn("-", str(bt1)) 20 | bt2 = BorderlessTable(data) 21 | self.assertIsNotNone(str(bt2)) 22 | self.assertIn("-", str(bt2)) 23 | self.assertRaises(ValueError, BorderlessTable, "BAD_DATA") 24 | self.assertRaises(ValueError, BorderlessTable, []) 25 | 26 | -------------------------------------------------------------------------------- /tests/test_helpers_licenses.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """License features' tests. 3 | 4 | """ 5 | from tinyscript.helpers.licenses import * 6 | from tinyscript.helpers.licenses import LICENSES 7 | 8 | from utils import TestCase 9 | 10 | 11 | class TestHelpersLicenses(TestCase): 12 | def test_license_functions(self): 13 | self.assertEqual(license("does_not_exist"), "Invalid license") 14 | self.assertEqual(license("afl-3.0"), LICENSES['afl-3.0']) 15 | self.assertIsNotNone(list_licenses()) 16 | self.assertIn("test", copyright("test")) 17 | self.assertIn("2000", copyright("test", 2000)) 18 | self.assertIn("2010", copyright("test", 2000, 2010)) 19 | 20 | -------------------------------------------------------------------------------- /tests/test_helpers_parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Root module's __conf__.py tests. 3 | 4 | """ 5 | from tinyscript.helpers import get_parser, Path 6 | from tinyscript.template import new, TARGETS, TEMPLATE 7 | 8 | from utils import * 9 | 10 | 11 | class TestConf(TestCase): 12 | def test_parser_retrieval(self): 13 | script = TEMPLATE.format(base="", target="").replace("# TODO: add arguments", "parser.add_argument('test')") 14 | with open(".test-script.py", 'wt') as f: 15 | f.write(script) 16 | p = get_parser(Path(".test-script.py")) 17 | subparsers = p.add_subparsers(dest="command") 18 | test = subparsers.add_parser("subtest", aliases=["test2"], help="test", parents=[p]) 19 | test.add_argument("--test") 20 | self.assertTrue(hasattr(p, "tokens")) 21 | 22 | -------------------------------------------------------------------------------- /tests/test_helpers_password.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Password input function tests. 3 | 4 | """ 5 | from tinyscript.helpers.password import * 6 | 7 | from utils import * 8 | 9 | 10 | class TestHelpersPassword(TestCase): 11 | def test_getpass(self): 12 | # test the policy 13 | self.assertRaises(ValueError, getpass, policy="BAD") 14 | self.assertRaises(ValueError, getpass, policy={'allowed': "BAD"}) 15 | self.assertRaises(ValueError, getpass, policy={'allowed': "?l", 'required': "?L"}) 16 | self.assertRaises(ValueError, getpass, policy={'wordlists': "BAD"}) 17 | for l in [(-1, 10), (10, -1), (10, 1)]: 18 | self.assertRaises(ValueError, getpass, policy={'length': l}) 19 | # test a few bad passwords 20 | WORDLIST = "./.wordlist" 21 | with open(WORDLIST, 'wt') as f: 22 | f.write("Test4321!") 23 | kwargs = {'policy': {'wordlists': ["wl_does_not_exist"]}} 24 | for i, p in enumerate(["test", "Test1", "Test1!", "Testtest", "testtesttest", "\x01\x02\x03", "Test4321!", 25 | "Th1s 1s 4 l0ng, v3ry l0ng, t00 l0ng c0mpl3x s3nt3nc3!"]): 26 | if i > 2: 27 | kwargs['policy'] = {'wordlists': [WORDLIST]} 28 | with mock_patch("getpass.getpass", return_value=p): 29 | self.assertRaises(ValueError, getpass, **kwargs) 30 | remove(WORDLIST) 31 | # test a few good passwords 32 | kwargs = {} 33 | for i, p in enumerate(["Test1234!", "Th1s 1s 4 l0ng s3nt3nc3!"]): 34 | if i > 1: 35 | kwargs['policy'] = {'wordlists': None} 36 | with mock_patch("getpass.getpass", return_value=p): 37 | pswd = getpass(**kwargs) 38 | self.assertEqual(pswd, p) 39 | 40 | def test_getrepass(self): 41 | with mock_patch("getpass.getpass", return_value="test"): 42 | self.assertRaises(ValueError, getrepass, pattern=r"[a-z]+\d+") 43 | with mock_patch("getpass.getpass", return_value="test1"): 44 | self.assertEqual(getrepass(pattern=r"[a-z]+\d+"), "test1") 45 | 46 | -------------------------------------------------------------------------------- /tests/test_helpers_timeout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Timeout utility assets' tests. 3 | 4 | """ 5 | from tinyscript.helpers.timeout import timeout, Timeout 6 | 7 | from utils import * 8 | 9 | 10 | class TestHelpersTimeout(TestCase): 11 | def test_timeout_execution(self): 12 | if WINDOWS: 13 | with self.assertRaises(NotImplementedError): 14 | timeout(1)(dummy_sleep) 15 | else: 16 | test = timeout(1)(dummy_sleep) 17 | self.assertIsNone(test()) 18 | test = timeout(1, "", True)(dummy_sleep) 19 | self.assertRaises(TimeoutError, test) 20 | test = timeout(3)(dummy_sleep) 21 | self.assertEqual(test(), "TEST") 22 | 23 | -------------------------------------------------------------------------------- /tests/test_preimports.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports module assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import * 6 | from tinyscript.preimports import load 7 | 8 | from utils import * 9 | 10 | 11 | class TestPreimports(TestCase): 12 | def test_preimports(self): 13 | BAD = "does_not_exist" 14 | load(BAD, lazy=False) 15 | self.assertIn(BAD, __imports__['bad']) 16 | for m in __imports__['standard'] + list(__imports__['enhanced'].keys()): 17 | self.assertIn(m, globals().keys()) 18 | for m in __imports__['bad']: 19 | self.assertNotIn(m, globals().keys()) 20 | 21 | -------------------------------------------------------------------------------- /tests/test_preimports_ftools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports code manipulation assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import functools 6 | 7 | from utils import * 8 | 9 | 10 | class TestPreimportsCode(TestCase): 11 | def test_class_wrapper(self): 12 | # source: https://stackoverflow.com/questions/6394511/python-functools-wraps-equivalent-for-classes 13 | @functools.wraps_cls 14 | class Memoized: 15 | def __init__(self, func): 16 | super().__init__() 17 | self.__func, self.__cache = func, {} 18 | def __call__(self, *args): 19 | try: 20 | return self.__cache[args] 21 | except KeyError: 22 | self.__cache[args] = v = self.__func(*args) 23 | return v 24 | except TypeError: 25 | return self.__func(*args) 26 | def __get__(self, obj, objtype): 27 | return functools.partial(self.__call__, obj) 28 | 29 | @Memoized 30 | def fibonacci(n): 31 | """fibonacci docstring""" 32 | if n in (0, 1): 33 | return n 34 | return fibonacci(n-1) + fibonacci(n-2) 35 | 36 | self.assertEqual(fibonacci.__doc__, "fibonacci docstring") 37 | self.assertIn("wrapper..fibonacci", repr(fibonacci)) 38 | 39 | -------------------------------------------------------------------------------- /tests/test_preimports_getpass.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports password input assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import getpass 6 | 7 | from utils import * 8 | 9 | 10 | class TestPreimportsGetpass(TestCase): 11 | def test_getcompliantpass(self): 12 | # test the policy 13 | self.assertRaises(ValueError, getpass.getcompliantpass, policy="BAD") 14 | self.assertRaises(ValueError, getpass.getcompliantpass, policy={'allowed': "BAD"}) 15 | self.assertRaises(ValueError, getpass.getcompliantpass, policy={'allowed': "?l", 'required': "?L"}) 16 | self.assertRaises(ValueError, getpass.getcompliantpass, policy={'wordlists': "BAD"}) 17 | for l in [(-1, 10), (10, -1), (10, 1)]: 18 | self.assertRaises(ValueError, getpass.getcompliantpass, policy={'length': l}) 19 | # test a few bad passwords 20 | WORDLIST = "./.wordlist" 21 | with open(WORDLIST, 'wt') as f: 22 | f.write("Test4321!") 23 | kwargs = {'once': True, 'policy': {'wordlists': ["wl_does_not_exist"]}} 24 | for i, p in enumerate(["test", "Test1", "Test1!", "Testtest", "testtesttest", "\x01\x02\x03", "Test4321!", 25 | "Th1s 1s 4 l0ng, v3ry l0ng, t00 l0ng c0mpl3x s3nt3nc3!"]): 26 | if i > 2: 27 | kwargs['policy'] = {'wordlists': [WORDLIST]} 28 | with mock_patch("getpass.getpass", return_value=p): 29 | pswd = getpass.getcompliantpass(**kwargs) 30 | self.assertIsNone(pswd) 31 | remove(WORDLIST) 32 | # test a few good passwords 33 | kwargs = {'once': True} 34 | for i, p in enumerate(["Test1234!", "Th1s 1s 4 l0ng s3nt3nc3!"]): 35 | if i > 1: 36 | kwargs['policy'] = {'wordlists': None} 37 | with mock_patch("getpass.getpass", return_value=p): 38 | pswd = getpass.getcompliantpass(**kwargs) 39 | self.assertEqual(pswd, p) 40 | 41 | -------------------------------------------------------------------------------- /tests/test_preimports_hashlib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports hashing assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import hashlib 6 | 7 | from utils import * 8 | 9 | 10 | FILE = "test-file.txt" 11 | 12 | 13 | class TestPreimportsHashlib(TestCase): 14 | def test_hashlib_improvements(self): 15 | touch(FILE) 16 | self.assertEqual(hashlib.hash_file(FILE), 17 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") 18 | self.assertEqual(hashlib.sha256_file(FILE), 19 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") 20 | with open(FILE, 'w') as f: 21 | f.write(100 * "A") 22 | self.assertEqual(hashlib.hash_file(FILE, "md5"), 23 | "8adc5937e635f6c9af646f0b23560fae") 24 | self.assertRaises(IOError, hashlib.hash_file, "does_not_exist") 25 | self.assertRaises(ValueError, hashlib.hash_file, FILE, 26 | "not_existing_hash_algo") 27 | remove(FILE) 28 | 29 | def test_hashlib_lookup_table(self): 30 | touch(FILE) 31 | with open(FILE, 'w') as f: 32 | f.write("12345678\nabcdefghi") 33 | self.assertEqual(hashlib.LookupTable(FILE), {'25d55ad283aa400af464c76d713c07ad': "12345678", 34 | '8aa99b1f439ff71293e95357bac6fd94': "abcdefghi"}) 35 | self.assertEqual(hashlib.LookupTable(FILE, prefix="test:"), {'c6b921e20761fcd95c9cc141389b10db': "12345678", 36 | 'ecfb190ec68ec6ada41cf487f5d076ce': "abcdefghi"}) 37 | self.assertEqual(hashlib.LookupTable(FILE, dict_filter=lambda x: x.isdigit()), 38 | {'25d55ad283aa400af464c76d713c07ad': "12345678"}) 39 | self.assertRaises(ValueError, hashlib.LookupTable, "does_not_exist") 40 | self.assertRaises(ValueError, hashlib.LookupTable, FILE, "bad_hash_algorithm") 41 | self.assertRaises(ValueError, hashlib.LookupTable, FILE, "md5", "bad_ratio") 42 | remove(FILE) 43 | 44 | -------------------------------------------------------------------------------- /tests/test_preimports_inspect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports code inspection assets' tests. 3 | 4 | """ 5 | from tinyscript.helpers.data.types import is_module 6 | from tinyscript.preimports import inspect 7 | 8 | from utils import * 9 | 10 | 11 | class TestPreimportsInspect(TestCase): 12 | def test_get_functions(self): 13 | m = inspect.getcallermodule() 14 | self.assertTrue(is_module(m)) 15 | self.assertEqual(m.__name__, "test_preimports_inspect") 16 | m = inspect.getmainmodule() 17 | self.assertTrue(is_module(m)) 18 | self.assertIn(m.__name__, ["__main__", "pytest"]) 19 | self.assertIsNotNone(inspect.getmainframe()) 20 | self.assertIn(('__name__', "__main__"), inspect.getmainglobals().items()) 21 | 22 | -------------------------------------------------------------------------------- /tests/test_preimports_itools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports iteration tools assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import itertools 6 | 7 | from utils import * 8 | 9 | 10 | @itertools.resettable 11 | def gen1(): 12 | yield "1" 13 | yield "2" 14 | 15 | 16 | def gen2(): 17 | yield "test" 18 | 19 | 20 | class TestPreimportsItertools(TestCase): 21 | def test_lazy_product(self): 22 | g = gen1() 23 | self.assertEqual(len(list(itertools.product2("ab", g, repeat=2))), 16) 24 | self.assertEqual(len(list(itertools.product2("ab", g, g, g))), 16) 25 | self.assertRaises(ValueError, list, itertools.product2("ab", repeat=-1)) 26 | 27 | def test_resettable_generator(self): 28 | self.assertRaises(ValueError, itertools.resettable, dummy_function) 29 | g1, g2 = gen1(), gen2() 30 | for i in range(10): 31 | self.assertEqual(next(g1), "1") 32 | self.assertEqual(next(g1), "2") 33 | self.assertRaises(StopIteration, next, g1) 34 | self.assertRaises(StopIteration, next, g1) 35 | g1 = itertools.reset(g1) 36 | self.assertRaises(itertools.NonResettableGeneratorException, itertools.reset, g2) 37 | 38 | -------------------------------------------------------------------------------- /tests/test_preimports_json.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports string assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import json 6 | 7 | from utils import * 8 | 9 | 10 | FNAME = ".test.json" 11 | TEST_JSON = """ 12 | # test long comment 1 13 | # with another line 14 | { 15 | "test": ["a", "b", "c"], 16 | "other": 1 # test comment 2 17 | } 18 | """ 19 | 20 | 21 | class TestPreimportsJson(TestCase): 22 | @classmethod 23 | def tearDownClass(cls): 24 | remove(FNAME) 25 | 26 | def test_commented_json_dumping(self): 27 | with open(FNAME, 'wt') as f: 28 | f.write(TEST_JSON) 29 | with open(FNAME) as f: 30 | d = json.loadc(f) 31 | d['another'] = True 32 | with open(FNAME, 'wb') as f: 33 | json.dumpc(d, f) 34 | with open(FNAME) as f: 35 | content = f.read() 36 | self.assertIn(" # test long comment 1", content) 37 | self.assertIn(" # with another line", content) 38 | self.assertIn(" # test comment 2", content) 39 | 40 | def test_commented_json_loading(self): 41 | with open(FNAME, 'wt') as f: 42 | f.write(TEST_JSON) 43 | with open(FNAME) as f: 44 | self.assertIsNotNone(json.loadc(f)) 45 | with open(FNAME, 'wb') as f: 46 | f.write(TEST_JSON.encode()) 47 | with open(FNAME, 'rb') as f: 48 | d = json.loadc(f) 49 | self.assertIn('test', d) 50 | self.assertIn('other', d) 51 | 52 | -------------------------------------------------------------------------------- /tests/test_preimports_logging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports logging assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import logging, os, sys 6 | 7 | from utils import * 8 | 9 | 10 | @logging.bindLogger 11 | def f_with_logging(testcase, **kwargs): 12 | logger.info("OK") 13 | testcase.assertEqual(logger.name, "other") 14 | 15 | 16 | class LoggingInFunc(object): 17 | def f1(self, testcase): 18 | testcase.assertRaises(AttributeError, getattr, self, "logger") 19 | 20 | @logging.bindLogger 21 | def f2(self, testcase, **kwargs): 22 | logger.info("OK") 23 | testcase.assertEqual(self.logger.name, "other") 24 | 25 | 26 | class TestPreimportsLogging(TestCase): 27 | def test_log_levels(self): 28 | self.assertRaises(ValueError, logging.addLogLevel, "info", "yellow", 10) 29 | levelname = "test" 30 | self.assertIsNone(logging.addLogLevel(levelname, "cyan", 1)) 31 | self.assertTrue(hasattr(logging, levelname.upper())) 32 | l = logging.getLogger("test_logger") 33 | self.assertTrue(hasattr(l, levelname)) 34 | self.assertIsNone(logging.delLogLevel(levelname)) 35 | self.assertRaises(ValueError, logging.delLogLevel, "does_not_exist") 36 | self.assertFalse(hasattr(logging, levelname.upper())) 37 | l = logging.getLogger("test_logger") 38 | self.assertFalse(hasattr(l, levelname)) 39 | self.assertIsNone(logging.addLevelName(100, levelname)) 40 | self.assertIsNone(logging.delLevelName(100)) 41 | self.assertIsNone(logging.addLevelName(100, levelname)) 42 | self.assertIsNone(logging.delLevelName(levelname)) 43 | self.assertIsNone(logging.setLoggingLevel("WARNING", r"test_")) 44 | self.assertEqual(l.level, logging.WARNING) 45 | 46 | def test_manipulate_loggers(self): 47 | l = logging.getLogger("test") 48 | h = logging.StreamHandler() 49 | l.addHandler(h) 50 | self.assertIn(h, l.handlers) 51 | self.assertIsNone(logging.setLoggers()) 52 | self.assertIsNone(logging.setLogger("test")) 53 | self.assertNotIn(h, l.handlers) 54 | f_with_logging(self, logger=logging.getLogger("other")) 55 | LoggingInFunc().f1(self) 56 | LoggingInFunc().f2(self, logger=logging.getLogger("other")) 57 | logging.renameLogger("test", "test2") 58 | self.assertEqual(l.name, "test2") 59 | self.assertRaises(ValueError, logging.renameLogger, "test", "test2") 60 | self.assertRaises(ValueError, logging.renameLogger, "test2", "test2") 61 | self.assertRaises(ValueError, logging.unsetLogger, "test") 62 | l2 = logging.getLogger("test3") 63 | l2.parent = l 64 | self.assertRaises(ValueError, logging.unsetLoggers, "test2") 65 | self.assertIsNone(logging.unsetLogger("test2", force=True)) 66 | k = list(logging.root.manager.loggerDict.keys()) 67 | self.assertNotIn("test", k) 68 | self.assertNotIn("test2", k) 69 | self.assertIn("test3", k) 70 | 71 | def test_std_to_logger(self): 72 | l = logging.getLogger("test") 73 | l.setLevel(logging.INFO) 74 | stdout = sys.stdout 75 | sys.stdout = logging.Std2Logger(l, "DEBUG") 76 | print("TEST") 77 | sys.stdout.flush() 78 | sys.stdout = stdout 79 | 80 | -------------------------------------------------------------------------------- /tests/test_preimports_random.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports random assets' tests. 3 | 4 | """ 5 | from collections import Counter 6 | from math import log 7 | from tinyscript.preimports import random 8 | 9 | from utils import * 10 | 11 | 12 | class TestPreimportsRandom(TestCase): 13 | def test_utility_functions(self): 14 | self.assertEqual(random.choice([1, 2, 3], [2, 3]), 1) 15 | self.assertRaises(IndexError, random.choice, []) 16 | self.assertIsNone(random.choice([], error=False)) 17 | self.assertIsNotNone(random.randstr()) 18 | self.assertEqual(random.randstr(0), "") 19 | self.assertEqual(len(random.randstr()), 8) 20 | self.assertEqual(len(random.randstr(20)), 20) 21 | self.assertNotIn("e", random.randstr(alphabet="abcd")) 22 | self.assertRaises(ValueError, random.randstr, -1) 23 | self.assertRaises(ValueError, random.randstr, 8, "") 24 | self.assertTrue(isinstance(random.randstr(16, b"\x00\x01\x02"), bytes)) 25 | for n, na in zip([8, 16, 64], [3, 4, 5]): 26 | for bs in [0, 16, 256]: 27 | for i in range(512): 28 | self.assertLess(max(Counter(random.randstr(n, "".join(chr(i) for i in range(na)), True, bs))\ 29 | .values()), n/(na-1) if (bs or n) > (na-1)/(1-(na-1)/na) else n/(na-2)) 30 | 31 | def test_random_lfsr(self): 32 | l = random.LFSR(target="0123456789abcdef") 33 | self.assertEqual(l.next_block("hex", update=False), "9617f3cf") 34 | self.assertEqual(l.next_block("hex"), "9617f3cf") 35 | self.assertEqual(l.next_block("hex"), "91f53906") 36 | self.assertTrue(l.next_block("str")) 37 | self.assertTrue(l.next_block("bin")) 38 | self.assertRaises(ValueError, random.LFSR) 39 | self.assertRaises(ValueError, random.LFSR, target=1234) 40 | self.assertRaises(ValueError, random.LFSR, "NO_NBITS_DEFINED") 41 | self.assertRaises(ValueError, random.LFSR, 1234, "BAD_TAPS", 32) 42 | self.assertRaises(ValueError, random.LFSR, "TOO_BIG_SEED", (1, 2, 3), 32) 43 | self.assertRaises(ValueError, random.LFSR, 0, (1, 2, 3), 32) 44 | self.assertRaises(ValueError, random.LFSR, ["B", "A", "D", "S", "E", "E", "D"], (1, 2, 3), 32) 45 | self.assertRaises(ValueError, l.next_block, "BAD_OUTPUT_FORMAT") 46 | self.assertRaises(ValueError, l.test, "abcd") 47 | self.assertTrue(l.next_block("str")) 48 | self.assertTrue(l.next_block("bin")) 49 | random.LFSR("abcdef", (1, 3, 5, 7), 32) 50 | 51 | def test_random_geffe(self): 52 | g = random.Geffe("1234567890ab") 53 | self.assertEqual(g.next_block("hex", update=False), "f59f02da") 54 | self.assertEqual(g.next_block("hex"), "f59f02da") 55 | self.assertEqual(g.next_block("hex"), "56ef5ba8") 56 | self.assertTrue(g.next_block("str")) 57 | self.assertTrue(g.next_block("bin")) 58 | self.assertTrue(g.next_block("hex")) 59 | g = random.Geffe("".join(map(str, [random.randint(0, 1) for _ in range(96)]))) 60 | g = random.Geffe(["a", "b", "c"]) 61 | self.assertTrue(g.next_block("hex")) 62 | g = random.Geffe([random.randint(0, 1) for _ in range(96)]) 63 | self.assertRaises(ValueError, random.Geffe, key="bad_key_of_length_20") 64 | self.assertRaises(ValueError, random.Geffe, seeds="BAD_SEEDS") 65 | self.assertRaises(ValueError, g.next_block, "BAD_OUTPUT_FORMAT") 66 | 67 | -------------------------------------------------------------------------------- /tests/test_preimports_re.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports regular expression assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import re 6 | 7 | from utils import * 8 | 9 | 10 | class TestPreimportsRe(TestCase): 11 | def test_re_strings_generation(self): 12 | len = lambda g: sum(1 for _ in g) 13 | self.assertEqual(list(re.strings(None)), []) 14 | self.assertTrue(re.randstr(r"abc").startswith("abc")) 15 | word = re.randstr(r"abc(def|ghi|jkl)") 16 | self.assertTrue(any(s in word for s in ["def", "ghi", "jkl"])) 17 | for i in range(20): 18 | self.assertEqual(re.randstr(r"[a]"), "a") 19 | self.assertTrue("a" not in re.randstr(r"[^a]")) 20 | self.assertEqual(len(re.randstrs(r"\D", 10)), 10) 21 | self.assertEqual(len(re.strings(r"[a]")), 1) 22 | self.assertEqual(len(re.randstrs(r"[a]", 10)), 10) 23 | self.assertEqual(len(re.strings(r"[ab]")), 2) 24 | self.assertEqual(len(re.strings(r"^[a]?$")), 2) 25 | self.assertEqual(len(re.randstrs(r"[ab]", 10)), 10) 26 | self.assertEqual(len(re.strings(r"[ab]{2}")), 4) 27 | self.assertEqual(len(re.strings(r"[ab]{1,2}")), 6) 28 | self.assertEqual(len(re.strings(r"[ab]{1,2}[0-3]{3}")), 384) 29 | self.assertTrue(all(s.startswith("a") and any(s.endswith(c) for c in "bcd") for s in re.strings(r"a(b|c|d)"))) 30 | self.assertEqual(re.size(None), 0) 31 | self.assertEqual(re.size(r".*", "inf"), float("inf")) 32 | self.assertEqual(re.size(r"(test)*\1", "inf"), float("inf")) 33 | self.assertEqual(re.size(r"(?:a|b)+", "inf"), float("inf")) 34 | self.assertEqual(re.size(r"[a-z]*", "inf"), float("inf")) 35 | for regex in [r"[ab]{1,3}.", r"(?<=ab)cd", r"(?<=-)\w+", r"([^\s])\1", r"[^\\]", r"(|[0-5])?"]: 36 | g = re.strings(regex) 37 | for i in range(min(50, re.size(regex))): 38 | self.assertIsNotNone(next(g)) 39 | # self.assertEqual(len(re.strings(regex, 1)), re.size(regex, 1)) 40 | for regex in [r"abc(1|2|3){1,3}", r"[ab]{1,2}c[d-eD0-3]"]: 41 | self.assertEqual(len(re.strings(regex)), re.size(regex)) 42 | 43 | -------------------------------------------------------------------------------- /tests/test_preimports_string.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Preimports string assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import string 6 | 7 | from utils import * 8 | 9 | 10 | class TestPreimportsString(TestCase): 11 | def test_extra_string_functions(self): 12 | STR = "this is a test" 13 | self.assertEqual(string.shorten("test"), "test") 14 | self.assertTrue(len(string.shorten(100 * STR)) < len(100 * STR)) 15 | self.assertTrue(string.shorten(100 * STR).endswith("...")) 16 | self.assertTrue(string.shorten(100 * STR, end="|||").endswith("|||")) 17 | self.assertRaises(ValueError, string.shorten, "test", "BAD_LENGTH") 18 | LST = ["base1", "base2", "base10", "base30", "base200"] 19 | l = sorted(LST) 20 | string.sort_natural(l) 21 | self.assertEqual(tuple(LST), tuple(l)) 22 | self.assertEqual(tuple(LST), tuple(string.sorted_natural(l))) 23 | 24 | -------------------------------------------------------------------------------- /tests/test_preimports_virtualenv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Virtual environment assets' tests. 3 | 4 | """ 5 | from tinyscript.preimports import os, virtualenv 6 | 7 | from utils import * 8 | 9 | 10 | REQS = "requirements-venv-test.txt" 11 | VENV = "venv" 12 | VENV2 = "venv2" 13 | 14 | 15 | class TestPreimportsVirtualenv(TestCase): 16 | def test_virtualenv_improvements(self): 17 | with open(REQS, 'w') as f: 18 | f.write("asciistuff") 19 | self.assertRaises(Exception, virtualenv.activate, "venv_does_not_exist") 20 | virtualenv.setup(VENV, REQS) 21 | if exists(REQS): 22 | remove(REQS) 23 | virtualenv.setup(VENV, ["os"]) 24 | virtualenv.install("asciistuff", "-v", progress_bar="off") 25 | os.environ['PIP_REQ_TRACKER'] = "/tmp/does_not_exist" 26 | virtualenv.setup(VENV2, ["sys"]) 27 | self.assertRaises(Exception, virtualenv.install, "sys", error=True) 28 | virtualenv.teardown() 29 | self.assertRaises(Exception, virtualenv.install, "test") 30 | with virtualenv.VirtualEnv(VENV2, remove=True) as venv: 31 | venv.install("asciistuff") 32 | self.assertTrue(venv.is_installed("setuptools")) 33 | self.assertTrue(venv.is_installed("asciistuff")) 34 | self.assertIsNotNone(venv.asciistuff) 35 | venv.install("does_not_exist") 36 | self.assertFalse(venv.is_installed("does_not_exist")) 37 | self.assertRaises(AttributeError, getattr, venv, "does_not_exist") 38 | virtualenv.teardown(VENV) 39 | self.assertRaises(Exception, virtualenv.PipPackage, "os") 40 | 41 | -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Template module assets' tests. 3 | 4 | """ 5 | from tinyscript.template import new, TARGETS, TEMPLATE 6 | 7 | from utils import * 8 | 9 | 10 | class TestTemplate(TestCase): 11 | def test_script_generation(self): 12 | SCR = "test-script" 13 | PY = SCR + ".py" 14 | new(SCR) 15 | self.assertTrue(exists(PY)) 16 | with open(PY) as f: 17 | self.assertIn("from tinyscript import *", f.read()) 18 | for t in TARGETS.keys(): 19 | new(SCR, t) 20 | self.assertTrue(exists(PY)) 21 | with open(PY) as f: 22 | self.assertIn("from {} import {}".format(*t.split(".")), f.read()) 23 | remove() 24 | 25 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """Test utility functions. 3 | 4 | """ 5 | import logging 6 | import sys 7 | from argparse import Action 8 | from io import StringIO 9 | from os import makedirs, remove as os_remove, rmdir as os_rmdir 10 | from os.path import dirname, exists, join 11 | from shutil import rmtree as shutil_rmtree 12 | from time import sleep 13 | from tinyscript.helpers import failsafe, Path, WINDOWS 14 | from tinyscript.argreparse import ArgumentParser 15 | from unittest import TestCase 16 | try: 17 | from unittest.mock import patch as mock_patch 18 | except ImportError: 19 | from mock import patch as mock_patch 20 | 21 | 22 | __all__ = ["args", "dirname", "dummy_function", "dummy_lambda", "dummy_sleep", "exists", "join", "logger", "logging", 23 | "makedirs", "mock_patch", "remove", "rmdir", "rmtree", "sleep", "sys", "temp_stdin", "touch", "temp_stdout", 24 | "tmpf", "FakeLogRecord", "FakeNamespace", "Path", "TestCase", "_FakeParserAction", "FIXTURES", "WINDOWS"] 25 | 26 | 27 | FIXTURES = ArgumentParser._globals_dict = { 28 | '__author__': "John Doe", 29 | '__contributors__': [ 30 | {'author': "James McAdams", 'email': "j.mcadams@hotmail.com"}, 31 | {'author': "h4x0r1234", 'reason': "for his kind testing"}, 32 | {'reason': "won't be displayed (no author and no email specified)"}, 33 | ], 34 | '__copyright__': "test", 35 | '__credits__': "Thanks to Bob for his contribution", 36 | '__doc__': "test tool", 37 | '__details__': "some more information", 38 | '__email__': "john.doe@example.com", 39 | '__examples__': ["-v"], 40 | '__example_limit__': {'main': 1}, 41 | '__license__': "agpl-v3.0", 42 | '__status__': "beta", 43 | '__version__': "1.2.3", 44 | } 45 | 46 | 47 | dummy_lambda = lambda *a, **k: None 48 | remove = failsafe(os_remove) 49 | rmdir = failsafe(os_rmdir) 50 | rmtree = failsafe(shutil_rmtree) 51 | tmpf = lambda name="test", ext="py": ".tinyscript-{}.{}".format(name, ext) 52 | 53 | 54 | def dummy_function(*a, **k): 55 | pass 56 | 57 | 58 | def dummy_sleep(*a, **k): 59 | sleep(2) 60 | return "TEST" 61 | 62 | 63 | def temp_stdin(tc, inputs): 64 | """ Temporary stdin test-case function. """ 65 | stdin = sys.stdin 66 | 67 | def clean(): 68 | sys.stdin = stdin 69 | 70 | tc.addCleanup(clean) 71 | sys.stdin = StringIO(inputs) 72 | 73 | 74 | def temp_stdout(tc): 75 | """ Temporary stdout/stderr test-case function. """ 76 | stdout, stderr = sys.stdout, sys.stderr 77 | 78 | def clean(): 79 | sys.stdout, sys.stderr = stdout, stderr 80 | 81 | tc.addCleanup(clean) 82 | sys.stdout, sys.stderr = StringIO(), StringIO() 83 | 84 | 85 | def touch(*filenames): 86 | """ Dummy touch file function. """ 87 | for fn in filenames: 88 | with open(fn, 'w') as f: 89 | f.write("") 90 | 91 | 92 | class FakeLogRecord(object): 93 | """ Fake log record class for testing logging. """ 94 | def __init__(self): 95 | self.exc_info = None 96 | self.exc_text = "" 97 | self.levelname = "INFO" 98 | self.msecs = 0 99 | self.relativeCreated = 0 100 | self.stack_info = None 101 | 102 | def __str__(self): 103 | return "" 104 | 105 | def getMessage(self): 106 | return "" 107 | 108 | 109 | class FakeNamespace(object): 110 | """ Fake namespace class for testing parsing. """ 111 | _collisions = {} 112 | 113 | 114 | class _FakeParserAction(Action): 115 | """ Fake parser action class for testing parsing. """ 116 | def __init__(self, *args, **kwargs): 117 | super(_FakeParserAction, self).__init__(*args, **kwargs) 118 | 119 | def __call__(self, *args, **kwargs): 120 | pass 121 | 122 | args = FakeNamespace() 123 | logger = logging.getLogger() 124 | logger.addHandler(logging.NullHandler()) 125 | 126 | --------------------------------------------------------------------------------