├── .SRCINFO ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── .travis ├── build_binaries.sh └── install.sh ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT_NOTES.md ├── HomebrewFormula └── kb.rb ├── ISSUE_TEMPLATE.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── PKGBUILD ├── PKGBUILD.stable ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── TODO.md ├── _config.yml ├── docker-compose.yml ├── docker ├── aliases ├── data │ └── .gitkeep └── dockerfile ├── docs ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conf.py ├── html │ └── index.html └── index.rst ├── img ├── kb-banner.png ├── kb_add.gif ├── kb_add_directory.gif ├── kb_add_from_scratch.gif ├── kb_add_title.gif ├── kb_delete_multiple.gif ├── kb_delete_name.gif ├── kb_edit.gif ├── kb_erase.gif ├── kb_export.gif ├── kb_general_demo.gif ├── kb_general_demo_alias.gif ├── kb_grep.gif ├── kb_grep_case_insensitive.gif ├── kb_import.gif ├── kb_list.gif ├── kb_list_category.gif ├── kb_list_tags.gif ├── kb_list_title_zip.gif ├── kb_list_verbose.gif ├── kb_logo.png ├── kb_non_text_demo.gif ├── kb_view.gif ├── kb_view_in_editor.gif ├── kb_view_title.gif └── kb_view_title_nocolor.gif ├── kb ├── __init__.py ├── __main__.py ├── cl_parser.py ├── commands │ ├── __init__.py │ ├── add.py │ ├── delete.py │ ├── edit.py │ ├── erase.py │ ├── export.py │ ├── grep.py │ ├── ingest.py │ ├── search.py │ ├── sync.py │ ├── template.py │ ├── update.py │ └── view.py ├── config.py ├── db.py ├── entities │ └── artifact.py ├── filesystem.py ├── history.py ├── initializer.py ├── main.py ├── opener.py ├── printer │ ├── __init__.py │ ├── grep.py │ ├── search.py │ ├── style.py │ └── template.py ├── styler.py └── viewer.py ├── misc └── rofi-kb-mode.sh ├── release ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── data ├── .kb │ ├── data │ │ ├── pt_ipmi │ │ ├── pt_tls │ │ ├── pt_wifi_tips │ │ └── pth │ └── kb.db └── .kb2 │ └── data │ ├── dir1 │ ├── pt_ipmi │ └── pth │ ├── dir2 │ ├── dir3 │ │ └── pt_wifi2 │ └── pt_wifi_tips │ ├── pt_ipmi │ ├── pt_tls │ ├── pt_wifi_tips │ └── pth ├── test_cl_parser.py ├── test_db.py ├── test_filesystem.py └── test_sanity.py /.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = kb-git 2 | pkgdesc = A command line minimalist knowledge base manager 3 | pkgver = 0 4 | pkgrel = 1 5 | url = https://github.com/gnebbia/kb.git 6 | arch = any 7 | license = GPL3 8 | makedepends = git 9 | provides = kb-git 10 | conflicts = python-kb-git 11 | conflicts = python-kb 12 | conflicts = kb 13 | source = git+https://github.com/gnebbia/kb.git 14 | md5sums = SKIP 15 | 16 | pkgname = kb-git 17 | 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://paypal.me/nebbione', 'https://xscode.com/gnebbia/kb'] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | .aliases 4 | 5 | # Virtual Environment 6 | env/ 7 | venv/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Vim 13 | [._]*.s[a-w][a-z] 14 | [._]s[a-w][a-z] 15 | *.un~ 16 | Session.vim 17 | 18 | # Packages 19 | *.egg 20 | *.egg-info 21 | dist 22 | build 23 | eggs 24 | parts 25 | var 26 | sdist 27 | develop-eggs 28 | .installed.cfg 29 | lib 30 | lib64 31 | 32 | # Installer logs 33 | pip-log.txt 34 | 35 | # Unit test / coverage reports 36 | .coverage 37 | .tox 38 | nosetests.xml 39 | tests/data/ 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # GitHub token file 47 | .pypt/gh-token 48 | 49 | #  50 | .DS_Store 51 | 52 | # vim session files 53 | *.vim 54 | 55 | # docker 56 | docker/data/* 57 | !docker/data/.gitkeep 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: focal 3 | language: python # this works for Linux but is an error on macOS or Windows 4 | jobs: 5 | include: 6 | - name: "Python 3.8.3 on Linux" 7 | python: 3.8 8 | - name: "Python 3.8.5 on macOS" 9 | os: osx 10 | osx_image: xcode12 # Python 3.8.2 running on macOS 10.15.6 11 | language: shell # 'language: python' is an error on Travis CI macOS 12 | # python: 3.8 # 'python:' is ignored on Travis CI macOS 13 | before_install: python3 --version ; pip3 --version ; sw_vers 14 | - name: "Python 3.8.5 on Windows" 15 | os: windows # Windows 10.0.17134 N/A Build 17134 16 | language: shell # 'language: python' is an error on Travis CI Windows 17 | # python: 3.8 # 'python:' is ignored on Travis CI Windows 18 | env: PATH=/c/Python38:/c/Python38/Scripts:$PATH 19 | before_install: 20 | - choco install python --version 3.8.5 # this install takes at least 1 min 30 sec 21 | 22 | install: 23 | - ./.travis/install.sh 24 | 25 | script: 26 | - python --version 27 | - codespell . --ignore-words-list=hist,ba --quiet-level=2 28 | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 29 | - pytest 30 | 31 | before_deploy: 32 | - ./.travis/build_binaries.sh 33 | 34 | deploy: 35 | provider: releases 36 | token: $RELEASE_TOKEN 37 | skip_cleanup: true 38 | file_glob: true 39 | file: 40 | - dist/kb_* 41 | on: 42 | tags: true 43 | name: $TRAVIS_TAG 44 | -------------------------------------------------------------------------------- /.travis/build_binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | V_REMOVED=$(tr -d "v" <<< "$TRAVIS_TAG") 4 | RELEASE_VER=$(tr "." "_" <<< "$V_REMOVED") 5 | 6 | if [ $TRAVIS_OS_NAME = 'osx' ]; then 7 | pyinstaller --paths kb/ --onefile kb/__main__.py -n kb_${RELEASE_VER}_osx 8 | elif [ $TRAVIS_OS_NAME = 'windows' ]; then 9 | pyinstaller --paths kb/ --onefile kb/__main__.py -n kb_${RELEASE_VER}_win 10 | else 11 | pyinstaller --paths kb/ --onefile kb/__main__.py -n kb_${RELEASE_VER}_linux 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /.travis/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $TRAVIS_OS_NAME = 'windows' ]; then 4 | PYTHON="py" 5 | else 6 | PYTHON="python3" 7 | fi 8 | 9 | $PYTHON -m pip install --upgrade pip setuptools 10 | $PYTHON -m pip install -r requirements-dev.txt 11 | $PYTHON -m pip install . 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | gnc 2 | elektroniz 3 | alshapton 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | :Info: This is the changelog for kb. 3 | :Author: gnc 4 | :Copyright: © 2020, gnc. 5 | :License: GPLv3 (see /LICENSE or :doc:`Appendix B `.) 6 | :Date: 2020-08-09 7 | :Version: 0.1.1 8 | 9 | ## Version History 10 | 11 | 0.1.1 12 | 13 | * Initial release. 14 | 15 | 0.1.2 16 | 17 | * Fixed important bugs of 0.1.1 18 | * Refactored 19 | * Added cross-platform binaries 20 | * Included docker image 21 | * Ready for pypi publishing 22 | 23 | 0.1.3 24 | 25 | * Added support for brew (with brew tap) 26 | * Fixed minor bugs 27 | * Fixed update mode '-e' flag 28 | * Fixed Windows bugs 29 | * Fixed cross-platforms tests 30 | * Improved continuous integration 31 | 32 | 0.1.4 33 | 34 | * Added body "-b" function in add 35 | * Fixed bugs related to the update function 36 | * Added templates 37 | * Added schema migration logic 38 | * Updated docs with a TOC 39 | 40 | 0.1.5 41 | 42 | * Solved minor bug related to viewing certain escape sequences 43 | * Added strings helping users for some exceptions 44 | * Implemented a faster edit/view functionality by guessing input id/title 45 | * Updated bash shortcuts 46 | * Updated Documentation 47 | 48 | 0.1.6 49 | 50 | * Added full path list mode (kb list -f) 51 | * Added rofi custom mode 52 | * Added grep matches mode (kb grep "string" -m) 53 | * Fixed important bug in grep mode 54 | * Added sync mode 55 | 56 | 0.1.7 57 | 58 | * Fixed grep bug with pull request 89 59 | * XDG Compliance: moving files to `$XDG_DATA_HOME/kb`, if it exists, and fall back 60 | to `$HOME/.local/share/kb` if that environment variable does not exist 61 | * Added kb to pkgsrc 62 | * Added stdin functionality to add artifacts. For example: 63 | `cat ../script.py | python -m kb add -t mycoolscript` 64 | `cat path/to/script.py | python -m kb add -t mycoolscript -c python_scripts` 65 | * Implemented the confirmation mechanism for artifact removal 66 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nebbionegiuseppe@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | :Info: These are the contribution rules for kb. 4 | :Copyright: 2012-2020, Giuseppe Nebbione 5 | :License: GPLv3 6 | 7 | Do you want to contribute to this project? Great! I’d love to see some help, 8 | but you must comply with some rules. 9 | 10 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL 11 | NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and 12 | "OPTIONAL" in this document are to be interpreted as described in 13 | RFC 2119. 14 | 15 | # Issue reporting 16 | 17 | GitHub Issues are the recommended way to report an issue. If you do not have an 18 | account there, get one or mail me. 19 | 20 | When pasting console sessions, you must paste them fully, *prompt-to-prompt*, 21 | to see all the messages and your input. Trim only stuff that you are 1000% 22 | sure that is not related to the project in question. 23 | 24 | # General preparations, rules and pull process 25 | 26 | ## Prepare 27 | 28 | A GitHub account is recommended. Patches by mail are accepted, but I’d prefer 29 | to work via GitHub. 30 | 31 | 32 | ## Rules 33 | 34 | 1. Commits must have short, informative and logical messages. Signoffs and 35 | long messages are recommended. "Fix #xxx" is required if an issue 36 | exists. 37 | 2. The following fancy Unicode characters should be used when 38 | needed: ``— " " ‘ ’``. ``…`` should not appear in console output, but may 39 | appear elsewhere. 40 | 3. For Python code, use the PEP 8 coding style and PEP 257 documentation style. 41 | For other languages, K&R style applies. Braces are mandatory in all blocks 42 | (even one-line blocks). Braces are on the same lines as class names and 43 | function signatures. Use 4-space indents. 44 | 45 | ## Request a Pull 46 | 47 | Done? Go hit the **Pull Request** button over on GitHub! And if you don’t 48 | use GitHub, ``git format-patch``. Other formats are not accepted. 49 | 50 | Your commit should be pulled up in a (longer) while. If I like it. Because 51 | some commits may be bad. So, do your best not to do those bad commits. 52 | -------------------------------------------------------------------------------- /DEVELOPMENT_NOTES.md: -------------------------------------------------------------------------------- 1 | # Development Notes 2 | 3 | - the command module, will use both filesystem and db to perform actions 4 | - the filesystem module, deals with low-level fs operations 5 | - the db module, deals with database operations 6 | -------------------------------------------------------------------------------- /HomebrewFormula/kb.rb: -------------------------------------------------------------------------------- 1 | class Kb < Formula 2 | include Language::Python::Virtualenv 3 | 4 | desc "Minimalist knowledge base manager" 5 | homepage "https://github.com/gnebbia/kb" 6 | url "https://github.com/gnebbia/kb.git", 7 | tag: "v0.1.7", 8 | revision: "be3aceb291e7e58e0d17eb4ceeac3090353c917f" 9 | license "GPL-3.0-or-later" 10 | 11 | depends_on "python@3.8" 12 | 13 | 14 | resource "attr" do 15 | url "https://files.pythonhosted.org/packages/de/be/ddc7f84d4e087144472a38a373d3e319f51a6faf6e5fc1ae897173675f21/attr-0.3.1.tar.gz" 16 | sha256 "9091548058d17f132596e61fa7518e504f76b9a4c61ca7d86e1f96dbf7d4775d" 17 | end 18 | 19 | resource "attrs" do 20 | url "https://files.pythonhosted.org/packages/f0/cb/80a4a274df7da7b8baf083249b0890a0579374c3d74b5ac0ee9291f912dc/attrs-20.3.0.tar.gz" 21 | sha256 "832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" 22 | end 23 | 24 | resource "colored" do 25 | url "https://files.pythonhosted.org/packages/b2/16/04827e24c14266d9161bd86bad50069fea453fa006c3d2b31da39251184a/colored-1.4.2.tar.gz" 26 | sha256 "056fac09d9e39b34296e7618897ed1b8c274f98423770c2980d829fd670955ed" 27 | end 28 | 29 | resource "gitdb" do 30 | url "https://files.pythonhosted.org/packages/b6/7b/e373bd7e93a5b4bb041b44601fbb96c7029033d6e838e0a6936fe4f25275/gitdb-4.0.6.tar.gz" 31 | sha256 "42535bb16b5db8983e2c4f6a714d29a8feba7165a12addc63e08fc672dfeccb9" 32 | end 33 | 34 | resource "GitPython" do 35 | url "https://files.pythonhosted.org/packages/5f/f2/ea3242d97695451ab1521775a85253e002942d2c8f4519ae1172c0f5f979/GitPython-3.1.14.tar.gz" 36 | sha256 "be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61" 37 | end 38 | 39 | resource "smmap" do 40 | url "https://files.pythonhosted.org/packages/2b/6f/d48bbed5aa971943759f4ede3f12dca40aa7faa44f22bad483de86780508/smmap-3.0.5.tar.gz" 41 | sha256 "84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50" 42 | end 43 | 44 | resource "toml" do 45 | url "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz" 46 | sha256 "b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 47 | end 48 | 49 | def install 50 | virtualenv_install_with_resources 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: 17 | - Subsystem: 18 | 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft kb 2 | graft docs 3 | graft tests 4 | include README.rst AUTHORS LICENSE CHANGELOG.rst setup.py setup.cfg requirements.txt 5 | global-exclude __pycache__ *.pyc 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default, lint 2 | 3 | default: 4 | python -m kb 5 | spell: 6 | codespell . --ignore-words-list=hist --skip=./.* --quiet-level=2 || true 7 | lint: 8 | pylint kb 9 | pep8: 10 | autopep8 kb --in-place --recursive --aggressive --aggressive 11 | clean: 12 | rm -rf build/ dist/ kb_manager.egg-info/ 13 | test: 14 | codespell . --ignore-words-list=hist,ba --quiet-level=2 15 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 16 | pytest 17 | reinstall: 18 | pip uninstall kb 19 | pyenv rehash 20 | pip install . 21 | pyenv rehash 22 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Giuseppe Nebbione 2 | 3 | pkgname=kb-git 4 | _reponame="kb" 5 | pkgver=r180.40d361d 6 | pkgrel=1 7 | pkgdesc="A command line minimalist knowledge base manager" 8 | arch=(any) 9 | url="https://github.com/gnebbia/kb.git" 10 | license=('GPL3') 11 | depends=('python' 'python-colored' 'python-gitpython' 'python-toml' 'python-attrs') 12 | makedepends=('git') 13 | provides=("kb-git") 14 | conflicts=("python-kb-git" "python-kb" "kb") 15 | source=("git+$url") 16 | md5sums=('') 17 | 18 | pkgver() { 19 | cd "$srcdir/${_reponame}" 20 | printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" 21 | } 22 | 23 | package() { 24 | cd "$srcdir/${_reponame}" 25 | python setup.py install --root="${pkgdir}/" --optimize=1 26 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" 27 | } 28 | 29 | -------------------------------------------------------------------------------- /PKGBUILD.stable: -------------------------------------------------------------------------------- 1 | # Maintainer: Giuseppe Nebbione 2 | 3 | pkgbase='kb' 4 | pkgname=('kb') 5 | _module='kb-manager' 6 | pkgver='0.1.7' 7 | pkgrel=1 8 | pkgdesc="A command line minimalist knowledge base manager" 9 | url="https://github.com/gnebbia/kb" 10 | depends=('python' 'python-colored' 'python-gitpython' 'python-toml' 'python-attrs') 11 | makedepends=('python-setuptools') 12 | license=('GPL3') 13 | arch=('any') 14 | source=("https://files.pythonhosted.org/packages/source/${_module::1}/$_module/$_module-$pkgver.tar.gz") 15 | sha256sums=('c8e79e856066e271f3d7196d8e0d1db297e181a125b521f5943f60f3f3f01e0a') 16 | 17 | build() { 18 | cd "${srcdir}/${_module}-${pkgver}" 19 | python setup.py build 20 | } 21 | 22 | package() { 23 | depends+=() 24 | cd "${srcdir}/${_module}-${pkgver}" 25 | python setup.py install --root="${pkgdir}" --optimize=1 --skip-build 26 | } 27 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | What does this implement/fix? Explain your changes. 2 | --------------------------------------------------- 3 | ... 4 | 5 | Does this close any currently open issues? 6 | ------------------------------------------ 7 | ... 8 | 9 | 10 | Any relevant logs, error output, etc? 11 | ------------------------------------- 12 | (If it’s long, please paste to https://ghostbin.com/ or https://bpaste.net and insert the link here.) 13 | 14 | Any other comments? 15 | ------------------- 16 | ... 17 | 18 | Where has this been tested? 19 | --------------------------- 20 | 21 | **Operating System:** ... 22 | 23 | **Platform:** ... 24 | 25 | **Target Platform:** ... 26 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO List 2 | 3 | This document shows the product roadmap for kb as a point-in-time. 4 | 5 | No guarantees can be given that a specific feature will be included in a release, nor can any definitive timeframe for any release be given. 6 | 7 | # Next Major Release 1.0: 8 | 9 | - The create kb database queries should be abstracted 10 | - Find a way to load custom templates by artifact (maybe store it in db?) (DONE) 11 | - Distributed API to use kb (kb as a service) (alshapton is working on this 12 | - Abstract kb database login 13 | - Update Instructions 0.x -> 1.0 14 | - Docker fixes - Alpine Linux/Vim/Nano/Python3 + dependencies + kb 15 | 16 | 17 | # Later Releases: 18 | 19 | - Include security checking and auto updates (dependabot) 20 | - Consider future of recent.hist file 21 | - Full test suite 22 | - Find a way to include complex artifacts 23 | - Write installation procedure for Windows (choco and scoop) 24 | - Add mobile application 25 | - Web application 26 | - SQLite change 27 | - Fix notepad (Windows) hell with automatic extensions... 28 | - Executable standalone - Pyinstaller 29 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | kb: 5 | build: 6 | context: ./ 7 | dockerfile: ./docker/dockerfile 8 | volumes: 9 | - ./docker/data:/data 10 | tty: true 11 | -------------------------------------------------------------------------------- /docker/aliases: -------------------------------------------------------------------------------- 1 | alias kbl="kb list" 2 | alias kbe="kb edit --id" 3 | alias kba="kb add" 4 | alias kbv="kb view --id" 5 | alias kbd="kb delete --id" 6 | alias kbg="kb grep" 7 | alias kbt="kb list --tags" 8 | -------------------------------------------------------------------------------- /docker/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/docker/data/.gitkeep -------------------------------------------------------------------------------- /docker/dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | COPY . /app 4 | 5 | WORKDIR /app 6 | 7 | RUN pip install -r requirements.txt && \ 8 | python setup.py install && \ 9 | cat /app/docker/aliases >> ~/.bashrc 10 | 11 | WORKDIR /data 12 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | :Info: This is the changelog for kb. 3 | :Author: gnc 4 | :Copyright: © 2020, gnc. 5 | :License: GPLv3 (see /LICENSE or :doc:`Appendix B `.) 6 | :Date: 2020-08-09 7 | :Version: 0.1.1 8 | 9 | ## Version History 10 | 11 | 0.1.1 12 | 13 | * Initial release. 14 | 15 | 0.1.2 16 | 17 | * Fixed important bugs of 0.1.1 18 | * Refactored 19 | * Added cross-platform binaries 20 | * Included docker image 21 | * Ready for pypi publishing 22 | 23 | 0.1.3 24 | 25 | * Added support for brew (with brew tap) 26 | * Fixed minor bugs 27 | * Fixed update mode '-e' flag 28 | * Fixed Windows bugs 29 | * Fixed cross-platforms tests 30 | * Improved continuous integration 31 | 32 | 0.1.4 33 | 34 | * Added body "-b" function in add 35 | * Fixed bugs related to the update function 36 | * Added templates 37 | * Added schema migration logic 38 | * Updated docs with a TOC 39 | 40 | 0.1.5 41 | 42 | * Solved minor bug related to viewing certain escape sequences 43 | * Added strings helping users for some exceptions 44 | * Implemented a faster edit/view functionality by guessing input id/title 45 | * Updated bash shortcuts 46 | * Updated Documentation 47 | 48 | 0.1.6 49 | 50 | * Added full path list mode (kb list -f) 51 | * Added rofi custom mode 52 | * Added grep matches mode (kb grep "string" -m) 53 | * Fixed important bug in grep mode 54 | * Added sync mode 55 | 56 | 0.1.7 57 | 58 | * Fixed grep bug with pull request 89 59 | * XDG Compliance: moving files to `$XDG_DATA_HOME/kb`, if it exists, and fall back 60 | to `$HOME/.local/share/kb` if that environment variable does not exist 61 | * Added kb to pkgsrc 62 | * Added stdin functionality to add artifacts. For example: 63 | `cat ../script.py | python -m kb add -t mycoolscript` 64 | `cat path/to/script.py | python -m kb add -t mycoolscript -c python_scripts` 65 | * Implemented the confirmation mechanism for artifact removal 66 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | :Info: These are the contribution rules for kb. 4 | :Copyright: 2012-2020, Giuseppe Nebbione 5 | :License: GPLv3 6 | 7 | Do you want to contribute to this project? Great! I’d love to see some help, 8 | but you must comply with some rules. 9 | 10 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL 11 | NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and 12 | "OPTIONAL" in this document are to be interpreted as described in 13 | RFC 2119. 14 | 15 | # Issue reporting 16 | 17 | GitHub Issues are the recommended way to report an issue. If you do not have an 18 | account there, get one or mail me. 19 | 20 | When pasting console sessions, you must paste them fully, *prompt-to-prompt*, 21 | to see all the messages and your input. Trim only stuff that you are 1000% 22 | sure that is not related to the project in question. 23 | 24 | # General preparations, rules and pull process 25 | 26 | ## Prepare 27 | 28 | A GitHub account is recommended. Patches by mail are accepted, but I’d prefer 29 | to work via GitHub. 30 | 31 | 32 | ## Rules 33 | 34 | 1. Commits must have short, informative and logical messages. Signoffs and 35 | long messages are recommended. "Fix #xxx" is required if an issue 36 | exists. 37 | 2. The following fancy Unicode characters should be used when 38 | needed: ``— " " ‘ ’``. ``…`` should not appear in console output, but may 39 | appear elsewhere. 40 | 3. For Python code, use the PEP 8 coding style and PEP 257 documentation style. 41 | For other languages, K&R style applies. Braces are mandatory in all blocks 42 | (even one-line blocks). Braces are on the same lines as class names and 43 | function signatures. Use 4-space indents. 44 | 45 | ## Request a Pull 46 | 47 | Done? Go hit the **Pull Request** button over on GitHub! And if you don’t 48 | use GitHub, ``git format-patch``. Other formats are not accepted. 49 | 50 | Your commit should be pulled up in a (longer) while. If I like it. Because 51 | some commits may be bad. So, do your best not to do those bad commits. 52 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # kb. A minimalist knowledge base manager 6 | 7 | 8 | Author: gnc 9 | 10 | Copyright: © 2020, gnc 11 | 12 | Date: 2022-09-21 13 | 14 | Version: 0.1.7 15 | 16 | 17 | ## Purpose 18 | 19 | kb is a text-oriented minimalist command line knowledge base manager. kb 20 | can be considered a quick note collection and access tool oriented toward 21 | software developers, penetration testers, hackers, students or whoever 22 | has to collect and organize notes in a clean way. Although kb is mainly 23 | targeted on text-based note collection, it supports non-text files as well 24 | (e.g., images, pdf, videos and others). 25 | 26 | The project was born from the frustration of trying to find a good way 27 | to quickly access my notes, procedures, cheatsheets and lists (e.g., 28 | payloads) but at the same time, keeping them organized. This is 29 | particularly useful for any kind of student. I use it in the context 30 | of penetration testing to organize pentesting procedures, cheatsheets, 31 | payloads, guides and notes. 32 | 33 | I found myself too frequently spending time trying to search for that 34 | particular payload list quickly, or spending too much time trying to find 35 | a specific guide/cheatsheet for a needed tool. kb tries to solve this 36 | problem by providing you a quick and intuitive way to access knowledge. 37 | 38 | In few words kb allows a user to quickly and efficiently: 39 | 40 | - collect items containing notes,guides,procedures,cheatsheets into 41 | an organized knowledge base; 42 | - filter the knowledge base on different metadata: title, category, 43 | tags and others; 44 | - visualize items within the knowledge base with (or without) syntax 45 | highlighting; 46 | - grep through the knowledge base using regexes; 47 | - import/export an entire knowledge base; 48 | 49 | Basically, kb provides a clean text-based way to organize your knowledge. 50 | 51 | 52 | ## Installation 53 | 54 | **You should have Python 3.6 or above installed.** 55 | 56 | To install the most recent stable version of kb just type: 57 | ```sh 58 | pip install -U kb-manager 59 | ``` 60 | 61 | If you want to install the bleeding-edge version of kb (that may have 62 | some bugs) you should do: 63 | ```sh 64 | git clone https://github.com/gnebbia/kb 65 | cd kb 66 | pip install -r requirements.txt 67 | python setup.py install 68 | 69 | # or with pip 70 | pip install -U git+https://github.com/gnebbia/kb 71 | ``` 72 | 73 | **Tip** for GNU/Linux and MacOS users: For a better user experience, 74 | also set the following kb bash aliases: 75 | ```sh 76 | cat < ~/.kb_alias 77 | alias kbl="kb list" 78 | alias kbe="kb edit" 79 | alias kba="kb add" 80 | alias kbv="kb view" 81 | alias kbd="kb delete --id" 82 | alias kbg="kb grep" 83 | alias kbt="kb list --tags" 84 | EOF 85 | echo "source ~/.kb_alias" >> ~/.bashrc 86 | source ~/.kb_alias 87 | ``` 88 | 89 | Please remember to upgrade kb frequently by doing: 90 | ```sh 91 | pip install -U kb-manager 92 | ``` 93 | 94 | ### Installation with homebrew 95 | 96 | To install using homebrew, use: 97 | ```sh 98 | brew tap gnebbia/kb https://github.com/gnebbia/kb.git 99 | brew install gnebbia/kb/kb 100 | ``` 101 | 102 | To upgrade with homebrew: 103 | ```sh 104 | brew update 105 | brew upgrade gnebbia/kb/kb 106 | ``` 107 | 108 | ### Installation from AUR 109 | 110 | Arch Linux users can install [kb](https://aur.archlinux.org/packages/kb) or [kb-git](https://aur.archlinux.org/packages/kb-git) with their favorite [AUR Helper](https://wiki.archlinux.org/index.php/AUR_helpers). 111 | 112 | Stable: 113 | ```sh 114 | yay -S kb 115 | ``` 116 | 117 | Dev: 118 | ```sh 119 | yay -S kb-git 120 | ``` 121 | 122 | ### Notes for Windows users 123 | 124 | Windows users should keep in mind these things: 125 | - DO NOT USE notepad as %EDITOR%, kb is not compatible with notepad, 126 | a reasonable alternative is notepad++; 127 | - %EDITOR% variable should ALWAYS be enclosed within double quotes; 128 | ```sh 129 | EDITOR=C:\Program Files\Editor\my cool editor.exe -> WRONG! 130 | EDITOR="C:\Program Files\Editor\my cool editor.exe" -> OK! 131 | ``` 132 | 133 | To set the "EDITOR" Environment variable by using cmd.exe, just issue 134 | the following commands, after having inserted the path to your desired 135 | text editor: 136 | ```sh 137 | set EDITOR="C:\path\to\editor\here.exe" 138 | setx EDITOR "\"C:\path\to\editor\here.exe\"" 139 | ``` 140 | 141 | To set the "EDITOR" Environment variable by using Powershell, just issue 142 | the following commands, after having inserted the path to your desired 143 | text editor: 144 | ```sh 145 | $env:EDITOR='"C:\path\to\editor\here.exe"' 146 | [System.Environment]::SetEnvironmentVariable('EDITOR','"C:\path\to\editor\here.exe"', [System.EnvironmentVariableTarget]::User) 147 | ``` 148 | 149 | #### Setting Aliases for cmd 150 | 151 | Open a cmd.exe terminal with administrative rights and paste 152 | the following commands: 153 | ```sh 154 | reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor" /v "AutoRun" /t REG_EXPAND_SZ /d "%USERPROFILE%\autorun.cmd" 155 | ( 156 | echo @echo off 157 | echo doskey kbl=kb list $* 158 | echo doskey kbe=kb edit $* 159 | echo doskey kba=kb add $* 160 | echo doskey kbv=kb view $* 161 | echo doskey kbd=kb delete --id $* 162 | echo doskey kbg=kb grep $* 163 | echo doskey kbt=kb list --tags $* 164 | )> %USERPROFILE%\autorun.cmd 165 | ``` 166 | 167 | #### Setting Aliases for Powershell 168 | 169 | Open a Powershell terminal and paste the following commands: 170 | ```sh 171 | @' 172 | function kbl { kb list $args } 173 | function kbe { kb edit $args } 174 | function kba { kb add $args } 175 | function kbv { kb view $args } 176 | function kbd { kb delete --id $args } 177 | function kbg { kb grep $args } 178 | function kbt { kb list --tags $args } 179 | '@ > $env:USERPROFILE\Documents\WindowsPowerShell\profile.ps1 180 | ``` 181 | 182 | 183 | ## Docker 184 | 185 | A docker setup has been included to help with development. 186 | 187 | To install and start the project with docker: 188 | ```sh 189 | docker-compose up -d 190 | docker-compose exec kb bash 191 | ``` 192 | 193 | The container has the aliases included in its `.bashrc` so you can use 194 | kb in the running container as you would if you installed it on the 195 | host directly. The `./docker/data` directory on the host is bound to 196 | `/data` in the container, which is the image's working directly also. 197 | To interact with the container, place (or symlink) the files on your host 198 | into the `./docker/data` directory, which can then be seen and used in 199 | the `/data` directory in the container. 200 | 201 | ## Usage 202 | 203 | 204 | 205 | ### List artifacts 206 | 207 | #### List all artifacts contained in the kb knowledge base 208 | ```sh 209 | kb list 210 | 211 | # or if aliases are used: 212 | kbl 213 | ``` 214 | 215 | #### List all artifacts containing the string "zip" 216 | ```sh 217 | kb list zip 218 | 219 | # or if aliases are used: 220 | kbl zip 221 | ``` 222 | 223 | #### List all artifacts belonging to the category "cheatsheet" 224 | ```sh 225 | kb list --category cheatsheet 226 | # or 227 | kb list -c cheatsheet 228 | 229 | # or if aliases are used: 230 | kbl -c cheatsheet 231 | ``` 232 | 233 | #### List all the artifacts having the tags "web" or "pentest" 234 | ```sh 235 | kb list --tags "web;pentest" 236 | 237 | # or if aliases are used: 238 | kbl --tags "web;pentest" 239 | ``` 240 | 241 | #### List using "verbose mode" 242 | ```sh 243 | kb list -v 244 | 245 | # or if aliases are used: 246 | kbl -v 247 | ``` 248 | 249 | 250 | ### Add artifacts 251 | 252 | #### Add a file to the collection of artifacts 253 | ```sh 254 | kb add ~/Notes/cheatsheets/pytest 255 | 256 | # or if aliases are used: 257 | kba ~/Notes/cheatsheets/pytest 258 | ``` 259 | 260 | #### Add a file to the artifacts 261 | ```sh 262 | kb add ~/ssh_tunnels --title pentest_ssh --category "procedure" \ 263 | --tags "pentest;network" --author "gnc" --status "draft" 264 | ``` 265 | 266 | #### Add all files contained in a directory to kb 267 | ```sh 268 | kb add ~/Notes/cheatsheets/general/* --category "cheatsheet" 269 | ``` 270 | 271 | #### Create a new artifact from scratch 272 | ```sh 273 | kb add --title "ftp" --category "notes" --tags "protocol;network" 274 | # a text editor ($EDITOR) will be launched for editing 275 | ``` 276 | 277 | #### Create a new artifact from the output of another program 278 | ```sh 279 | kb add --title "my_network_scan" --category "scans" --body "$(nmap -T5 -p80 192.168.1.0/24)" 280 | ``` 281 | 282 | ### Delete artifacts 283 | 284 | #### Delete an artifact by ID 285 | ```sh 286 | kb delete --id 2 287 | 288 | # or if aliases are used: 289 | kbd 2 290 | ``` 291 | 292 | #### Delete multiple artifacts by ID 293 | ```sh 294 | kb delete --id 2 3 4 295 | 296 | # or if aliases are used: 297 | kbd 2 3 4 298 | ``` 299 | 300 | #### Delete an artifact by name 301 | ```sh 302 | kb delete --title zap --category cheatsheet 303 | ``` 304 | 305 | 306 | ### View artifacts 307 | 308 | #### View an artifact by id 309 | ```sh 310 | kb view --id 3 311 | # or 312 | kb view -i 3 313 | # or 314 | kb view 3 315 | 316 | # or if aliases are used: 317 | kbv 3 318 | ``` 319 | 320 | #### View an artifact by name 321 | ```sh 322 | kb view --title "gobuster" 323 | # or 324 | kb view -t "gobuster" 325 | # or 326 | kb view gobuster 327 | ``` 328 | 329 | #### View an artifact without colors 330 | ```sh 331 | kb view -t dirb -n 332 | ``` 333 | 334 | #### View an artifact within a text-editor 335 | ```sh 336 | kb view -i 2 -e 337 | 338 | # or if aliases are used: 339 | kbv 2 -e 340 | ``` 341 | 342 | 343 | ### Edit artifacts 344 | 345 | Editing artifacts involves opening a text editor. 346 | Hence, binary files cannot be edited by kb. 347 | 348 | The editor can be set by the "EDITOR" environment 349 | variable. 350 | 351 | #### Edit an artifact by id 352 | ```sh 353 | kb edit --id 13 354 | # or 355 | kbe 13 356 | # or if aliases are used: 357 | kbe 13 358 | ``` 359 | 360 | #### Edit an artifact by name 361 | ```sh 362 | kb edit --title "git" --category "cheatsheet" 363 | # or 364 | kb edit -t "git" -c "cheatsheet" 365 | # or if git is unique as artifact 366 | kb edit git 367 | ``` 368 | 369 | ### Grep through artifacts 370 | 371 | #### Grep through the knowledge base 372 | ```sh 373 | kb grep "[bg]zip" 374 | 375 | # or if aliases are used: 376 | kbg "[bg]zip" 377 | ``` 378 | 379 | #### Grep (case-insensitive) through the knowledge base 380 | ```sh 381 | kb grep -i "[BG]ZIP" 382 | ``` 383 | 384 | #### Grep in "verbose mode" through the knowledge base 385 | ```sh 386 | kb grep -v "[bg]zip" 387 | ``` 388 | 389 | #### Grep through the knowledge base and show matching lines 390 | ```sh 391 | kb grep -m "[bg]zip" 392 | ``` 393 | 394 | ### Import/Export/Erase a knowledge base 395 | 396 | #### Export the current knowledge base 397 | 398 | To export the entire knowledge base, do: 399 | ```sh 400 | kb export 401 | ``` 402 | This will generate a .kb.tar.gz archive that can 403 | be later be imported by kb. 404 | 405 | If you want to export only data (so that it can be used in other software): 406 | 407 | ```sh 408 | kb export --only-data 409 | ``` 410 | 411 | This will export a directory containing a subdirectory for each category 412 | and within these subdirectories we will have all the artifacts belonging 413 | to that specific category. 414 | 415 | #### Import a knowledge base 416 | ```sh 417 | kb import archive.kb.tar.gz 418 | ``` 419 | **NOTE**: Importing a knowledge base erases all the previous 420 | data. Basically it erases everything and imports the new knowledge base. 421 | 422 | #### Erase the entire knowledge base 423 | ```sh 424 | kb erase 425 | ``` 426 | 427 | 428 | ### Manage Templates 429 | 430 | kb supports custom templates for the artifacts. 431 | A template is basically a file using the "toml" format, 432 | structured in this way: 433 | ```sh 434 | TITLES = [ "^#.*", "blue", ] 435 | WARNINGS = [ "!.*" , "yellow",] 436 | COMMENTS = [ ";;.*", "green", ] 437 | ``` 438 | 439 | Where the first element of each list is a regex and the second element 440 | is a color. 441 | 442 | Note that by default an artifact is assigned with the 'default' 443 | template, and this template can be changed too (look at "Edit a template" 444 | subsection). 445 | 446 | 447 | #### List available templates 448 | 449 | To list all available templates: 450 | ```sh 451 | kb template list 452 | ``` 453 | 454 | To list all the templates containing the string "theory": 455 | ```sh 456 | kb template list "theory" 457 | ``` 458 | 459 | #### Create a new template 460 | 461 | Create a new template called "lisp-cheatsheets", note that 462 | an example template will be put as example in the editor. 463 | ```sh 464 | kb template new lisp-cheatsheets 465 | ``` 466 | 467 | #### Delete a template 468 | 469 | To delete the template called "lisp-cheatsheets" just do: 470 | ```sh 471 | kb template delete lisp-cheatsheets 472 | ``` 473 | 474 | #### Edit a template 475 | 476 | To edit the template called "listp-cheatsheets" just do: 477 | ```sh 478 | kb template edit lisp-cheatsheets 479 | ``` 480 | 481 | #### Add a template 482 | 483 | We can also add a template from an already existing toml configuration file 484 | by just doing: 485 | ```sh 486 | kb template add ~/path/to/myconfig.toml --title myconfig 487 | ``` 488 | 489 | #### Change template for an artifact 490 | 491 | We can change the template for an existing artifact by ID by using the 492 | update command: 493 | ```sh 494 | kb update --id 2 --template "lisp-cheatsheets" 495 | ``` 496 | 497 | #### Apply a template to all artifacts of a category 498 | 499 | We can apply the template "lisp-cheatsheets" to all artifacts 500 | belonging to the category "lispcode" by doing: 501 | ```sh 502 | kb template apply "lisp-cheatsheets" --category "lispcode" 503 | ``` 504 | 505 | #### Apply a template to all artifacts having zip in their title 506 | 507 | We can apply the template "dark" to all artifacts having in their title 508 | the string "zip" (e.g., bzip, 7zip, zipper) by doing: 509 | ```sh 510 | kb template apply "dark" --title "zip" --extended-match 511 | # or 512 | kb template apply "dark" --title "zip" -m 513 | ``` 514 | We can always have our queries to "contain" the string by using 515 | the `--extended-match` option when using `kb template apply`. 516 | 517 | #### Apply a template to all artifacts having specific properties 518 | 519 | We can apply the template "light" to all artifacts of the category 520 | "cheatsheet" who have as author "gnc" and as status "OK" by doing: 521 | ```sh 522 | kb template apply "light" --category "cheatsheet" --author "gnc" --status "OK" 523 | ``` 524 | 525 | ### Integrating kb with other tools 526 | 527 | kb can be integrated with other tools. 528 | 529 | #### kb and rofi 530 | 531 | We can integrate kb with rofi, a custom mode has been developed 532 | accessible in the "misc" directory within this repository. 533 | 534 | We can launch rofi with this mode by doing: 535 | 536 | ```sh 537 | rofi -show kb -modi kb:/path/to/rofi-kb-mode.sh 538 | ``` 539 | 540 | ### Experimental 541 | 542 | #### Synchronize kb with a remote git repository 543 | 544 | Synchronization with a remote git repository is experimental at the moment. 545 | Anyway we can initialize our knowledge base to a created empty 546 | github/gitlab (other git service) repository by doing: 547 | ```sh 548 | kb sync init 549 | ``` 550 | 551 | We can then push our knowledge base to the remote git repository with: 552 | ```sh 553 | kb sync push 554 | ``` 555 | 556 | We can pull (e.g., from another machine) our knowledge base from the 557 | remote git repository with: 558 | ```sh 559 | kb sync pull 560 | ``` 561 | 562 | We can at any time view to what remote endpoint our knowledge is synchronizing 563 | to with: 564 | ```sh 565 | kb sync info 566 | ``` 567 | 568 | 569 | ## UPGRADE 570 | 571 | If you want to upgrade kb to the most recent stable release do: 572 | ```sh 573 | pip install -U kb-manager 574 | ``` 575 | 576 | If instead you want to update kb to the most recent release 577 | (that may be bugged), do: 578 | ```sh 579 | git clone https://github.com/gnebbia/kb 580 | cd kb 581 | pip install --upgrade . 582 | ``` 583 | 584 | ## DONATIONS 585 | 586 | I am an independent developer working on kb in my free time, 587 | if you like kb and would like to say thank you, buy me a beer! 588 | 589 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://paypal.me/nebbione) 590 | 591 | ## COPYRIGHT 592 | 593 | Copyright 2020 Giuseppe Nebbione. 594 | 595 | This program is free software: you can redistribute it and/or modify 596 | it under the terms of the GNU General Public License as published by 597 | the Free Software Foundation, either version 3 of the License, or 598 | (at your option) any later version. 599 | 600 | This program is distributed in the hope that it will be useful, 601 | but WITHOUT ANY WARRANTY; without even the implied warranty of 602 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 603 | GNU General Public License for more details. 604 | 605 | You should have received a copy of the GNU General Public License 606 | along with this program. If not, see . 607 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # kb documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Dec 14 21:02:58 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'kb' 44 | copyright = u'2020, gnc' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1.7' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1.7' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'kbdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'kb.tex', u'kb Documentation', 182 | u'gnc', 'manual'), 183 | ] 184 | 185 | latex_elements = {'papersize': 'a4paper', 'fontpkg': '\\usepackage{tgheros}', 186 | 'fncychap': '\\usepackage[Sonny]{fncychap}'} 187 | 188 | # The name of an image file (relative to this directory) to place at the top of 189 | # the title page. 190 | #latex_logo = None 191 | 192 | # For "manual" documents, if this is true, then toplevel headings are parts, 193 | # not chapters. 194 | #latex_use_parts = False 195 | 196 | # If true, show page references after internal links. 197 | #latex_show_pagerefs = False 198 | 199 | # If true, show URL addresses after external links. 200 | #latex_show_urls = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'kb', u'kb Documentation', 218 | [u'gnc'], 1) 219 | ] 220 | 221 | 222 | # Example configuration for intersphinx: refer to the Python standard library. 223 | intersphinx_mapping = {'http://docs.python.org/': None} 224 | -------------------------------------------------------------------------------- /docs/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to Read The Docs 4 | 5 | The docs are at kb.readthedocs.org. 6 | You will be redirected there in a while. If not, click the link above. 7 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | kb 3 | =============================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | README for kb 9 | CONTRIBUTING 10 | LICENSE 11 | CHANGELOG 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | -------------------------------------------------------------------------------- /img/kb-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb-banner.png -------------------------------------------------------------------------------- /img/kb_add.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_add.gif -------------------------------------------------------------------------------- /img/kb_add_directory.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_add_directory.gif -------------------------------------------------------------------------------- /img/kb_add_from_scratch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_add_from_scratch.gif -------------------------------------------------------------------------------- /img/kb_add_title.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_add_title.gif -------------------------------------------------------------------------------- /img/kb_delete_multiple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_delete_multiple.gif -------------------------------------------------------------------------------- /img/kb_delete_name.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_delete_name.gif -------------------------------------------------------------------------------- /img/kb_edit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_edit.gif -------------------------------------------------------------------------------- /img/kb_erase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_erase.gif -------------------------------------------------------------------------------- /img/kb_export.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_export.gif -------------------------------------------------------------------------------- /img/kb_general_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_general_demo.gif -------------------------------------------------------------------------------- /img/kb_general_demo_alias.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_general_demo_alias.gif -------------------------------------------------------------------------------- /img/kb_grep.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_grep.gif -------------------------------------------------------------------------------- /img/kb_grep_case_insensitive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_grep_case_insensitive.gif -------------------------------------------------------------------------------- /img/kb_import.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_import.gif -------------------------------------------------------------------------------- /img/kb_list.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_list.gif -------------------------------------------------------------------------------- /img/kb_list_category.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_list_category.gif -------------------------------------------------------------------------------- /img/kb_list_tags.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_list_tags.gif -------------------------------------------------------------------------------- /img/kb_list_title_zip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_list_title_zip.gif -------------------------------------------------------------------------------- /img/kb_list_verbose.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_list_verbose.gif -------------------------------------------------------------------------------- /img/kb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_logo.png -------------------------------------------------------------------------------- /img/kb_non_text_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_non_text_demo.gif -------------------------------------------------------------------------------- /img/kb_view.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_view.gif -------------------------------------------------------------------------------- /img/kb_view_in_editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_view_in_editor.gif -------------------------------------------------------------------------------- /img/kb_view_title.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_view_title.gif -------------------------------------------------------------------------------- /img/kb_view_title_nocolor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/img/kb_view_title_nocolor.gif -------------------------------------------------------------------------------- /kb/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright 2020, Giuseppe Nebbione. 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """ 20 | A knowledge base organizer 21 | 22 | :Copyright: © 2020, gnc. 23 | :License: GPLv3 (see /LICENSE). 24 | """ 25 | 26 | __title__ = 'kb' 27 | __version__ = '0.1.7' 28 | __author__ = 'gnc' 29 | __license__ = 'GPLv3' 30 | __docformat__ = 'restructuredtext en' 31 | 32 | __all__ = () 33 | 34 | # import gettext 35 | # G = gettext.translation('kb', '/usr/share/locale', fallback='C') 36 | # _ = G.gettext 37 | -------------------------------------------------------------------------------- /kb/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | Main routine of kb. 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | from kb.main import main 14 | 15 | 16 | __all__ = ('main',) 17 | 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /kb/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/kb/commands/__init__.py -------------------------------------------------------------------------------- /kb/commands/add.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb add command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import shlex 15 | import sys 16 | from pathlib import Path 17 | from subprocess import call 18 | from typing import Dict 19 | import kb.db as db 20 | import kb.initializer as initializer 21 | import kb.filesystem as fs 22 | from kb.entities.artifact import Artifact 23 | 24 | 25 | def add(args: Dict[str, str], config: Dict[str, str]): 26 | """ 27 | Adds a list of artifacts to the knowledge base of kb. 28 | 29 | Arguments: 30 | args: - a dictionary containing the following fields: 31 | file -> a list of files to add to kb 32 | title -> the title assigned to the artifact(s) 33 | category -> the category assigned to the artifact(s) 34 | tags -> the tags assigned to the artifact(s) 35 | author -> the author to assign to the artifact 36 | status -> the status to assign to the artifact 37 | config: - a configuration dictionary containing at least 38 | the following keys: 39 | PATH_KB_DB - the database path of KB 40 | PATH_KB_DATA - the data directory of KB 41 | EDITOR - the editor program to call 42 | """ 43 | # Check if the add command has proper arguments/options 44 | is_valid_add = args["file"] or args["title"] 45 | if not is_valid_add: 46 | print("Please, either specify a file or a title for the new artifact") 47 | sys.exit(1) 48 | 49 | # Check initialization 50 | initializer.init(config) 51 | 52 | conn = db.create_connection(config["PATH_KB_DB"]) 53 | if args["file"]: 54 | for fname in args["file"]: 55 | if fs.is_directory(fname): 56 | continue 57 | add_file_to_kb(conn, args, config, fname) 58 | else: 59 | # Get title for the new artifact 60 | title = args["title"] 61 | 62 | # Assign a "default" category if not provided 63 | category = args["category"] or "default" 64 | 65 | # Create "category" directory if it does not exist 66 | category_path = Path(config["PATH_KB_DATA"], category) 67 | category_path.mkdir(parents=True, exist_ok=True) 68 | 69 | is_stdin_empty = sys.stdin.isatty() 70 | 71 | if not db.is_artifact_existing(conn, title, category): 72 | # If a file is provided, copy the file to kb directory 73 | # otherwise open up the editor and create some content 74 | artifact_path = str(Path(category_path, title)) 75 | if args["body"]: 76 | with open(artifact_path, "w+") as art_file: 77 | body = args["body"].replace("\\n", "\n") 78 | art_file.write(body) 79 | elif not is_stdin_empty: 80 | with open(artifact_path, "w+") as art_file: 81 | body_lines = sys.stdin.readlines() 82 | art_file.writelines(body_lines) 83 | else: 84 | shell_cmd = shlex.split( 85 | config["EDITOR"]) + [artifact_path] 86 | call(shell_cmd) 87 | 88 | new_artifact = Artifact( 89 | id=None, title=title, category=category, 90 | path="{category}/{title}".format(category=category, title=title), 91 | tags=args["tags"], 92 | status=args["status"], author=args["author"], template=args["template"]) 93 | db.insert_artifact(conn, new_artifact) 94 | 95 | 96 | def validate(args): 97 | """ 98 | Validate arguments for the add command 99 | 100 | Arguments: 101 | args - the dictionary of arguments 102 | passed to the add command 103 | 104 | Returns: 105 | A boolean, True if the add command is valid 106 | """ 107 | return bool(args["file"] or args["title"]) 108 | 109 | 110 | def add_file_to_kb( 111 | conn, 112 | args: Dict[str, str], 113 | config: Dict[str, str], 114 | fname: str 115 | ) -> None: 116 | """ 117 | Adds a file to the kb knowledge base. 118 | 119 | Arguments: 120 | conn - the connection to the database object 121 | args - the args dictionary passed to the add command, 122 | it must contain at least the following keys: 123 | title, category, tags, status, author 124 | config - the configuration dictionary that must contain 125 | at least the following key: 126 | PATH_KB_DATA, the path to where artifact are stored 127 | fname - the path of the file to add to kb 128 | """ 129 | title = args["title"] or fs.get_basename(fname) 130 | category = args["category"] or "default" 131 | template = args["template"] or "default" 132 | 133 | category_path = Path(config["PATH_KB_DATA"], category) 134 | category_path.mkdir(parents=True, exist_ok=True) 135 | 136 | try: 137 | fs.copy_file(fname, Path(category_path, title)) 138 | except FileNotFoundError: 139 | print("Error: The specified file does not exist!".format(fname=fname)) 140 | sys.exit(1) 141 | 142 | if not db.is_artifact_existing(conn, title, category): 143 | fs.copy_file(fname, Path(category_path, title)) 144 | 145 | new_artifact = Artifact( 146 | id=None, 147 | title=title, category=category, 148 | path="{category}/{title}".format(category=category, title=title), 149 | tags=args["tags"], 150 | status=args["status"], author=args["author"], template=template) 151 | db.insert_artifact(conn, new_artifact) 152 | -------------------------------------------------------------------------------- /kb/commands/delete.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb delete command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import sys 15 | from typing import Dict 16 | from pathlib import Path 17 | import kb.db as db 18 | import kb.initializer as initializer 19 | import kb.history as history 20 | import kb.filesystem as fs 21 | 22 | 23 | def delete(args: Dict[str, str], config: Dict[str, str]): 24 | """ 25 | Delete a list of artifacts from the kb knowledge base. 26 | 27 | Arguments: 28 | args: - a dictionary containing the following fields: 29 | id -> a list of IDs (the ones you see with kb list) 30 | associated to the artifacts we want to delete 31 | title -> the title assigned to the artifact(s) 32 | category -> the category assigned to the artifact(s) 33 | config: - a configuration dictionary containing at least 34 | the following keys: 35 | PATH_KB_DB - the database path of KB 36 | PATH_KB_DATA - the data directory of KB 37 | PATH_KB_HIST - the history menu path of KB 38 | """ 39 | initializer.init(config) 40 | 41 | if args["id"]: 42 | for i in args["id"]: 43 | delete_by_id(i, args["force"], config) 44 | 45 | elif args["title"]: 46 | delete_by_name(args["title"], args["category"], args["force"], config) 47 | 48 | 49 | def delete_by_id(id: int, is_forced: bool, config: Dict[str, str]): 50 | """ 51 | Edit the content of an artifact by id. 52 | 53 | Arguments: 54 | id: - the ID (the one you see with kb list) 55 | associated to the artifact to delete 56 | config: - a configuration dictionary containing at least 57 | the following keys: 58 | PATH_KB_DB - the database path of KB 59 | PATH_KB_DATA - the data directory of KB 60 | PATH_KB_HIST - the history menu path of KB 61 | EDITOR - the editor program to call 62 | """ 63 | conn = db.create_connection(config["PATH_KB_DB"]) 64 | artifact_id = history.get_artifact_id(config["PATH_KB_HIST"], id) 65 | artifact = db.get_artifact_by_id(conn, artifact_id) 66 | 67 | if not artifact: 68 | print("Error: Invalid artifact referenced") 69 | return 70 | 71 | if not is_forced: 72 | confirm = ask_confirmation(artifact.title,artifact.category) 73 | if not artifact or not confirm: 74 | print("No artifact was removed") 75 | return 76 | 77 | db.delete_artifact_by_id(conn, artifact_id) 78 | 79 | category_path = Path(config["PATH_KB_DATA"], artifact.category) 80 | 81 | try: 82 | Path(category_path, artifact.title).unlink() 83 | except FileNotFoundError: 84 | pass 85 | 86 | if fs.count_files(category_path) == 0: 87 | fs.remove_directory(category_path) 88 | 89 | print("Artifact {category}/{title} removed!".format( 90 | category=artifact.category, title=artifact.title)) 91 | 92 | 93 | def delete_by_name(title: str, category: str, is_forced: bool, config: Dict[str, str]): 94 | """ 95 | Edit the content of an artifact by name, that is title/category 96 | 97 | Arguments: 98 | title: - the title assigned to the artifact to delete 99 | category: - the category assigned to the artifact to delete 100 | config: - a configuration dictionary containing at least 101 | the following keys: 102 | PATH_KB_DB - the database path of KB 103 | PATH_KB_DATA - the data directory of KB 104 | PATH_KB_HIST - the history menu path of KB 105 | EDITOR - the editor program to call 106 | """ 107 | conn = db.create_connection(config["PATH_KB_DB"]) 108 | artifacts = db.get_artifacts_by_filter(conn, title=title, 109 | category=category, 110 | is_strict=True) 111 | if len(artifacts) == 1: 112 | artifact = artifacts.pop() 113 | 114 | if not is_forced: 115 | confirm = ask_confirmation(artifact.title,artifact.category) 116 | if not artifact or not confirm: 117 | print("No artifact was removed") 118 | return 119 | 120 | db.delete_artifact_by_id(conn, artifact.id) 121 | print("Artifact {}/{} removed!".format(artifact.category, artifact.title)) 122 | elif len(artifacts) > 1: 123 | print( 124 | "There is more than one artifact with that title, please specify a category") 125 | else: 126 | print( 127 | "There is no artifact with that name, please specify a correct artifact name") 128 | 129 | 130 | def ask_confirmation(title: str, category: str): 131 | """ 132 | Ask confirmation for the deletion of an artifact 133 | 134 | Arguments: 135 | title: - the title assigned to the artifact to delete 136 | category: - the category assigned to the artifact to delete 137 | 138 | Returns: 139 | A boolean that is true if the user really wants to remove 140 | an artifact, i.e., "y" or "yes" have been typed at the prompt 141 | """ 142 | answer = input( 143 | "Are you sure you want to delete {category}/{title}? [y/n]".format( 144 | category=category, title=title)) 145 | 146 | return not (answer.lower() not in ["y","yes"]) 147 | -------------------------------------------------------------------------------- /kb/commands/edit.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb edit command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import shlex 15 | from pathlib import Path 16 | from subprocess import call 17 | from typing import Dict 18 | import kb.db as db 19 | import kb.initializer as initializer 20 | import kb.history as history 21 | 22 | 23 | def edit(args: Dict[str, str], config: Dict[str, str]): 24 | """ 25 | Edit the content of an artifact. 26 | 27 | Arguments: 28 | args: - a dictionary containing the following fields: 29 | id -> the IDs (the one you see with kb list) 30 | associated to the artifact we want to edit 31 | title -> the title assigned to the artifact(s) 32 | category -> the category assigned to the artifact(s) 33 | config: - a configuration dictionary containing at least 34 | the following keys: 35 | PATH_KB_DB - the database path of KB 36 | PATH_KB_DATA - the data directory of KB 37 | PATH_KB_HIST - the history menu path of KB 38 | EDITOR - the editor program to call 39 | """ 40 | initializer.init(config) 41 | 42 | # if an ID is specified, load artifact with that ID 43 | if args["id"]: 44 | edit_by_id(args["id"], config) 45 | 46 | # else if a title is specified 47 | elif args["title"]: 48 | edit_by_name(args["title"], args["category"], config) 49 | 50 | # else try to guess 51 | elif args["nameid"]: 52 | if args["nameid"].isdigit(): 53 | edit_by_id(args["nameid"], config) 54 | else: 55 | edit_by_name(args["nameid"], args["category"], config) 56 | 57 | 58 | def edit_by_id(id: int, config: Dict[str, str]): 59 | """ 60 | Edit the content of an artifact by id. 61 | 62 | Arguments: 63 | id: - the ID (the one you see with kb list) 64 | associated to the artifact to edit 65 | config: - a configuration dictionary containing at least 66 | the following keys: 67 | PATH_KB_DB - the database path of KB 68 | PATH_KB_DATA - the data directory of KB 69 | PATH_KB_HIST - the history menu path of KB 70 | EDITOR - the editor program to call 71 | """ 72 | conn = db.create_connection(config["PATH_KB_DB"]) 73 | artifact = history.get_artifact( 74 | conn, config["PATH_KB_HIST"], id) 75 | 76 | category_path = Path(config["PATH_KB_DATA"], artifact.category) 77 | 78 | shell_cmd = shlex.split(config["EDITOR"]) + \ 79 | [str(Path(category_path, artifact.title))] 80 | call(shell_cmd) 81 | 82 | 83 | def edit_by_name(title: str, category: str, config: Dict[str, str]): 84 | """ 85 | Edit the content of an artifact by name, that is title/category 86 | 87 | Arguments: 88 | title: - the title assigned to the artifact(s) 89 | category: - the category assigned to the artifact(s) 90 | config: - a configuration dictionary containing at least 91 | the following keys: 92 | PATH_KB_DB - the database path of KB 93 | PATH_KB_DATA - the data directory of KB 94 | PATH_KB_HIST - the history menu path of KB 95 | EDITOR - the editor program to call 96 | """ 97 | conn = db.create_connection(config["PATH_KB_DB"]) 98 | artifacts = db.get_artifacts_by_filter(conn, title=title, 99 | category=category, 100 | is_strict=True) 101 | 102 | if len(artifacts) == 1: 103 | artifact = artifacts.pop() 104 | category_path = Path(config["PATH_KB_DATA"], artifact.category) 105 | shell_cmd = shlex.split( 106 | config["EDITOR"]) + [str(Path(category_path, artifact.title))] 107 | call(shell_cmd) 108 | elif len(artifacts) > 1: 109 | print( 110 | "There is more than one artifact with that title, please specify a category") 111 | else: 112 | print( 113 | "There is no artifact with that name, please specify a correct artifact name") 114 | -------------------------------------------------------------------------------- /kb/commands/erase.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb erase command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | from typing import Dict 15 | import kb.filesystem as fs 16 | 17 | 18 | def erase(args: Dict[str, str], config: Dict[str, str]): 19 | """ 20 | Erase the entire kb knowledge base (or only the database). 21 | 22 | Arguments: 23 | args: - a dictionary containing the following fields: 24 | db -> a boolean, if true, only the database 25 | will be deleted 26 | config: - a configuration dictionary containing at least 27 | the following keys: 28 | PATH_KB - the main path of KB 29 | PATH_KB_DB - the database path of KB 30 | PATH_KB_HIST - the history menu path of KB 31 | """ 32 | if args["db"]: 33 | answer = input( 34 | "Are you sure you want to erase the kb database ? [YES/NO]") 35 | if answer.lower() == "yes": 36 | try: 37 | fs.remove_file(config["PATH_KB_DB"]) 38 | fs.remove_file(config["PATH_KB_HIST"]) 39 | print("kb database deleted successfully!") 40 | except FileNotFoundError: 41 | pass 42 | else: 43 | answer = input( 44 | "Are you sure you want to erase the whole kb knowledge base ? [YES/NO]") 45 | if answer.lower() == "yes": 46 | try: 47 | fs.remove_directory(config["PATH_KB"]) 48 | print("kb knowledge base deleted successfully!") 49 | except FileNotFoundError: 50 | pass 51 | -------------------------------------------------------------------------------- /kb/commands/export.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb export command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import time 15 | import tarfile 16 | from pathlib import Path 17 | from typing import Dict 18 | 19 | 20 | def export(args: Dict[str, str], config: Dict[str, str]): 21 | """ 22 | Export the entire kb knowledge base. 23 | 24 | Arguments: 25 | args: - a dictionary containing the following fields: 26 | file -> a string representing the wished output 27 | filename 28 | config: - a configuration dictionary containing at least 29 | the following keys: 30 | PATH_KB - the main path of KB 31 | """ 32 | fname = args["file"] or time.strftime("%d_%m_%Y-%H%M%S") 33 | archive_ext = ".kb.tar.gz" 34 | if not fname.endswith(archive_ext): 35 | fname = fname + archive_ext 36 | 37 | if args["only_data"]: 38 | with tarfile.open(fname, mode='w:gz') as archive: 39 | archive.add(config["PATH_KB_DATA"], arcname="kb", recursive=True) 40 | else: 41 | with tarfile.open(fname, mode='w:gz') as archive: 42 | archive.add(config["PATH_KB"], arcname=".kb", recursive=True) 43 | -------------------------------------------------------------------------------- /kb/commands/grep.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb grep command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import sys 15 | from pathlib import Path 16 | from typing import Dict 17 | import kb.db as db 18 | import kb.initializer as initializer 19 | import kb.printer.grep as printer 20 | import kb.history as history 21 | import kb.filesystem as fs 22 | 23 | 24 | def grep(args: Dict[str, str], config: Dict[str, str]): 25 | """ 26 | Grep through the list of artifacts of the knowledge base of kb. 27 | 28 | Arguments: 29 | args: - a dictionary containing the following fields: 30 | regex -> the regex to search for 31 | case_insensitive -> a boolean, if true, 32 | the search will be case insensitive 33 | matches -> a boolean, if true, only the raw 34 | matches will be shown 35 | verbose -> a boolean, if true, a verbose 36 | output is produced on screen 37 | config: - a configuration dictionary containing at least 38 | the following keys: 39 | PATH_KB_DB - the database path of KB 40 | PATH_KB_DATA - the data directory of KB 41 | PATH_KB_HIST - the history menu path of KB 42 | """ 43 | initializer.init(config) 44 | 45 | conn = db.create_connection(config["PATH_KB_DB"]) 46 | 47 | # Get all artifacts 48 | rows = db.get_artifacts_by_filter(conn, title="") 49 | 50 | # Get all the file paths related to the artifacts in the database 51 | file_list = [Path(config["PATH_KB_DATA"], r.category, r.title) 52 | for r in rows] 53 | 54 | # Grep in the files 55 | results = fs.grep_in_files( 56 | file_list, 57 | args["regex"], 58 | args["case_insensitive"]) 59 | 60 | # If user specified --matches -> just show matching lines and exit 61 | color_mode = not args["no_color"] 62 | if args["matches"]: 63 | printer.print_grep_matches(results, color_mode) 64 | sys.exit(0) 65 | 66 | # Get the list of artifact tuples in the form (category,title) 67 | artifact_names = [fs.get_filename_parts_wo_prefix( 68 | res[0], config["PATH_KB_DATA"]) for res in results] 69 | 70 | # Get the set of uniq artifacts 71 | uniq_artifact_names = set(artifact_names) 72 | 73 | # Get the number of matches (hits) for each path found 74 | filecounts = get_hits_per_artifact_name(artifact_names) 75 | 76 | grep_result = list() 77 | 78 | for art in uniq_artifact_names: 79 | 80 | artifact = db.get_artifacts_by_filter( 81 | conn, category="/".join(art[:-1]), title=art[-1], is_strict=True)[0] 82 | 83 | if artifact: 84 | no_of_hits = filecounts[art] 85 | grep_result.append((artifact, no_of_hits)) 86 | 87 | # Sort by number of hits, the largest -> the first 88 | grep_result.sort(key=lambda x: x[1], reverse=True) 89 | 90 | grep_artifacts = [r[0] for r in grep_result] 91 | grep_hits = [r[1] for r in grep_result] 92 | 93 | # Write to history file 94 | history.write(config["PATH_KB_HIST"], grep_artifacts) 95 | 96 | color_mode = not args["no_color"] 97 | if args["verbose"]: 98 | printer.print_grep_result_verbose( 99 | grep_artifacts, grep_hits, color_mode) 100 | else: 101 | printer.print_grep_result(grep_artifacts, grep_hits, color_mode) 102 | 103 | 104 | def get_hits_per_artifact_name(artifact_name_list): 105 | """ 106 | Get the dictionary related to the number of hits 107 | for each artifact name found. 108 | 109 | Arguments: 110 | artifact_name_list - a list of tuples, where each 111 | tuple is (category, title) 112 | Returns: 113 | A dictionary having as key the tuple (category,title) 114 | and as value the number of hits of that 115 | """ 116 | namecounts = dict() 117 | for artname in artifact_name_list: 118 | if artname in namecounts: 119 | namecounts[artname] += 1 120 | else: 121 | namecounts[artname] = 1 122 | return namecounts 123 | -------------------------------------------------------------------------------- /kb/commands/ingest.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb import command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import tarfile 15 | from pathlib import Path 16 | from typing import Dict 17 | import kb.filesystem as fs 18 | 19 | 20 | def ingest(args: Dict[str, str], config: Dict[str, str]): 21 | """ 22 | Import an entire kb knowledge base. 23 | 24 | Arguments: 25 | args: - a dictionary containing the following fields: 26 | file -> a string representing the path to the archive 27 | to be imported 28 | config: - a configuration dictionary containing at least 29 | the following keys: 30 | PATH_KB - the main path of KB 31 | """ 32 | if args["file"].endswith(".tar.gz"): 33 | answer = input("You are about to import a whole knowledge base " 34 | "are you sure you want to wipe your previous " 35 | " kb data ? [YES/NO]") 36 | if answer.lower() == "yes": 37 | print("Previous kb knowledge base data wiped...") 38 | try: 39 | fs.remove_directory(config["PATH_KB"]) 40 | except FileNotFoundError: 41 | pass 42 | tar = tarfile.open(args["file"], "r:gz") 43 | tar.extractall(Path.home()) 44 | tar.close() 45 | print("kb archive {fname} imported".format(fname=args["file"])) 46 | else: 47 | print("Please provide a file exported through kb with kb.tar.gz extension") 48 | -------------------------------------------------------------------------------- /kb/commands/search.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb search command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | from typing import Dict 15 | import kb.db as db 16 | import kb.initializer as initializer 17 | import kb.printer.search as printer 18 | import kb.history as history 19 | 20 | 21 | def search(args: Dict[str, str], config: Dict[str, str]): 22 | """ 23 | Search artifacts within the knowledge base of kb. 24 | 25 | Arguments: 26 | args: - a dictionary containing the following fields: 27 | query -> filter for the title field of the artifact 28 | category -> filter for the category field of the artifact 29 | tags -> filter for the tags field of the artifact 30 | author -> filter for the author field of the artifact 31 | status -> filter for the status field of the artifact 32 | config: - a configuration dictionary containing at least 33 | the following keys: 34 | PATH_KB_DB - the database path of KB 35 | PATH_KB_DATA - the data directory of KB 36 | PATH_KB_HIST - the history menu path of KB 37 | EDITOR - the editor program to call 38 | """ 39 | # Check initialization 40 | initializer.init(config) 41 | 42 | tags_list = None 43 | if args["tags"] and args["tags"] != "": 44 | tags_list = args["tags"].split(';') 45 | 46 | conn = db.create_connection(config["PATH_KB_DB"]) 47 | rows = db.get_artifacts_by_filter( 48 | conn, 49 | title=args["query"], 50 | category=args["category"], 51 | tags=tags_list, 52 | status=args["status"], 53 | author=args["author"]) 54 | 55 | # rows.sort(key=lambda x: x[1]) 56 | artifacts = sorted(rows, key=lambda x: x.title) 57 | 58 | # Write to history file 59 | history.write(config["PATH_KB_HIST"], artifacts) 60 | 61 | # Is full_identifier mode enabled? 62 | if args["full_identifier"]: 63 | printer.print_search_result_full_mode(artifacts) 64 | return 65 | 66 | # Print resulting list 67 | color_mode = not args["no_color"] 68 | if args["verbose"]: 69 | printer.print_search_result_verbose(artifacts, color_mode) 70 | else: 71 | printer.print_search_result(artifacts, color_mode) 72 | -------------------------------------------------------------------------------- /kb/commands/sync.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb export command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import time 15 | from typing import Dict 16 | import kb.filesystem as fs 17 | import git 18 | 19 | 20 | def sync(args: Dict[str, str], config: Dict[str, str]): 21 | """ 22 | Synchronize the knowledge base with a remote repository. 23 | 24 | Arguments: 25 | args: - a dictionary containing the following fields: 26 | file -> a string representing the wished output 27 | filename 28 | config: - a configuration dictionary containing at least 29 | the following keys: 30 | PATH_KB - the main path of KB 31 | """ 32 | 33 | operation = args["operation"] 34 | 35 | if operation == "init": 36 | if is_local_git_repo_initialized(config["PATH_KB_GIT"]): 37 | print("Warning: a git repository already exists...") 38 | cancel_repo = input( 39 | "Do you want to re-init your repository ? [Type YES in that case] ") 40 | if cancel_repo == "YES": 41 | fs.remove_directory(config["PATH_KB_GIT"]) 42 | git_init(config["PATH_KB"]) 43 | else: 44 | print("Maybe you wanted to type \"kb sync push\" or \"kb sync pull\"!") 45 | else: 46 | git_init(config["PATH_KB"]) 47 | elif operation == "push": 48 | if not is_local_git_repo_initialized(config["PATH_KB_GIT"]): 49 | print("Warning: You did not initialize any repository...") 50 | print("try: kb sync init") 51 | return 52 | git_push(config["PATH_KB"]) 53 | elif operation == "pull": 54 | if not is_local_git_repo_initialized(config["PATH_KB_GIT"]): 55 | print("Warning: no local git repository has been instantiated!") 56 | cancel_repo = input( 57 | "Do you want to remove possible local kb files and sync from a remote repository? [Type YES in that case] ") 58 | if cancel_repo == "YES": 59 | try: 60 | fs.remove_directory(config["PATH_KB"]) 61 | except BaseException: 62 | pass 63 | git_clone(config["PATH_KB"]) 64 | else: 65 | git_pull(config["PATH_KB_GIT"]) 66 | elif operation == "info": 67 | if not is_local_git_repo_initialized(config["PATH_KB_GIT"]): 68 | print("No local git repository has been instantiated!") 69 | return 70 | show_info(config["PATH_KB"]) 71 | else: 72 | print("Error: No valid operation has been specified") 73 | return 74 | 75 | 76 | def is_local_git_repo_initialized(git_path): 77 | if fs.is_directory(git_path): 78 | return True 79 | 80 | 81 | def git_push(repo_path): 82 | try: 83 | kb_repo = git.Repo(repo_path) 84 | kb_repo.git.add('--all') 85 | timestamp = time.strftime("%d/%m/%Y-%H:%M:%S") 86 | kb_repo.index.commit("kb synchronization {ts}".format(ts=timestamp)) 87 | kb_repo.remote( 88 | name='origin').push( 89 | refspec='{}:{}'.format( 90 | "main", 91 | "main")) 92 | print("Repository correctly synchronized to remote!") 93 | except BaseException: 94 | print('Some error occurred while pushing the code') 95 | print('Check your internet connection or the existence of the remote repository') 96 | 97 | 98 | def git_pull(repo_path): 99 | try: 100 | kb_repo = git.Repo(repo_path) 101 | origin = kb_repo.remotes.origin 102 | origin.pull(origin.refs[0].remote_head) 103 | print("Repository correctly synchronized from remote!") 104 | except BaseException: 105 | print('Some error occurred while pulling the code') 106 | print('Check your internet connection or the existence of the remote repository') 107 | 108 | 109 | def git_clone(repo_path): 110 | remote_repo_url = input( 111 | "Insert the URL of the remote repo (e.g., https://github/user/mykb): ") 112 | if remote_repo_url: 113 | git.Repo.clone_from(remote_repo_url, repo_path) 114 | print("Knowledge base correctly pulled from remote!") 115 | else: 116 | print("Error: Check your internet connection or provide a valid repository") 117 | 118 | 119 | def git_init(repo_path): 120 | print("Create a remote empty repository on github/gitlab or other git provider...") 121 | remote_repo_url = input( 122 | "Insert the URL of the created empty remote repo (e.g., https://github/user/mykb): ") 123 | if remote_repo_url: 124 | local_repo = git.Repo.init(repo_path) 125 | remote = local_repo.create_remote("origin", url=remote_repo_url) 126 | local_repo.git.add('--all') 127 | timestamp = time.strftime("%d/%m/%Y-%H:%M:%S") 128 | local_repo.index.commit("kb synchronization {ts}".format(ts=timestamp)) 129 | print("Initialization with remote may take time...") 130 | print("Please provide remote credentials and wait...") 131 | remote.push(refspec='{}:{}'.format("main", "main")) 132 | print("Remote repository correctly initialized!") 133 | else: 134 | print("Error: Provide a valid remote URL") 135 | 136 | 137 | def show_info(repo_path): 138 | repo = git.cmd.Git(repo_path) 139 | print("Remote repository:") 140 | print(repo.execute(["git", "remote", "-v"])) 141 | -------------------------------------------------------------------------------- /kb/commands/template.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb template command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import shlex 15 | import sys 16 | import toml 17 | from pathlib import Path 18 | from subprocess import call 19 | from typing import Dict, List 20 | import kb.db as db 21 | import kb.initializer as initializer 22 | import kb.filesystem as fs 23 | import kb.config as conf 24 | from kb.entities.artifact import Artifact 25 | import kb.printer.template as printer 26 | 27 | 28 | def get_templates(templates_path: str) -> List[str]: 29 | """ 30 | Get the list of available templates. 31 | 32 | Arguments: 33 | templates_path - the path where all templates are stored 34 | 35 | Returns: 36 | A list of strings representing the available templates 37 | """ 38 | return fs.list_files(templates_path) 39 | 40 | 41 | def search(args: Dict[str, str], config: Dict[str, str]): 42 | """ 43 | Search templates installed in kb. 44 | 45 | Arguments: 46 | args: - a dictionary containing the following fields: 47 | query -> filter for the title field of the artifact 48 | config: - a configuration dictionary containing at least 49 | the following keys: 50 | PATH_KB_TEMPLATES - the path to where the templates of KB 51 | are stored 52 | """ 53 | # template_list = fs.list_files(config["PATH_KB_TEMPLATES"]) 54 | template_list = get_templates(config["PATH_KB_TEMPLATES"]) 55 | if not template_list: 56 | return 57 | 58 | if args["query"]: 59 | template_list = [x for x in template_list if args["query"] in x] 60 | color_mode = not args["no_color"] 61 | printer.print_template_search_result(template_list, color_mode) 62 | 63 | 64 | def apply_on_set(args: Dict[str, str], config: Dict[str, str]): 65 | """ 66 | Apply the specified template to all the filtered artifacts 67 | """ 68 | # Check initialization 69 | initializer.init(config) 70 | 71 | tags_list = None 72 | if args["tags"] and args["tags"] != "": 73 | tags_list = args["tags"].split(';') 74 | 75 | conn = db.create_connection(config["PATH_KB_DB"]) 76 | is_query_strict = not args["extended_match"] 77 | rows = db.get_artifacts_by_filter( 78 | conn, 79 | title=args["title"], 80 | category=args["category"], 81 | tags=tags_list, 82 | status=args["status"], 83 | author=args["author"], 84 | is_strict=is_query_strict) 85 | 86 | for artifact in rows: 87 | updated_artifact = Artifact( 88 | id=artifact.id, 89 | title=artifact.title, 90 | category=artifact.category, 91 | tags=artifact.tags, 92 | author=artifact.author, 93 | status=artifact.status, 94 | template=args["template"]) 95 | db.update_artifact_by_id(conn, artifact.id, updated_artifact) 96 | 97 | 98 | def new(args: Dict[str, str], config: Dict[str, str]): 99 | """ 100 | Create a new template from scratch starting from the default template. 101 | 102 | Arguments: 103 | args: - a dictionary containing the following fields: 104 | template -> the name of the new template to create 105 | config: - a configuration dictionary containing at least 106 | the following keys: 107 | PATH_KB_TEMPLATES - the path to where the templates of KB 108 | are stored 109 | PATH_KB_DEFAULT_TEMPLATE - the path to where the default template of KB 110 | is stored 111 | EDITOR - the editor program to call 112 | """ 113 | template_path = str(Path(config["PATH_KB_TEMPLATES"]) / args["template"]) 114 | 115 | if fs.is_file(template_path): 116 | print( 117 | "ERROR: The template you inserted corresponds to an existing one. " 118 | "Please specify another name for the new template") 119 | sys.exit(1) 120 | 121 | fs.create_directory(Path(template_path).parent) 122 | # fs.copy_file(config["PATH_KB_DEFAULT_TEMPLATE"], template_path) 123 | 124 | with open(template_path, 'w') as tmplt: 125 | tmplt.write("# This is an example configuration template\n\n\n") 126 | tmplt.write(toml.dumps(conf.DEFAULT_TEMPLATE)) 127 | 128 | shell_cmd = shlex.split( 129 | config["EDITOR"]) + [template_path] 130 | call(shell_cmd) 131 | 132 | 133 | def add(args: Dict[str, str], config: Dict[str, str]): 134 | """ 135 | Add a new template to the templates available in kb. 136 | 137 | Arguments: 138 | args: - a dictionary containing the following fields: 139 | file -> the path to the template to include in kb templates 140 | title -> the title to assign to the kb template added 141 | config: - a configuration dictionary containing at least 142 | the following keys: 143 | PATH_KB_TEMPLATES - the path to where the templates of KB 144 | are stored 145 | """ 146 | template_path = args["file"] 147 | if args["title"]: 148 | dest_path = str(Path(config["PATH_KB_TEMPLATES"]) / args["title"]) 149 | else: 150 | dest_path = config["PATH_KB_TEMPLATES"] 151 | fs.copy_file(template_path, dest_path) 152 | 153 | 154 | def delete(args: Dict[str, str], config: Dict[str, str]): 155 | """ 156 | Delete a template from the kb templates. 157 | 158 | Arguments: 159 | args: - a dictionary containing the following fields: 160 | template -> the name of the template to remove 161 | config: - a configuration dictionary containing at least 162 | the following keys: 163 | PATH_KB_TEMPLATES - the path to where the templates of KB 164 | are stored 165 | """ 166 | template_name = args["template"] 167 | fs.remove_file(Path(config["PATH_KB_TEMPLATES"], template_name)) 168 | 169 | 170 | def edit(args: Dict[str, str], config: Dict[str, str]): 171 | """ 172 | Edit a template from the kb templates. 173 | 174 | Arguments: 175 | args: - a dictionary containing the following fields: 176 | template -> the name of the template to edit 177 | config: - a configuration dictionary containing at least 178 | the following keys: 179 | PATH_KB_TEMPLATES - the path to where the templates of KB 180 | are stored 181 | EDITOR - the editor program to call 182 | """ 183 | template_path = str(Path(config["PATH_KB_TEMPLATES"]) / args["template"]) 184 | 185 | if not fs.is_file(template_path): 186 | print("ERROR: The template you want to edit does not exist. " 187 | "Please specify a valid template to edit or create a new one") 188 | sys.exit(1) 189 | 190 | shell_cmd = shlex.split( 191 | config["EDITOR"]) + [template_path] 192 | call(shell_cmd) 193 | 194 | 195 | COMMANDS = { 196 | 'add': add, 197 | 'delete': delete, 198 | 'edit': edit, 199 | 'list': search, 200 | 'new': new, 201 | 'apply': apply_on_set, 202 | } 203 | 204 | 205 | def template(args: Dict[str, str], config: Dict[str, str]): 206 | """ 207 | Manage templates for kb. 208 | 209 | Arguments: 210 | args: - a dictionary containing the following fields: 211 | template_command -> the sub-command to execute for templates 212 | that can be: "add", "delete", "edit", 213 | "list" or "new". 214 | file -> used if the command is add, representing the template 215 | file to add to kb 216 | template -> used if the command is "delete", "edit" or "new" 217 | to represent the name of the template 218 | query -> used if the command is "list" 219 | config: - a configuration dictionary containing at least 220 | the following keys: 221 | PATH_KB_DEFAULT_TEMPLATE - the path to the kb default template 222 | PATH_KB_TEMPLATES - the path to kb templates 223 | EDITOR - the editor program to call 224 | """ 225 | 226 | # Check initialization 227 | initializer.init(config) 228 | 229 | COMMANDS[args["template_command"]](args, config) 230 | -------------------------------------------------------------------------------- /kb/commands/update.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb edit command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import shlex 15 | from subprocess import call 16 | from typing import Dict 17 | from pathlib import Path 18 | import kb.db as db 19 | import kb.initializer as initializer 20 | import kb.history as history 21 | import kb.filesystem as fs 22 | from kb.entities.artifact import Artifact 23 | 24 | 25 | def update(args: Dict[str, str], config: Dict[str, str]): 26 | """ 27 | Update artifact properties within the knowledge base of kb. 28 | 29 | Arguments: 30 | args: - a dictionary containing the following fields: 31 | id -> a list of IDs (the ones you see with kb list) 32 | associated to the artifact to update 33 | title -> the title to be assigned to the artifact 34 | to update 35 | category -> the category to be assigned to the 36 | artifact to update 37 | tags -> the tags to be assigned to the artifact 38 | to update 39 | author -> the author to be assigned to the artifact 40 | to update 41 | status -> the status to be assigned to the artifact 42 | to update 43 | template -> the template to be assigned to the artifact 44 | to update 45 | edit_content -> a boolean, if True -> also open the 46 | artifact to edit the content 47 | config: - a configuration dictionary containing at least 48 | the following keys: 49 | PATH_KB_DB - the database path of KB 50 | PATH_KB_DATA - the data directory of KB 51 | PATH_KB_HIST - the history menu path of KB 52 | EDITOR - the editor program to call 53 | """ 54 | initializer.init(config) 55 | 56 | conn = db.create_connection(config["PATH_KB_DB"]) 57 | 58 | # if an ID is specified, load artifact with that ID 59 | if args["id"]: 60 | old_artifact = history.get_artifact(conn, 61 | config["PATH_KB_HIST"], args["id"]) 62 | if not old_artifact: 63 | print("The artifact you are trying to update does not exist! " 64 | "Please insert a valid ID...") 65 | return None 66 | 67 | updated_artifact = Artifact( 68 | id=None, 69 | title=args["title"], 70 | category=args["category"], 71 | tags=args["tags"], 72 | author=args["author"], 73 | status=args["status"], 74 | template=args["template"]) 75 | 76 | db.update_artifact_by_id(conn, old_artifact.id, updated_artifact) 77 | # If either title or category has been changed, we must move the file 78 | if args["category"] or args["title"]: 79 | old_category_path = Path( 80 | config["PATH_KB_DATA"], 81 | old_artifact.category) 82 | new_category_path = Path( 83 | config["PATH_KB_DATA"], 84 | args["category"] or old_artifact.category) 85 | fs.create_directory(new_category_path) 86 | 87 | fs.move_file(Path(old_category_path, old_artifact.title), Path( 88 | new_category_path, args["title"] or old_artifact.title)) 89 | # else if a title is specified 90 | elif args["title"]: 91 | artifact = db.get_uniq_artifact_by_filter(conn, title=args["title"], 92 | category=args["category"], 93 | author=args["author"], 94 | status=args["status"], 95 | is_strict=True) 96 | 97 | if artifact: 98 | category_path = Path(config["PATH_KB_DATA"], artifact.category) 99 | else: 100 | print( 101 | "There is none or more than one artifact with that title, please specify a category") 102 | 103 | if args["edit_content"] or args["body"]: 104 | if args["title"]: 105 | artifact_path = str(Path(category_path, artifact.title)) 106 | shell_cmd = shlex.split(config["EDITOR"]) + [artifact_path] 107 | elif args["id"]: 108 | artifact_path = str(Path(config["PATH_KB_DATA"]) 109 | / old_artifact.category 110 | / old_artifact.title) 111 | shell_cmd = shlex.split(config["EDITOR"]) + [artifact_path] 112 | 113 | if args["body"]: 114 | args["body"] = args["body"].replace("\\n", "\n") 115 | with open(artifact_path, 'w') as art_file: 116 | art_file.write(args["body"]) 117 | else: 118 | call(shell_cmd) 119 | -------------------------------------------------------------------------------- /kb/commands/view.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb view command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import os 15 | import platform 16 | import sys 17 | import shlex 18 | import tempfile 19 | from subprocess import call 20 | from pathlib import Path 21 | from typing import Dict 22 | import kb.db as db 23 | import kb.filesystem as fs 24 | import kb.history as history 25 | import kb.initializer as initializer 26 | import kb.opener as opener 27 | import kb.viewer as viewer 28 | from kb.config import get_markers 29 | from kb.entities.artifact import Artifact 30 | 31 | 32 | def view(args: Dict[str, str], config: Dict[str, str]): 33 | """ 34 | View an artifact contained in the knowledge base of kb. 35 | 36 | Arguments: 37 | args: - a dictionary containing the following fields: 38 | id -> the IDs (the one you see with kb list) 39 | associated to the artifact to view 40 | title -> the title of the artifact to view 41 | category -> the category of the artifact to view 42 | editor -> a boolean, if True the file will 43 | be opened in a text editor as a temporary file 44 | hence the original will not be affected 45 | config: - a configuration dictionary containing at least 46 | the following keys: 47 | PATH_KB_DB - the database path of KB 48 | PATH_KB_DATA - the data directory of KB 49 | PATH_KB_HIST - the history menu path of KB 50 | PATH_KB_DEFAULT_TEMPLATE - the file associated to the markers 51 | EDITOR - the editor program to call 52 | """ 53 | # Check initialization 54 | initializer.init(config) 55 | 56 | color_mode = not args["no_color"] 57 | if args["id"]: 58 | view_by_id(args["id"], config, args["editor"], color_mode) 59 | elif args["title"]: 60 | view_by_name( 61 | args["title"], 62 | args["category"], 63 | config, 64 | args["editor"], 65 | color_mode) 66 | elif args["nameid"]: 67 | if args["nameid"].isdigit(): 68 | view_by_id(args["nameid"], config, args["editor"], color_mode) 69 | else: 70 | view_by_name( 71 | args["nameid"], 72 | args["category"], 73 | config, 74 | args["editor"], 75 | color_mode) 76 | 77 | 78 | def view_by_id(id: int, 79 | config: Dict[str, 80 | str], 81 | open_editor: bool, 82 | color_mode: bool): 83 | """ 84 | View the content of an artifact by id. 85 | 86 | Arguments: 87 | id: - the ID (the one you see with kb list) 88 | associated to the artifact we want to edit 89 | config: - a configuration dictionary containing at least 90 | the following keys: 91 | PATH_KB_DB - the database path of KB 92 | PATH_KB_DATA - the data directory of KB 93 | PATH_KB_HIST - the history menu path of KB 94 | EDITOR - the editor program to call 95 | open_editor - a boolean, if True it will open the artifact as 96 | a temporary copy in editor 97 | color_mode - a boolean, if True the colors on screen will be 98 | enabled when printed on stdout 99 | """ 100 | conn = db.create_connection(config["PATH_KB_DB"]) 101 | artifact_id = history.get_artifact_id( 102 | config["PATH_KB_HIST"], id) 103 | 104 | artifact = db.get_artifact_by_id(conn, artifact_id) 105 | 106 | if not artifact: 107 | sys.exit(1) 108 | 109 | category_path = Path(config["PATH_KB_DATA"], artifact.category) 110 | artifact_path = Path(category_path, artifact.title) 111 | 112 | if open_editor: 113 | tmpfname = fs.get_temp_filepath() 114 | fs.copy_file(artifact_path, tmpfname) 115 | 116 | shell_cmd = shlex.split(config["EDITOR"]) + [tmpfname] 117 | call(shell_cmd) 118 | fs.remove_file(tmpfname) 119 | 120 | sys.exit(0) 121 | 122 | # View File 123 | if fs.is_text_file(artifact_path): 124 | markers = get_template(artifact, config) 125 | viewer.view(artifact_path, markers, color=color_mode) 126 | else: 127 | opener.open_non_text_file(artifact_path) 128 | 129 | 130 | def view_by_name(title: str, 131 | category: str, 132 | config: Dict[str, 133 | str], 134 | open_editor: bool, 135 | color_mode: bool): 136 | """ 137 | View the content of an artifact by name, that is title/category 138 | 139 | Arguments: 140 | title: - the title assigned to the artifact(s) 141 | category: - the category assigned to the artifact(s) 142 | config: - a configuration dictionary containing at least 143 | the following keys: 144 | PATH_KB_DB - the database path of KB 145 | PATH_KB_DATA - the data directory of KB 146 | PATH_KB_HIST - the history menu path of KB 147 | EDITOR - the editor program to call 148 | open_editor - a boolean, if True it will open the artifact as 149 | a temporary copy in editor 150 | color_mode - a boolean, if True the colors on screen will be 151 | enabled when printed on stdout 152 | """ 153 | conn = db.create_connection(config["PATH_KB_DB"]) 154 | artifacts = db.get_artifacts_by_filter(conn, title=title, 155 | category=category, 156 | is_strict=True) 157 | if len(artifacts) == 1: 158 | artifact = artifacts.pop() 159 | category_path = Path(config["PATH_KB_DATA"], artifact.category) 160 | artifact_path = Path(category_path, artifact.title) 161 | 162 | if open_editor: 163 | tmpfname = fs.get_temp_filepath() 164 | fs.copy_file(artifact_path, tmpfname) 165 | 166 | shell_cmd = shlex.split(config["EDITOR"]) + [tmpfname] 167 | call(shell_cmd) 168 | fs.remove_file(tmpfname) 169 | sys.exit(0) 170 | 171 | # View File 172 | if fs.is_text_file(artifact_path): 173 | markers = get_template(artifact, config) 174 | viewer.view(artifact_path, markers, color=color_mode) 175 | else: 176 | opener.open_non_text_file(artifact_path) 177 | elif len(artifacts) > 1: 178 | print( 179 | "There is more than one artifact with that title, please specify a category") 180 | else: 181 | print( 182 | "There is no artifact with that name, please specify a correct artifact name") 183 | 184 | 185 | def get_template(artifact: Artifact, config: Dict[str, str]) -> str: 186 | """" 187 | Get template for a specific artifact. 188 | 189 | Arguments: 190 | artifact - the artifact to get the template for 191 | config: - a configuration dictionary containing at least 192 | the following keys: 193 | PATH_KB_DEFAULT_TEMPLATE - the file associated to the markers 194 | PATH_KB_TEMPLATES - the path where templates are stored 195 | 196 | Returns: 197 | A dictionary containing markers, where the key is a regex 198 | and the value is a string representing a color. 199 | """ 200 | template = artifact.template or "default" 201 | if template == "default": 202 | markers = get_markers(config["PATH_KB_DEFAULT_TEMPLATE"]) 203 | else: 204 | markers = get_markers( 205 | str(Path(*[config["PATH_KB_TEMPLATES"]] + template.split('/')))) 206 | return markers 207 | -------------------------------------------------------------------------------- /kb/config.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb config module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | __all__ = () 15 | 16 | import os 17 | from sys import platform 18 | from pathlib import Path 19 | import toml 20 | 21 | BASE_PATH = Path(os.environ.get("XDG_DATA_HOME",Path(Path.home(),".local","share")),"kb") 22 | 23 | 24 | DEFAULT_CONFIG = { 25 | "PATH_KB": str(Path(BASE_PATH)), 26 | "PATH_KB_DB": str(Path(BASE_PATH, "kb.db")), 27 | "PATH_KB_HIST": str(Path(BASE_PATH, "recent.hist")), 28 | "PATH_KB_DATA": str(Path(BASE_PATH, "data")), 29 | "PATH_KB_GIT": str(Path(BASE_PATH, ".git")), 30 | # for future use 31 | "PATH_KB_CONFIG": str(Path(BASE_PATH, "kb.conf.py")), 32 | "PATH_KB_TEMPLATES": str(Path(BASE_PATH, "templates")), 33 | "PATH_KB_DEFAULT_TEMPLATE": str(Path(BASE_PATH, "templates", "default")), 34 | "DB_SCHEMA_VERSION": 1, 35 | "EDITOR": os.environ.get("EDITOR", "vim"), 36 | "INITIAL_CATEGORIES": ["default", ] 37 | } 38 | 39 | 40 | DEFAULT_TEMPLATE = { 41 | "TITLES": ("^#.*", "blue"), 42 | "WARNINGS": ("^!.*", "yellow"), 43 | } 44 | 45 | 46 | def get_markers(markers_path: str): 47 | """ 48 | Load markers file 49 | 50 | Arguments: 51 | markers_path - the path to the toml markers file 52 | 53 | Returns a dictionary containing markers 54 | """ 55 | try: 56 | return toml.load(markers_path) 57 | except toml.TomlDecodeError: 58 | print("Error: The provided file is not in the toml format") 59 | except FileNotFoundError: 60 | print("Error: The provided file does not exist or cannot be accessed") 61 | -------------------------------------------------------------------------------- /kb/entities/artifact.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb artifact frozen dataclass 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import attr 15 | from typing import List, Set, Optional 16 | 17 | 18 | @attr.s(auto_attribs=True, frozen=True, slots=True) 19 | class Artifact: 20 | id: Optional[int] 21 | title: str 22 | category: str 23 | path: Optional[str] = None 24 | tags: Optional[str] = None 25 | status: Optional[str] = None 26 | author: Optional[str] = None 27 | template: Optional[str] = None 28 | -------------------------------------------------------------------------------- /kb/filesystem.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb filesystem module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | __all__ = () 15 | 16 | import os 17 | import re 18 | import shutil 19 | import tempfile 20 | from pathlib import Path 21 | from typing import List 22 | 23 | 24 | def list_files(directory: str) -> List[str]: 25 | """ 26 | List all files contained in a directory recursively, 27 | similarly to the "find" UNIX command 28 | 29 | Args: 30 | directory - a string representing the target directory 31 | 32 | Returns: 33 | A list of strings representing the path of files contained 34 | in the directory 35 | """ 36 | 37 | # Get kbdir path 38 | dirpath = Path(directory) 39 | 40 | # Get list of files in the form: file1, dir1/file2, ... 41 | files = [str(f.relative_to(dirpath)) 42 | for f in dirpath.rglob("*") if f.is_file()] 43 | return files 44 | 45 | 46 | def list_dirs(directory: str) -> List[str]: 47 | """ 48 | List all sub-directories contained in a directory 49 | 50 | Args: 51 | directory - a string representing the path to a directory 52 | 53 | Returns: 54 | A list of strings representing the path of directories contained 55 | in the provided directory 56 | """ 57 | # Get kbdir path 58 | dirpath = Path(directory) 59 | 60 | # Get list of files in the form: file1, dir1/file2, ... 61 | files = [str(f.relative_to(dirpath)) 62 | for f in dirpath.rglob("*") if f.is_dir()] 63 | return files 64 | 65 | 66 | def touch_file(filename: str): 67 | """ 68 | Creates a new empty file, in the style of the UNIX 69 | touch program. 70 | 71 | Arguments: 72 | 73 | filename - a path to a filename 74 | """ 75 | Path(filename).touch() 76 | 77 | 78 | def get_basename(filename: str) -> str: 79 | """ 80 | Get basename for a file 81 | 82 | Arguments: 83 | filename - a path to a filename 84 | 85 | Returns: 86 | The basename of the provided file 87 | """ 88 | return Path(filename).name 89 | 90 | 91 | def copy_file(source: str, dest: str) -> None: 92 | """ 93 | Copies a file to the provided destination 94 | 95 | Arguments: 96 | source - the path to the source file to copy 97 | dest - the destination path of the copy 98 | """ 99 | shutil.copy2(Path(source), Path(dest)) 100 | 101 | 102 | def remove_file(filename: str) -> None: 103 | """ 104 | Removes a file from the filesystem 105 | 106 | Arguments: 107 | filename - the file to remove from the kb directory 108 | """ 109 | try: 110 | Path(filename).unlink() 111 | except FileNotFoundError: 112 | pass 113 | 114 | 115 | def remove_directory(directory: str) -> None: 116 | """ 117 | Removes a directory from the filesystem 118 | 119 | Arguments: 120 | directory - the directory to remove from the kb system 121 | """ 122 | shutil.rmtree(directory) 123 | 124 | 125 | def create_directory(directory: str) -> None: 126 | """ 127 | Create a directory if it does not exist. 128 | 129 | Arguments: 130 | directory - the directory path to be created 131 | """ 132 | os.makedirs(Path(directory), exist_ok=True) 133 | 134 | 135 | def is_directory(path: str) -> bool: 136 | """ 137 | Checks if the provided path is a directory. 138 | 139 | Arguments: 140 | path - the path to check 141 | 142 | Returns: 143 | A boolean, if true, the path corresponds to a directory 144 | """ 145 | return os.path.isdir(path) 146 | 147 | 148 | def is_file(path: str) -> bool: 149 | """ 150 | Checks if the provided path corresponds to a regular file. 151 | 152 | Arguments: 153 | path - the path to check 154 | 155 | Returns: 156 | A boolean, if true, the path corresponds to a regular file 157 | """ 158 | return os.path.isfile(path) 159 | 160 | 161 | def count_files(directory: str) -> int: 162 | """ 163 | Count the number of files in a directory 164 | 165 | Arguments: 166 | directory - the directory where to count files 167 | 168 | Returns: 169 | the number of files contained in the directory 170 | """ 171 | return len(list(Path(directory).iterdir())) 172 | 173 | 174 | def move_file(source: str, dest: str) -> None: 175 | """ 176 | Moves a file to the provided destination 177 | 178 | Arguments: 179 | source - the path to the source file to copy 180 | dest - the destination path of the copy 181 | """ 182 | shutil.move(source, dest) 183 | 184 | 185 | def get_temp_filepath() -> str: 186 | """ 187 | Generates a temporary file path. 188 | 189 | Returns: 190 | A boolean, True if the file is of type text. 191 | """ 192 | tmpfilename = None 193 | while tmpfilename is None: 194 | random_tmp_path = str(Path(tempfile.gettempdir(), 195 | os.urandom(24).hex())) 196 | if not os.path.isfile(random_tmp_path): 197 | tmpfilename = random_tmp_path 198 | return tmpfilename 199 | 200 | 201 | def is_text_file(filename: str) -> bool: 202 | """ 203 | Determines if a file is textual (that can be 204 | nicely viewed in a text editor) or belonging 205 | to other types. 206 | 207 | Arguments: 208 | filename - the file name/path to check 209 | 210 | Returns: 211 | A boolean, True if the file is of type text. 212 | """ 213 | txt_extensions = ("", ".conf", ".ini", ".txt", 214 | ".md", ".rst", ".ascii", ".org", ".tex") 215 | 216 | file_ext = os.path.splitext(filename)[1] 217 | 218 | return file_ext in txt_extensions 219 | 220 | 221 | def get_filename_parts_wo_prefix( 222 | filename: str, 223 | prefix_to_remove: str) -> List[str]: 224 | """ 225 | Get filename parts without the provided prefix. 226 | E.g., if the filename is "/path/to/data/dir1/file2.txt" 227 | and the prefix to remove is "/path/to/data" then the 228 | returned will be a tuple containing ("dir1","file2.txt") 229 | 230 | Arguments: 231 | filename - a string or path provided by pathlib 232 | prefix_to_remove - a string or path provided by pathlib that 233 | will be removed from the filename 234 | 235 | Returns: 236 | The provided filename without the provided prefix 237 | """ 238 | prefix_path = Path(prefix_to_remove) 239 | file_path = Path(filename) 240 | 241 | try: 242 | return file_path.relative_to(prefix_path).parts 243 | except ValueError: 244 | file_path.parts 245 | 246 | 247 | def grep_in_files( 248 | filelist: str, 249 | regex: str, 250 | case_insensitive: bool = False 251 | ) -> List[str]: 252 | """ 253 | Grep recursively through a file list by trying to match 254 | a regex with the content of all the files. 255 | This is equivalent to: 256 | grep -nr 'regex' file1 file2 ... 257 | 258 | Arguments: 259 | filelist - the file list where to match the regex 260 | regex - the regex to match 261 | 262 | Returns 263 | A list of tuples containing (filepath, line number, matched string) 264 | """ 265 | if case_insensitive: 266 | pattern = re.compile(regex, re.IGNORECASE) 267 | else: 268 | pattern = re.compile(regex) 269 | 270 | matches = list() 271 | for fname in filelist: 272 | try: 273 | with open(fname) as handle: 274 | for i, line in enumerate(handle): 275 | match = pattern.search(line) 276 | linenumber = i + 1 277 | if match: 278 | matches.append((fname, linenumber, line.strip())) 279 | # !!!TODO: This can be of inspiration for later result show 280 | # print("%s:%s: %s" % (filepath, lineno, 281 | # line.strip().replace(mo.group(), "\033[92m%s\033[0m"% 282 | # mo.group()))) 283 | except UnicodeDecodeError: 284 | # In this case the file is binary, 285 | # so we don't search through binary files 286 | continue 287 | return matches 288 | 289 | 290 | def grep_in_files_uniq( 291 | filelist: str, 292 | regex: str, 293 | case_insensitive=False 294 | ) -> List[str]: 295 | """ 296 | Grep recursively through a list of files by trying to match 297 | a regex with the content of all the found files. 298 | Note that it will return only the set of filenames matched without 299 | the content. 300 | This is equivalent to: 301 | grep -lnr 'regex' file1 file2 ... 302 | 303 | Arguments: 304 | filelist - the file list where to match the regex 305 | regex - the regex to match 306 | 307 | Returns 308 | A list of file paths matching the regex. 309 | """ 310 | if case_insensitive: 311 | pattern = re.compile(regex, re.IGNORECASE) 312 | else: 313 | pattern = re.compile(regex) 314 | 315 | matches = list() 316 | for fname in filelist: 317 | try: 318 | with open(fname) as handle: 319 | for line in handle: 320 | match = pattern.search(line) 321 | if match: 322 | matches.append(fname) 323 | except UnicodeDecodeError: 324 | # In this case the file is binary, 325 | # so we don't search through binary files 326 | continue 327 | return list(set(matches)) 328 | -------------------------------------------------------------------------------- /kb/history.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb history module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | from typing import List 15 | from kb.entities.artifact import Artifact 16 | import kb.db as db 17 | 18 | 19 | def get_artifact_id(hist_file_path: str, list_id: int) -> int: 20 | """ 21 | Get the database ID related to an artifact based 22 | on the list ID (ID shown by kb list). 23 | This function is based on the history file path 24 | that is used to retrieve the correspondence between 25 | list ID and database artifact ID 26 | 27 | Arguments: 28 | hist_file_path - the path to the history file, 29 | this is generally in 30 | $HOME/.kb/recent.hist 31 | list_id - the ID shown by kb list 32 | 33 | Returns: 34 | The database ID corresponding to the artifact or 35 | None in case of non-valid list ID 36 | """ 37 | with open(hist_file_path, 'r') as hfile: 38 | for line in hfile: 39 | items = line.split(",") 40 | if items[0] == list_id: 41 | return items[1] 42 | return None 43 | 44 | 45 | def write(hist_file_path: str, search_result: List) -> None: 46 | """ 47 | Write the kb history file with the results of 48 | a search/list operation. 49 | 50 | Arguments: 51 | hist_file_path - the path to the history file, 52 | this is generally in 53 | $HOME/.kb/recent.hist 54 | search_result - the results returned from 55 | a DB query 56 | """ 57 | with open(hist_file_path, "w") as hfile: 58 | hfile.write('view_id,db_id\n') 59 | for view_id, result in enumerate(search_result): 60 | hfile.write("{},{}\n".format(view_id, result.id)) 61 | 62 | 63 | def get_artifact(conn, hist_file_path: str, list_id: int) -> Artifact: 64 | """ 65 | Get an artifact based on the list ID (ID shown by kb list). 66 | This function is based on the history file path 67 | that is used to retrieve the correspondence between 68 | list ID and database artifact ID 69 | 70 | Arguments: 71 | hist_file_path - the path to the history file, 72 | this is by default in 73 | $HOME/.kb/recent.hist 74 | list_id - the ID shown by kb list 75 | 76 | Returns: 77 | The artifact corresponding to list_id shown by kb list 78 | None in case of non-valid list ID 79 | """ 80 | artifact_id = get_artifact_id( 81 | hist_file_path, list_id) 82 | 83 | return db.get_artifact_by_id(conn, artifact_id) 84 | -------------------------------------------------------------------------------- /kb/initializer.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb initializer module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import os 15 | from pathlib import Path 16 | import toml 17 | import kb.db as db 18 | import kb.filesystem as fs 19 | import kb.config as conf 20 | 21 | 22 | def init(config): 23 | """ 24 | Initialize kb with the provided configuration. 25 | 26 | Arguments: 27 | config - a dictionary containing the following keys: 28 | PATH_KB - the path to kb 29 | (~/.kb by default) 30 | PATH_KB_DB - the path to kb database 31 | (~/.kb/kb.db by default) 32 | PATH_KB_DATA - the path to kb data 33 | (~/.kb/data/ by default) 34 | PATH_KB_DEFAULT_TEMPLATE - the path to kb markers 35 | (~/.kb/templates/default by default) 36 | """ 37 | if not is_initialized(config): 38 | create_kb_files(config) 39 | 40 | 41 | def create_kb_files(config): 42 | """ 43 | Create the kb files and infrastructure 44 | 45 | Arguments: 46 | config - a dictionary containing the following keys: 47 | PATH_KB - the path to kb 48 | (~/.kb by default) 49 | PATH_KB_DB - the path to kb database 50 | (~/.kb/kb.db by default) 51 | PATH_KB_DATA - the path to kb data 52 | (~/.kb/data/ by default) 53 | INITIAL_CATEGORIES - a list containing the initial 54 | categories contained within kb 55 | PATH_KB_TEMPLATES - the path to kb templates 56 | (~/.kb/templates/ by default) 57 | DB_SCHEMA_VERSION - the database schema version 58 | """ 59 | # Get paths for kb from configuration 60 | kb_path = config["PATH_KB"] 61 | db_path = config["PATH_KB_DB"] 62 | data_path = config["PATH_KB_DATA"] 63 | initial_categs = config["INITIAL_CATEGORIES"] 64 | templates_path = config["PATH_KB_TEMPLATES"] 65 | schema_version = config["DB_SCHEMA_VERSION"] 66 | default_template_path = str(Path(templates_path) / "default") 67 | 68 | # Create main kb 69 | fs.create_directory(kb_path) 70 | 71 | # Create kb database 72 | if not os.path.exists(db_path): 73 | db.create_kb_database(db_path, schema_version) 74 | 75 | # Check schema version 76 | conn = db.create_connection(db_path) 77 | current_schema_version = db.get_schema_version(conn) 78 | 79 | if current_schema_version == 0: 80 | db.migrate_v0_to_v1(conn) 81 | 82 | # Create "data" directory 83 | fs.create_directory(data_path) 84 | 85 | # Create "templates" directory 86 | fs.create_directory(templates_path) 87 | 88 | # Create kb initial categories directories 89 | for category in initial_categs: 90 | category_path = Path(data_path, category) 91 | fs.create_directory(category_path) 92 | 93 | # Create markers file 94 | with open(default_template_path, 'w') as cfg: 95 | cfg.write(toml.dumps(conf.DEFAULT_TEMPLATE)) 96 | 97 | 98 | def is_initialized(config) -> bool: 99 | """ 100 | Check if kb is correctly initialized, 101 | ensure that: 102 | 1 - the .kb directory exists 103 | 2 - the kb database exists 104 | 3 - the kb data directory exists 105 | 106 | Arguments: 107 | config - a dictionary containing the following keys: 108 | PATH_KB - the path to kb 109 | (~/.kb by default) 110 | PATH_KB_DB - the path to kb 111 | (~/.kb/kb.db by default) 112 | PATH_KB_DATA - the path to kb 113 | (~/.kb/data/ by default) 114 | Returns: 115 | True is kb is correctly initialized, False otherwise 116 | """ 117 | kb_path = config["PATH_KB"] 118 | db_path = config["PATH_KB_DB"] 119 | data_path = config["PATH_KB_DATA"] 120 | templates_path = config["PATH_KB_TEMPLATES"] 121 | 122 | for path in [kb_path, db_path, data_path, templates_path]: 123 | if not os.path.exists(path): 124 | return False 125 | 126 | conn = db.create_connection(db_path) 127 | return db.is_schema_updated_to_version(conn, config["DB_SCHEMA_VERSION"]) 128 | -------------------------------------------------------------------------------- /kb/main.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb main module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | __all__ = () 15 | 16 | import sys 17 | from kb.cl_parser import parse_args 18 | 19 | from kb.commands.add import add 20 | from kb.commands.search import search 21 | from kb.commands.edit import edit 22 | from kb.commands.update import update 23 | from kb.commands.delete import delete 24 | from kb.commands.template import template 25 | from kb.commands.view import view 26 | from kb.commands.grep import grep 27 | from kb.commands.erase import erase 28 | from kb.commands.ingest import ingest 29 | from kb.commands.export import export 30 | from kb.commands.sync import sync 31 | 32 | from kb.config import DEFAULT_CONFIG 33 | 34 | 35 | COMMANDS = { 36 | 'add': add, 37 | 'delete': delete, 38 | 'edit': edit, 39 | 'update': update, 40 | 'list': search, 41 | 'view': view, 42 | 'grep': grep, 43 | 'erase': erase, 44 | 'import': ingest, 45 | 'export': export, 46 | 'template': template, 47 | 'sync': sync, 48 | } 49 | 50 | 51 | def dispatch(function, *args, **kwargs): 52 | """ 53 | Dispatch command line action to proper 54 | kb function 55 | """ 56 | return COMMANDS[function](*args, **kwargs) 57 | 58 | 59 | def main(): 60 | """Main routine of kb.""" 61 | args = parse_args(sys.argv[1:]) 62 | 63 | cmd = args.command 64 | cmd_params = vars(args) 65 | 66 | dispatch(cmd, cmd_params, config=DEFAULT_CONFIG) 67 | -------------------------------------------------------------------------------- /kb/opener.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb opener module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import os 15 | import subprocess 16 | import platform 17 | 18 | 19 | def open_non_text_file(filename): 20 | """ 21 | Open a non-text file 22 | """ 23 | if platform.system() == 'Darwin': 24 | subprocess.Popen(['open', filename]) 25 | elif platform.system() == 'Windows': 26 | os.startfile(filename) 27 | else: 28 | subprocess.Popen(['xdg-open', filename]) 29 | -------------------------------------------------------------------------------- /kb/printer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/kb/printer/__init__.py -------------------------------------------------------------------------------- /kb/printer/grep.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb printer for grep command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import pathlib 15 | from typing import List 16 | from kb.config import DEFAULT_CONFIG as config 17 | import kb.filesystem as fs 18 | from kb.printer.style import ALT_BGROUND, BOLD, UND, RESET 19 | from kb.entities.artifact import Artifact 20 | 21 | 22 | def generate_grep_header( 23 | grep_result: List[Artifact], 24 | hits_list: List[int], 25 | color: bool = True 26 | ) -> None: 27 | """ 28 | Generates kb grep results header. 29 | 30 | Arguments: 31 | grep_result - the list of Artifacts for generating 32 | an adapted header 33 | hits_list - a list ordered as the artifacts 34 | representing the number of grep 35 | matches for that artifact 36 | color - a boolean, True if color is enabled 37 | 38 | Returns: 39 | A string representing the header for the list of artifacts 40 | """ 41 | if not grep_result: 42 | return 43 | 44 | min_length = 20 45 | len_id = max(len(str(len(grep_result) - 1)), 2) 46 | 47 | len_title = max( 48 | max([len(art.title) if art.title else 0 for art in grep_result]), min_length) 49 | len_categ = max(max( 50 | [len(art.category) if art.category else 0 for art in grep_result]), min_length) 51 | len_tags = max( 52 | max([len(art.tags) if art.tags else 0 for art in grep_result]), min_length) 53 | 54 | len_hits = max(len(str(max(hits_list))), 4) 55 | 56 | header = " [ {id} ] {title} {category} {hits} {tags}".format( 57 | id="ID".rjust(len_id), 58 | title="Title".ljust(len_title), 59 | category="Category".ljust(len_categ), 60 | hits="Hits".ljust(len_hits), 61 | tags="Tags".ljust(len_tags)) 62 | 63 | if color: 64 | return UND + BOLD + header + RESET 65 | return header 66 | 67 | 68 | def generate_grep_header_verbose( 69 | grep_result: List[Artifact], 70 | hits_list: List[int], 71 | color: bool = True 72 | ) -> None: 73 | """ 74 | Generates kb grep results header in verbose mode. 75 | 76 | Arguments: 77 | grep_result - the list of Artifacts for generating 78 | an adapted header 79 | hits_list - a list ordered as the artifacts 80 | representing the number of grep 81 | matches for that artifact 82 | color - a boolean, True if color is enabled 83 | 84 | Returns: 85 | A string representing the header for the list of artifacts 86 | """ 87 | 88 | if not grep_result: 89 | return 90 | 91 | min_length = 20 92 | sec_min_length = 10 93 | len_id = max(len(str(len(grep_result) - 1)), 2) 94 | 95 | len_title = max( 96 | max([len(art.title) if art.title else 0 for art in grep_result]), min_length) 97 | len_categ = max(max( 98 | [len(art.category) if art.category else 0 for art in grep_result]), min_length) 99 | len_tags = max( 100 | max([len(art.tags) if art.tags else 0 for art in grep_result]), min_length) 101 | len_author = max(max( 102 | [len(art.author) if art.author else 0 for art in grep_result]), sec_min_length) 103 | len_status = max(max( 104 | [len(art.status) if art.status else 0 for art in grep_result]), sec_min_length) 105 | 106 | len_hits = max(len(str(max(hits_list))), 4) 107 | 108 | header = " [ {id} ] {title} {category} {hits} {tags} {author} {status}".format( 109 | id="ID".rjust(len_id), 110 | title="Title".ljust(len_title), 111 | category="Category".ljust(len_categ), 112 | hits="Hits".ljust(len_hits), 113 | tags="Tags".ljust(len_tags), 114 | author="Author".ljust(len_author), 115 | status="Status".ljust(len_status)) 116 | 117 | if color: 118 | return UND + BOLD + header + RESET 119 | return header 120 | 121 | 122 | def print_grep_result( 123 | grep_result: List[Artifact], 124 | hits_list: List[int], 125 | color: bool = True 126 | ) -> None: 127 | """ 128 | Print kb query grep results. 129 | 130 | Arguments: 131 | grep_result - the list of Artifacts to print 132 | in the form of grep result 133 | hits_list - a list ordered as the artifacts 134 | representing the number of grep 135 | matches for that artifact 136 | color - a boolean, if True, color is enabled 137 | """ 138 | if not grep_result: 139 | return 140 | 141 | print(generate_grep_header(grep_result, hits_list, color=color)) 142 | print() 143 | 144 | min_length = 20 145 | 146 | len_id = max(len(str(len(grep_result) - 1)), 2) 147 | 148 | len_title = max( 149 | max([len(art.title) if art.title else 0 for art in grep_result]), min_length) 150 | len_categ = max(max( 151 | [len(art.category) if art.category else 0 for art in grep_result]), min_length) 152 | len_tags = max( 153 | max([len(art.tags) if art.tags else 0 for art in grep_result]), min_length) 154 | 155 | len_hits = max(len(str(max(hits_list))), 4) 156 | 157 | # Print results 158 | for view_id, artifact in enumerate(grep_result): 159 | tags = artifact.tags if artifact.tags else "" 160 | 161 | hits_id = view_id 162 | hits = str(hits_list[hits_id]) 163 | 164 | result_line = " [ {id} ] {title} {category} {hits} {tags}".format( 165 | id=str(view_id).rjust(len_id), 166 | title=artifact.title.ljust(len_title), 167 | category=artifact.category.ljust(len_categ), 168 | hits=hits.ljust(len_hits), 169 | tags=tags.ljust(len_tags)) 170 | 171 | if color and (view_id % 2 == 0): 172 | print(ALT_BGROUND + result_line + RESET) 173 | else: 174 | print(result_line) 175 | 176 | 177 | def print_grep_result_verbose( 178 | grep_result: List[Artifact], 179 | hits_list: List[int], 180 | color: bool = True 181 | ) -> None: 182 | """ 183 | Print more verbose kb query grep results. 184 | 185 | Arguments: 186 | grep_result - the list of Artifacts to print 187 | sorted by number of hits 188 | hits_list - a list ordered as the grep_result artifacts 189 | (i.e., by number of hits) representing the number 190 | of grep matches for that artifact 191 | color - a boolean, if True, color is enabled 192 | """ 193 | if not grep_result: 194 | return 195 | 196 | print(generate_grep_header(grep_result, hits_list, color=color)) 197 | print() 198 | 199 | min_length = 20 200 | sec_min_length = 10 201 | 202 | len_id = max(len(str(len(grep_result) - 1)), 2) 203 | 204 | len_title = max( 205 | max([len(art.title) if art.title else 0 for art in grep_result]), min_length) 206 | len_categ = max(max( 207 | [len(art.category) if art.category else 0 for art in grep_result]), min_length) 208 | len_tags = max( 209 | max([len(art.tags) if art.tags else 0 for art in grep_result]), min_length) 210 | len_author = max(max( 211 | [len(art.author) if art.author else 0 for art in grep_result]), sec_min_length) 212 | len_status = max(max( 213 | [len(art.status) if art.status else 0 for art in grep_result]), sec_min_length) 214 | 215 | len_hits = max(len(str(max(hits_list))), 4) 216 | 217 | # Print results 218 | for view_id, artifact in enumerate(grep_result): 219 | tags = artifact.tags if artifact.tags else "" 220 | 221 | author = artifact.author if artifact.author else "" 222 | status = artifact.status if artifact.status else "" 223 | 224 | hits_id = view_id 225 | hits = str(hits_list[hits_id]) 226 | 227 | result_line = " [ {id} ] {title} {category} {hits} {tags} {author} {status}".format( 228 | id=str(view_id).rjust(len_id), 229 | title=artifact.title.ljust(len_title), 230 | category=artifact.category.ljust(len_categ), 231 | hits=hits.ljust(len_hits), 232 | tags=tags.ljust(len_tags), 233 | author=author.ljust(len_author), 234 | status=status.ljust(len_status)) 235 | 236 | if color and (view_id % 2 == 0): 237 | print(ALT_BGROUND + result_line + RESET) 238 | else: 239 | print(result_line) 240 | 241 | # This function still has to be implemented, this is just a placeholder 242 | 243 | 244 | def print_grep_matches(grep_matches, color=True): 245 | """ 246 | Print text associated to grep matches. 247 | 248 | Arguments: 249 | grep_matches - the list of Artifacts to print 250 | in the form of grep matches 251 | color - a boolean, if True, color is enabled 252 | """ 253 | 254 | for view_id, match in enumerate(grep_matches): 255 | path = "/".join( 256 | fs.get_filename_parts_wo_prefix( 257 | match[0], config["PATH_KB_DATA"])) 258 | line_number = match[1] 259 | matched_text = match[2] 260 | 261 | if color: 262 | path = BOLD + str(path) + RESET 263 | line_number = BOLD + str(line_number) + RESET 264 | 265 | result_line = "{path}:{line_number}:{matched_text}".format( 266 | path=path, 267 | line_number=line_number, 268 | matched_text=matched_text) 269 | print(result_line) 270 | -------------------------------------------------------------------------------- /kb/printer/search.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb printer for search command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | from typing import List 15 | from kb.printer.style import ALT_BGROUND, BOLD, UND, RESET 16 | from kb.entities.artifact import Artifact 17 | 18 | 19 | def generate_search_header( 20 | search_result: List[Artifact], 21 | color: bool = True 22 | ) -> str: 23 | """ 24 | Generates kb query search results header. 25 | 26 | Arguments: 27 | search_result - the list of Artifacts for generating 28 | an adapted header 29 | color - a boolean, True if color is enabled 30 | 31 | Returns: 32 | A string representing the header for the list of artifacts 33 | """ 34 | 35 | if not search_result: 36 | return 37 | 38 | min_length = 20 39 | len_id = max(len(str(len(search_result) - 1)), 2) 40 | 41 | len_title = max( 42 | max([len(art.title) if art.title else 0 for art in search_result]), min_length) 43 | len_categ = max(max( 44 | [len(art.category) if art.category else 0 for art in search_result]), min_length) 45 | len_tags = max( 46 | max([len(art.tags) if art.tags else 0 for art in search_result]), min_length) 47 | 48 | header = " [ {id} ] {title} {category} {tags}".format( 49 | id="ID".rjust(len_id), 50 | title="Title".ljust(len_title), 51 | category="Category".ljust(len_categ), 52 | tags="Tags".ljust(len_tags)) 53 | 54 | if color: 55 | return UND + BOLD + header + RESET 56 | return header 57 | 58 | 59 | def generate_search_header_verbose( 60 | search_result: List[Artifact], 61 | color: bool = True 62 | ) -> str: 63 | """ 64 | Generates kb query search results header in verbose mode. 65 | 66 | Arguments: 67 | search_result - the list of Artifacts for generating 68 | an adapted header 69 | color - a boolean, True if color is enabled 70 | 71 | Returns: 72 | A string representing the header for the list of artifacts 73 | """ 74 | if not search_result: 75 | return 76 | 77 | min_length = 20 78 | sec_min_length = 10 79 | len_id = max(len(str(len(search_result) - 1)), 2) 80 | 81 | len_title = max( 82 | max([len(art.title) if art.title else 0 for art in search_result]), min_length) 83 | len_categ = max(max( 84 | [len(art.category) if art.category else 0 for art in search_result]), min_length) 85 | len_tags = max( 86 | max([len(art.tags) if art.tags else 0 for art in search_result]), min_length) 87 | len_author = max(max( 88 | [len(art.author) if art.author else 0 for art in search_result]), sec_min_length) 89 | len_status = max(max( 90 | [len(art.status) if art.status else 0 for art in search_result]), sec_min_length) 91 | len_template = max(max( 92 | [len(art.template) if art.template else 0 for art in search_result]), sec_min_length) 93 | 94 | header = " [ {id} ] {title} {category} {tags} {author} {status} {template}".format( 95 | id="ID".rjust(len_id), 96 | title="Title".ljust(len_title), 97 | category="Category".ljust(len_categ), 98 | tags="Tags".ljust(len_tags), 99 | author="Author".ljust(len_author), 100 | status="Status".ljust(len_status), 101 | template="Template".ljust(len_template)) 102 | 103 | if color: 104 | return UND + BOLD + header + RESET 105 | return header 106 | 107 | 108 | def print_search_result( 109 | search_result: List[Artifact], 110 | color: bool = True 111 | ) -> None: 112 | """ 113 | Print kb query search results 114 | 115 | Arguments: 116 | search_result - the list of Artifacts to print 117 | in the form of search result 118 | color - a boolean, True if color is enabled 119 | """ 120 | if not search_result: 121 | return 122 | 123 | print(generate_search_header(search_result, color=color)) 124 | print() 125 | 126 | min_length = 20 127 | len_id = max(len(str(len(search_result) - 1)), 2) 128 | 129 | len_title = max( 130 | max([len(art.title) if art.title else 0 for art in search_result]), min_length) 131 | len_categ = max(max( 132 | [len(art.category) if art.category else 0 for art in search_result]), min_length) 133 | len_tags = max( 134 | max([len(art.tags) if art.tags else 0 for art in search_result]), min_length) 135 | 136 | # Print results 137 | for view_id, artifact in enumerate(search_result): 138 | tags = artifact.tags if artifact.tags else "" 139 | 140 | result_line = " - [ {id} ] {title} {category} {tags}".format( 141 | id=str(view_id).rjust(len_id), 142 | title=artifact.title.ljust(len_title), 143 | category=artifact.category.ljust(len_categ), 144 | tags=tags.ljust(len_tags)) 145 | 146 | if color and (view_id % 2 == 0): 147 | print(ALT_BGROUND + result_line + RESET) 148 | else: 149 | print(result_line) 150 | 151 | 152 | def print_search_result_verbose( 153 | search_result: List[Artifact], 154 | color: bool = True 155 | ) -> None: 156 | """ 157 | Print kb query search results in verbose mode. 158 | 159 | Arguments: 160 | search_result - the list of Artifacts to print 161 | in the form of search result 162 | color - a boolean, True if color is enabled 163 | """ 164 | if not search_result: 165 | return 166 | 167 | print(generate_search_header_verbose(search_result, color=color)) 168 | print() 169 | 170 | min_length = 20 171 | sec_min_length = 10 172 | len_id = max(len(str(len(search_result) - 1)), 2) 173 | 174 | len_title = max( 175 | max([len(art.title) if art.title else 0 for art in search_result]), min_length) 176 | len_categ = max(max( 177 | [len(art.category) if art.category else 0 for art in search_result]), min_length) 178 | len_tags = max( 179 | max([len(art.tags) if art.tags else 0 for art in search_result]), min_length) 180 | len_author = max(max( 181 | [len(art.author) if art.author else 0 for art in search_result]), sec_min_length) 182 | len_status = max(max( 183 | [len(art.status) if art.status else 0 for art in search_result]), sec_min_length) 184 | len_template = max(max( 185 | [len(art.template) if art.template else 0 for art in search_result]), sec_min_length) 186 | 187 | # Print results 188 | for view_id, artifact in enumerate(search_result): 189 | tags = artifact.tags if artifact.tags else "" 190 | author = artifact.author if artifact.author else "" 191 | status = artifact.status if artifact.status else "" 192 | template = artifact.template if artifact.template else "" 193 | 194 | result_line = " [ {id} ] {title} {category} {tags} {author} {status} {template}".format( 195 | id=str(view_id).rjust(len_id), 196 | title=artifact.title.ljust(len_title), 197 | category=artifact.category.ljust(len_categ), 198 | tags=tags.ljust(len_tags), 199 | author=author.ljust(len_author), 200 | status=status.ljust(len_status), 201 | template=template.ljust(len_template)) 202 | 203 | if color and (view_id % 2 == 0): 204 | print(ALT_BGROUND + result_line + RESET) 205 | else: 206 | print(result_line) 207 | 208 | 209 | def print_search_result_full_mode( 210 | search_result: List[Artifact], 211 | ) -> None: 212 | """ 213 | Print kb query search results in full-identifier mode, that is: 214 | category/title 215 | 216 | Arguments: 217 | search_result - the list of Artifacts to print 218 | in the form of search result 219 | """ 220 | if not search_result: 221 | return 222 | # Print results 223 | for view_id, artifact in enumerate(search_result): 224 | 225 | result_line = "{category}/{title}".format( 226 | category=artifact.category, 227 | title=artifact.title) 228 | 229 | print(result_line) 230 | -------------------------------------------------------------------------------- /kb/printer/style.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb printer style module for constants 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | from typing import List 15 | import kb.styler as styler 16 | from kb.entities.artifact import Artifact 17 | 18 | ALT_BGROUND = styler.set_bg('#303030') 19 | BOLD = styler.set_style('bold') 20 | UND = styler.set_style('underline') 21 | RESET = styler.reset() 22 | -------------------------------------------------------------------------------- /kb/printer/template.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb printer for template command module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | from typing import List 15 | from kb.printer.style import ALT_BGROUND, BOLD, UND, RESET 16 | from kb.entities.artifact import Artifact 17 | 18 | 19 | def generate_template_search_header( 20 | color: bool = True 21 | ) -> str: 22 | """ 23 | Generates kb query template results header. 24 | 25 | Arguments: 26 | color - a boolean, True if color is enabled 27 | 28 | Returns: 29 | A string representing the header for the list of templates 30 | """ 31 | header = "Templates" 32 | 33 | if color: 34 | return UND + BOLD + header + RESET 35 | return header 36 | 37 | 38 | def print_template_search_result( 39 | template_search_result: List[str], 40 | color: bool = True 41 | ) -> None: 42 | """ 43 | Print kb template search results. 44 | 45 | Arguments: 46 | template_search_result - the list of templates 47 | color - a boolean, True if color is enabled 48 | """ 49 | 50 | print(generate_template_search_header(color=color)) 51 | print() 52 | 53 | len_template_name = max([len(template) 54 | for template in template_search_result]) 55 | 56 | # Print template search results 57 | for view_id, template in enumerate(template_search_result): 58 | result_line = " - {template}".format( 59 | template=template.ljust(len_template_name)) 60 | 61 | if color and (view_id % 2 == 0): 62 | print(ALT_BGROUND + result_line + RESET) 63 | else: 64 | print(result_line) 65 | -------------------------------------------------------------------------------- /kb/styler.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb styler module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | 13 | This is a gateway for the styler module used to styled text 14 | """ 15 | import colored 16 | 17 | 18 | def set_bg(color: str) -> str: 19 | """ 20 | Set background color. 21 | 22 | Arguments: 23 | color - the color string to set, that 24 | can be either a word (e.g., "green") 25 | or an hex code (e.g., "#00AB00") 26 | Returns: 27 | A string representing the code to set the background color 28 | """ 29 | return colored.bg(color) 30 | 31 | 32 | def set_fg(color: str) -> str: 33 | """ 34 | Set foreground color. 35 | 36 | Arguments: 37 | color - the color string to set, that 38 | can be either a word (e.g., "green") 39 | or an hex code (e.g., "#00AB00") 40 | Returns: 41 | A string representing the code to set the foreground color 42 | """ 43 | return colored.fg(color) 44 | 45 | 46 | def set_style(style: str) -> str: 47 | """ 48 | Set a specific text style 49 | 50 | Arguments: 51 | style - a string representing the desired 52 | style, examples: 53 | "bold" 54 | "underline" 55 | Returns: 56 | A string representing the code to set the desired style 57 | """ 58 | return colored.attr(style) 59 | 60 | 61 | def reset() -> str: 62 | """ 63 | Reset applied style. 64 | 65 | Returns: 66 | A string representing the code to reset the style and colors to default 67 | """ 68 | return colored.attr('reset') 69 | -------------------------------------------------------------------------------- /kb/viewer.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb v0.1.7 3 | # A knowledge base organizer 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | """ 8 | kb viewer module 9 | 10 | :Copyright: © 2020, gnc. 11 | :License: GPLv3 (see /LICENSE). 12 | """ 13 | 14 | import re 15 | from typing import Dict 16 | from kb.styler import set_fg, reset 17 | 18 | 19 | def colorize_string(string, color): 20 | """ 21 | This function applies the provided color to the specified string 22 | 23 | Arguments: 24 | msg - The message to be colored 25 | color - The name of the color (or hex code), e.g., "red" or "#C0C0C0" 26 | 27 | Returns: 28 | A colored message 29 | """ 30 | return set_fg(color) + string + reset() 31 | 32 | 33 | def colorize_row(row, markers=None): 34 | """ 35 | This function takes a string and a dictionary of markers 36 | (where key is regex and value is color) and 37 | the transforms the passed string (row) with colors. 38 | 39 | Arguments: 40 | row - the string 41 | markers - a dictionary having as key strings 42 | representing the purpose of the formatting 43 | and having as values a list where 44 | the first element is a regex and the second 45 | element is the color value. 46 | Example: 47 | markers = { 48 | "TITLE": ["^#.*", "blue"] 49 | "WARNINGS": ["^!.*", "yellow"] 50 | } 51 | 52 | Returns: 53 | A new message colored following the rules within the markers 54 | dictionary 55 | """ 56 | colored_row = row 57 | for mark in markers: 58 | regex = re.compile(rf'{(markers[mark][0])}') 59 | color = markers[mark][1] 60 | 61 | match = regex.search(row) 62 | 63 | if match: 64 | colored_row = re.sub( 65 | regex, colorize_string( 66 | match.group(0).replace( 67 | "\\", "\\\\"), color), rf'{row}') 68 | row = colored_row 69 | 70 | return colored_row 71 | 72 | 73 | def colorize_output(data, markers): 74 | """ 75 | This function takes an input a list of strings, for example they 76 | can be taken from a file or any other source, processes them 77 | and returns a list of formatted colored strings ready to be 78 | visualized. 79 | 80 | Arguments: 81 | data - A list of strings. 82 | markers - an object contains configured marks 83 | 84 | Returns: 85 | A new formatted list 86 | """ 87 | if markers is None: 88 | return data 89 | colorized_output = list() 90 | for row in data: 91 | colorized_output.append(colorize_row(row, markers)) 92 | return colorized_output 93 | 94 | 95 | def view(filepath: str, markers: Dict[str, str], color: bool = True) -> None: 96 | """ 97 | Visualize the specified file with applied markers 98 | if color is True. 99 | 100 | Arguments: 101 | filepath - the file to visualize on stdout 102 | markers - the markers dictionary to apply 103 | color - a boolean, if True color is enabled 104 | """ 105 | content = "" 106 | with open(filepath) as fname: 107 | content = fname.read() 108 | 109 | # Print on screen with proper markers 110 | lines = content.splitlines() 111 | if color: 112 | colored_lines = colorize_output(lines, markers) 113 | for line in colored_lines: 114 | print(line) 115 | else: 116 | for line in lines: 117 | print(line) 118 | -------------------------------------------------------------------------------- /misc/rofi-kb-mode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function gen_options() 4 | { 5 | kb list -f 6 | } 7 | gen_options 8 | 9 | 10 | TERMINAL="alacritty" 11 | 12 | choice="$@" 13 | 14 | if [[ -n "$choice" ]] 15 | then 16 | title=$(basename "$choice") 17 | category=$(dirname "$choice") 18 | coproc ("$TERMINAL" -e kb edit -t "$title" -c "$category" > /dev/null 2>&1 ) 19 | fi 20 | -------------------------------------------------------------------------------- /release: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # The Release Script 3 | # Part of the Python Project Template. 4 | # Copyright © 2013-2018, Chris Warrick. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are 9 | # met: 10 | # 11 | # 1. Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions, and the following disclaimer. 13 | # 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions, and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 18 | # 3. Neither the name of the author of this software nor the names of 19 | # contributors to this software may be used to endorse or promote 20 | # products derived from this software without specific prior written 21 | # consent. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 29 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | . .pypt/config 36 | function status { 37 | echo $@ 38 | } 39 | 40 | function warning { 41 | echo 'WARNING: '$@ 42 | } 43 | 44 | function error { 45 | echo 'ERROR: '$@ 46 | } 47 | 48 | # DEFINE WHAT PROJECTPYPI IS 49 | PROJECTPYPI="kb-manager" 50 | PROJECTGH="kb" 51 | PROJECT="kb" 52 | function cleanup { 53 | rm -rf $PROJECTPYPI.egg-info build || true 54 | rm -rf **/__pycache__ || true 55 | } 56 | 57 | 58 | status '*** gnebbia Release Script' 59 | 60 | echo -n "Version number: " 61 | read version 62 | 63 | echo $version | grep '^[0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}$' > /dev/null 64 | 65 | if [[ $? != 0 ]]; then 66 | echo $version | grep '^[0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\-[0-9A-Za-z-]\{1,\}$' > /dev/null 67 | if [[ $? != 0 ]]; then 68 | warning 'version number is not compliant with versioning scheme (Semantic Versioning 2.0)' 69 | echo -n 'Continue? [y/N] ' 70 | read vercont 71 | if [[ $vercont == 'y' || $vercont == 'Y' ]]; then 72 | echo 'Continuing.' 73 | else 74 | exit 2 75 | fi 76 | else 77 | status 'NOTICE: pre-release version number in use.' 78 | echo -n 'Continue? [Y/n] ' 79 | read vercont 80 | if [[ $vercont == 'n' || $vercont == 'N' ]]; then 81 | exit 2 82 | else 83 | echo 'Continuing.' 84 | fi 85 | fi 86 | fi 87 | 88 | # Creating all the dates at the exact same time. 89 | date=$(date '+%Y-%m-%d') 90 | 91 | 92 | status 'Replacing versions and dates in files...' 93 | sed "s/Version: .*/Version: $version/g" README.md -i 94 | sed "s/Version: .*/Version: $version/g" docs/README.md -i 95 | sed "s/Date: .*/Date: $date/g" README.md -i 96 | sed "s/Date: .*/Date: $date/g" docs/README.md -i 97 | sed "s/version=.*/version='$version',/g" setup.py -i 98 | sed "s/version = .*/version = '$version'/g" docs/conf.py -i 99 | sed "s/release = .*/release = '$version'/g" docs/conf.py -i 100 | sed "s/:Version: .*/:Version: $version/g" docs/**/*.rst -i 101 | sed "s/# $PROJECT v.*/# $PROJECT v$version/" $PROJECT/**/*.py -i 102 | sed "s/__version__ = .*/__version__ = '$version'/g" $PROJECT/__init__.py -i 103 | sed "s/:Date: .*/:Date: $date/g" docs/*.md -i 104 | 105 | # [[ -e "PKGBUILD" ]] && sed "s/pkgver=.*/pkgver=$version/g" PKGBUILD -i || true 106 | # [[ -e "PKGBUILD-git" ]] && sed "s/pkgver=.*/pkgver=$version/g" PKGBUILD-git -i || true 107 | 108 | 109 | if [[ -e tests ]]; then 110 | status 'Running tests...' 111 | pytest tests/ 112 | if [[ $? != 0 ]]; then 113 | error "Tests failed. Fix your code or don't come back." 114 | exit 1 115 | fi 116 | fi 117 | 118 | # ADD THESE 119 | # ./setup.py sdist bdist_wheel || exit $? 120 | # twine upload -s dist/$PROJECTPYPI-$version.tar.gz dist/$PROJECTPYPI-$version*.whl || exit $? 121 | 122 | if [[ -e PKGBUILD ]]; then 123 | status 'Updating AUR PKGBUILDs...' 124 | md5out=$(md5sum 'dist/'$PROJECTLC'-'$version'.tar.gz'|awk '{print $1}') 125 | sed "s/md5sums=.*/md5sums=('$md5out')/" PKGBUILD -i 126 | fi 127 | 128 | cleanup 129 | 130 | # git add -A --ignore-errors . || exit $? 131 | 132 | # git commit -S -asF $cmfn2 || exit $? 133 | # git tag -sm "Version $version" v$version || exit $? 134 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | codespell 3 | coverage 4 | flake8 5 | pyinstaller 6 | pytest 7 | pytest-cov 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attr 2 | attrs 3 | colored 4 | toml 5 | gitpython 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E741 3 | 4 | [wheel] 5 | universal = 1 6 | 7 | [tool:pytest] 8 | norecursedirs = .git 9 | addopts = --cov=kb --cov-report term-missing 10 | 11 | [coverage:run] 12 | branch = True 13 | omit = tests/* 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | import io 4 | from setuptools import setup, find_packages 5 | 6 | 7 | setup(name='kb-manager', 8 | version='0.1.7', 9 | description='A minimalist knowledge base manager', 10 | keywords='kb', 11 | author='gnc', 12 | author_email='nebbionegiuseppe@gmail.com', 13 | url='https://github.com/gnebbia/kb', 14 | download_url='https://github.com/gnebbia/kb/archive/v0.1.7.tar.gz', 15 | license='GPLv3', 16 | long_description=io.open( 17 | './docs/README.md', 'r', encoding='utf-8').read(), 18 | long_description_content_type="text/markdown", 19 | platforms='any', 20 | zip_safe=False, 21 | # http://pypi.python.org/pypi?%3Aaction=list_classifiers 22 | classifiers=[ 'Programming Language :: Python', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Programming Language :: Python :: 3.7', 26 | 'Programming Language :: Python :: 3.8', 27 | 'Operating System :: OS Independent', 28 | ], 29 | packages=find_packages(exclude=('tests',)), 30 | include_package_data=True, 31 | install_requires=["colored","toml","attr","attrs","gitpython"], 32 | python_requires='>=3.6', 33 | entry_points={ 34 | 'console_scripts':[ 35 | 'kb = kb.main:main', 36 | ] 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # kb test suite 3 | # Copyright © 2020, gnc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are 8 | # met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions, and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions, and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # 17 | # 3. Neither the name of the author of this software nor the names of 18 | # contributors to this software may be used to endorse or promote 19 | # products derived from this software without specific prior written 20 | # consent. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /tests/data/.kb/data/pt_ipmi: -------------------------------------------------------------------------------- 1 | # IPMI 2 | 3 | http://www.tenable.com/plugins/index.php?view=single&id=68931 4 | 5 | Tools required: 6 | ipmitool 7 | freeipmi-tools 8 | 9 | ipmitool -I lanplus -H 192.168.0.1 -U Administrator -P notapassword user list 10 | 11 | # Specifying Cipher Suite Zero 12 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword user list 13 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword chassis status 14 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword help 15 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword shell 16 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword sensor 17 | 18 | -------------------------------------------------------------------------------- /tests/data/.kb/data/pt_tls: -------------------------------------------------------------------------------- 1 | openssl s_client -host www.yahoo.com -port 443 2 | sslscan www.yahoo.com 3 | tlssled www.yahoo.com 443 4 | nmap --script sslv2 www.yahoo.com 5 | nmap --script ssl-cert www.yahoo.com 6 | nmap --script ssl-date www.yahoo.com 7 | nmap --script ssl-enum-ciphers www.yahoo.com 8 | nmap --script ssl-google-cert-catalog www.yahoo.com 9 | msf > use auxiliary/pro/web_ssl_scan 10 | msf > use auxiliary/scanner/ssl/openssl_heartbleed 11 | msf > use auxiliary/server/openssl_heartbeat_client_memory 12 | -------------------------------------------------------------------------------- /tests/data/.kb/data/pt_wifi_tips: -------------------------------------------------------------------------------- 1 | # WiFi Pentesting Tips 2 | I've tried to consolidate information from a variety of sources to 3 | assist penetration testing during a wireless assessment. This list 4 | includes plausible tactics, techniques, and procedures (TTP). 5 | I've decided to publish these in an easy to read, and hopefully digestible 6 | blog fashion. Some of the items below will be updated occasionally, 7 | with new attack vectors. 8 | 9 | - Find a device, tool, software or environment that is reliable for RF 10 | testing. There are so many tools, wrappers and hardware to choose from 11 | 12 | - Uncovering Hidden SSIDS: "Hidden SSID is a configuration 13 | where the access point does not broadcast its SSID in beacon frames" 14 | 15 | - Building a Wireless Penetration Environment using Docker, "When 16 | Whales Fly" 17 | 18 | - Aircrack-ng is a complete suite of tools to assess WiFi network security 19 | 20 | - By using your Let's Encrypt certificate 21 | you can effectively avoid internal SSL certificate issues, by not 22 | relying on self-signed certificates. 23 | 24 | - There are a lot of devices you can use to test: "ESP32 WiFi Hash 25 | Monster, store EAPOL & PMKID packets in an SD card using a M5STACK / 26 | ESP32 device" 27 | 28 | - Buy a WiFi Pineapple, "The WiFi Pineapple® NANO and TETRA are the 29 | 6th generation pentest platforms from Hak5. 30 | 31 | - Learn how to use MANA (https://github.com/sensepost/hostapd-mana), 32 | "SensePost's modified hostapd for wifi attacks" 33 | 34 | - Download Wpa sycophant (https://github.com/sensepost/wpa_sycophant) 35 | for an EAP relay attack (https://w1f1.net/) 36 | 37 | - Cheap WiFi hacks (http://deauther.com/) 38 | 39 | - Wpa3-vuln: wpa_supplicant 2.7 vulnerable to Mathy's WPA3 bugs 40 | (https://github.com/sensepost/wpa3-vuln) 41 | 42 | - The default TX-Power of most USB wireless cards is 20 dBm, but by 43 | issuing two commands you can increase your transmission power. 44 | (Type "iw reg set BO" then "iwconfig wlan0 txpower 30") 45 | 46 | - Check out wiggle, there are a lot of of opportunities to use this data 47 | for offensive and defensive research. 48 | 49 | - Captive Portal Attack 50 | (https://github.com/FluxionNetwork/fluxion/wiki/Captive-Portal-Attack) 51 | "attack attempts to retrieve the target access point's WPA/WPA2 key by 52 | means of a rogue network with a border authentication captive portal" 53 | 54 | - crEAP is a python script that will identify WPA Enterprise mode EAP 55 | types and if insecure protocols are in use, will attempt to harvest 56 | usernames and/or handshakes) 57 | (https://www.shellntel.com/blog/2015/9/23/assessing-enterprise-wireless-network) 58 | 59 | - You can deuathenticate a client by using 60 | a command like this: "aireplay-ng -0 1 -a 00:14:6C:7E:40:80 -c 61 | 00:0F:B5:34:30:30 ath0" 62 | 63 | - Download Eaphammer, it allows targeted evil twin attacks against 64 | WPA2-Enterprise networks. Indirect wireless pivots using hostile 65 | portal attacks. 66 | 67 | - Hcxdumptool is a small tool to capture packets from wlan devices 68 | 69 | - Hcxtools is a portable solution for conversion of WiFi Hcxdumptool 70 | files to hashcat formats 71 | 72 | - Freeradius-wpe: Though dated, still may have a valid use case during 73 | a wireless assessment 74 | 75 | - Hostapd-wpe (The first hostapd Evil Twin implementation and my favorite 76 | tool for WPA2-Enterprise attacks) 77 | 78 | - Use the additional WPE command line "-s" which return EAP-Success 79 | messages after credentials are harvested (See Understanding PEAP In-Depth) 80 | (https://sensepost.com/blog/2019/understanding-peap-in-depth/) 81 | 82 | - Pwning WiFi-networks with bettercap and the PMKID client less attack 83 | (https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) 84 | 85 | - Build a pwnagtchi: Pwnagotchi is an A2C-based "AI" 86 | powered by bettercap and running on a Raspberry Pi Zero W that 87 | learns from its surrounding WiFi environment in order to maximize 88 | the crackable WPA key material it captures 89 | 90 | - When actively scanning the client endpoint searches for wireless 91 | networks by transmitting probes and listening for responses from the 92 | access points within range 93 | 94 | - During a passive scan, the client device waits for beacons, 95 | which are specific frames transmitted by the AP. (A probe response, 96 | also includes the configuration and capabilities of the wireless 97 | networks they service) 98 | 99 | - Wifite2: Automates attacks that you may need against WPS/WEP) 100 | 101 | - Some wireless penetration tests may require one to 102 | ask for credentials and log onto their guest network. This is okay 103 | and doesn't mean you have failed. 104 | 105 | - Always check Guest Network for Client Isolation issues. Can I get to 106 | sensitive areas of the "Corporate" network, or any wired network from 107 | the isolated network used by contractors, etc.? 108 | 109 | - Ensure that Client Isolation is on all wireless networks if possible. 110 | (If client isolation is off, then you can perform man-in-the-middle attacks.) 111 | 112 | - Look for rogue access points by detaching your omni antenna, or using 113 | a directional antenna for better results. 114 | 115 | - Look into Fluke gear if performing extensive rogue access point hunting 116 | or heat mapping. 117 | 118 | - Use KRACK to ensure a wireless device that has not been patched or 119 | updated is not vulnerable to known attacks. 120 | 121 | - TP-Link Archer C5 AC1200 is a Wireless Evil Twin Hardware Penetration 122 | Testing Router. 123 | 124 | - Test the companies WIPS/WIPS by connecting a hardware rogue access 125 | point to the corporate network. 126 | 127 | - Most "Ralink Wi-Fi chipset" cards work out the box with Kali .(The 128 | ALFA and Panda's in the gist below work out of the box with the latest 129 | Kali and can scan for 802.11ac clients) 130 | 131 | - Antennas do matter, they have different signal patterns. These signal 132 | patterns make a difference. 133 | 134 | - Signal/power are obviously important, clients will roam from different 135 | the 2.4 and 5 GHz bands based on signal strength and distance to the AP 136 | 137 | - In an EAP downgrade attack, modify the eap.user file when running 138 | hostapd so that it downgrades the EAP connection to use EAP-GTC for 139 | authentication. 140 | (This will affect Android and iOS and Mac OS Supplicants to downgrade 141 | the victim's supplicant to use a less secure method. Let the clear 142 | text credentials rain in, aka getting some L00tbooty) 143 | 144 | - Karma exploits a behaviour of some Wi-Fi devices, combined with the 145 | lack of access point authentication in numerous WiFi protocols. It is 146 | a variant of the evil twin attack. Details of the attack were first 147 | published in 2004 by Dino dai Zovi and Shaun Macaulay. 148 | 149 | - Understand that you are not always trying to attack the access point, 150 | you must go after the user's device or PNL. (Preferred Network List) 151 | 152 | - When it comes to taxonomy and identifying clients, you may find that one 153 | program, or suite of tools is better than the other for certain set tasks. 154 | 155 | - Know Beacon Attack, "An attacker that can guess the ESSID of an 156 | open network in the victim device's Preferred Network List, will be 157 | able to broadcast the corresponding beacon frame and have that device 158 | automatically associate with an attacker-controlled access point" 159 | 160 | - Ever get access to a network that was hardened, but it was a guest 161 | network? Set up your own Evil Twin network, using the real guest WPA2 162 | PSK. This is a technique I use to show why companies should rotate their 163 | guest network passwords, it can affect offices globally. 164 | -------------------------------------------------------------------------------- /tests/data/.kb/data/pth: -------------------------------------------------------------------------------- 1 | # How to pass the hash with common tools 2 | 3 | # Execute WMI queries 4 | pth-wmic -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 "select Name from Win32_UserAccount" 5 | 6 | # Execute WMI commands 7 | pth-wims -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 "cmd.exe /c whoami > c:\temp\result.txt" 8 | 9 | # Connect with SMB shares 10 | pth-smbclient -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25/c$ 11 | 12 | # Connect with SMB shell 13 | pth-rpcclient -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 14 | -------------------------------------------------------------------------------- /tests/data/.kb/kb.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnebbia/kb/862a1abc68da7af5128dfb346a2c0f22e2cc9c1d/tests/data/.kb/kb.db -------------------------------------------------------------------------------- /tests/data/.kb2/data/dir1/pt_ipmi: -------------------------------------------------------------------------------- 1 | # IPMI 2 | 3 | http://www.tenable.com/plugins/index.php?view=single&id=68931 4 | 5 | Tools required: 6 | ipmitool 7 | freeipmi-tools 8 | 9 | ipmitool -I lanplus -H 192.168.0.1 -U Administrator -P notapassword user list 10 | 11 | # Specifying Cipher Suite Zero 12 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword user list 13 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword chassis status 14 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword help 15 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword shell 16 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword sensor 17 | 18 | -------------------------------------------------------------------------------- /tests/data/.kb2/data/dir1/pth: -------------------------------------------------------------------------------- 1 | # How to pass the hash with common tools 2 | 3 | # Execute WMI queries 4 | pth-wmic -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 "select Name from Win32_UserAccount" 5 | 6 | # Execute WMI commands 7 | pth-wims -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 "cmd.exe /c whoami > c:\temp\result.txt" 8 | 9 | # Connect with SMB shares 10 | pth-smbclient -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25/c$ 11 | 12 | # Connect with SMB shell 13 | pth-rpcclient -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 14 | -------------------------------------------------------------------------------- /tests/data/.kb2/data/dir2/dir3/pt_wifi2: -------------------------------------------------------------------------------- 1 | # WiFi Pentesting Tips 2 | I've tried to consolidate information from a variety of sources to 3 | assist penetration testing during a wireless assessment. This list 4 | includes plausible tactics, techniques, and procedures (TTP). 5 | I've decided to publish these in an easy to read, and hopefully digestible 6 | blog fashion. Some of the items below will be updated occasionally, 7 | with new attack vectors. 8 | 9 | - Find a device, tool, software or environment that is reliable for RF 10 | testing. There are so many tools, wrappers and hardware to choose from 11 | 12 | - Uncovering Hidden SSIDS: "Hidden SSID is a configuration 13 | where the access point does not broadcast its SSID in beacon frames" 14 | 15 | - Building a Wireless Penetration Environment using Docker, "When 16 | Whales Fly" 17 | 18 | - Aircrack-ng is a complete suite of tools to assess WiFi network security 19 | 20 | - By using your Let's Encrypt certificate 21 | you can effectively avoid internal SSL certificate issues, by not 22 | relying on self-signed certificates. 23 | 24 | - There are a lot of devices you can use to test: "ESP32 WiFi Hash 25 | Monster, store EAPOL & PMKID packets in an SD card using a M5STACK / 26 | ESP32 device" 27 | 28 | - Buy a WiFi Pineapple, "The WiFi Pineapple® NANO and TETRA are the 29 | 6th generation pentest platforms from Hak5. 30 | 31 | - Learn how to use MANA (https://github.com/sensepost/hostapd-mana), 32 | "SensePost's modified hostapd for wifi attacks" 33 | 34 | - Download Wpa sycophant (https://github.com/sensepost/wpa_sycophant) 35 | for an EAP relay attack (https://w1f1.net/) 36 | 37 | - Cheap WiFi hacks (http://deauther.com/) 38 | 39 | - Wpa3-vuln: wpa_supplicant 2.7 vulnerable to Mathy's WPA3 bugs 40 | (https://github.com/sensepost/wpa3-vuln) 41 | 42 | - The default TX-Power of most USB wireless cards is 20 dBm, but by 43 | issuing two commands you can increase your transmission power. 44 | (Type "iw reg set BO" then "iwconfig wlan0 txpower 30") 45 | 46 | - Check out wiggle, there are a lot of of opportunities to use this data 47 | for offensive and defensive research. 48 | 49 | - Captive Portal Attack 50 | (https://github.com/FluxionNetwork/fluxion/wiki/Captive-Portal-Attack) 51 | "attack attempts to retrieve the target access point's WPA/WPA2 key by 52 | means of a rogue network with a border authentication captive portal" 53 | 54 | - crEAP is a python script that will identify WPA Enterprise mode EAP 55 | types and if insecure protocols are in use, will attempt to harvest 56 | usernames and/or handshakes) 57 | (https://www.shellntel.com/blog/2015/9/23/assessing-enterprise-wireless-network) 58 | 59 | - You can deuathenticate a client by using 60 | a command like this: "aireplay-ng -0 1 -a 00:14:6C:7E:40:80 -c 61 | 00:0F:B5:34:30:30 ath0" 62 | 63 | - Download Eaphammer, it allows targeted evil twin attacks against 64 | WPA2-Enterprise networks. Indirect wireless pivots using hostile 65 | portal attacks. 66 | 67 | - Hcxdumptool is a small tool to capture packets from wlan devices 68 | 69 | - Hcxtools is a portable solution for conversion of WiFi Hcxdumptool 70 | files to hashcat formats 71 | 72 | - Freeradius-wpe: Though dated, still may have a valid use case during 73 | a wireless assessment 74 | 75 | - Hostapd-wpe (The first hostapd Evil Twin implementation and my favorite 76 | tool for WPA2-Enterprise attacks) 77 | 78 | - Use the additional WPE command line "-s" which return EAP-Success 79 | messages after credentials are harvested (See Understanding PEAP In-Depth) 80 | (https://sensepost.com/blog/2019/understanding-peap-in-depth/) 81 | 82 | - Pwning WiFi-networks with bettercap and the PMKID client less attack 83 | (https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) 84 | 85 | - Build a pwnagtchi: Pwnagotchi is an A2C-based "AI" 86 | powered by bettercap and running on a Raspberry Pi Zero W that 87 | learns from its surrounding WiFi environment in order to maximize 88 | the crackable WPA key material it captures 89 | 90 | - When actively scanning the client endpoint searches for wireless 91 | networks by transmitting probes and listening for responses from the 92 | access points within range 93 | 94 | - During a passive scan, the client device waits for beacons, 95 | which are specific frames transmitted by the AP. (A probe response, 96 | also includes the configuration and capabilities of the wireless 97 | networks they service) 98 | 99 | - Wifite2: Automates attacks that you may need against WPS/WEP) 100 | 101 | - Some wireless penetration tests may require one to 102 | ask for credentials and log onto their guest network. This is okay 103 | and doesn't mean you have failed. 104 | 105 | - Always check Guest Network for Client Isolation issues. Can I get to 106 | sensitive areas of the "Corporate" network, or any wired network from 107 | the isolated network used by contractors, etc.? 108 | 109 | - Ensure that Client Isolation is on all wireless networks if possible. 110 | (If client isolation is off, then you can perform man-in-the-middle attacks.) 111 | 112 | - Look for rogue access points by detaching your omni antenna, or using 113 | a directional antenna for better results. 114 | 115 | - Look into Fluke gear if performing extensive rogue access point hunting 116 | or heat mapping. 117 | 118 | - Use KRACK to ensure a wireless device that has not been patched or 119 | updated is not vulnerable to known attacks. 120 | 121 | - TP-Link Archer C5 AC1200 is a Wireless Evil Twin Hardware Penetration 122 | Testing Router. 123 | 124 | - Test the companies WIPS/WIPS by connecting a hardware rogue access 125 | point to the corporate network. 126 | 127 | - Most "Ralink Wi-Fi chipset" cards work out the box with Kali .(The 128 | ALFA and Panda's in the gist below work out of the box with the latest 129 | Kali and can scan for 802.11ac clients) 130 | 131 | - Antennas do matter, they have different signal patterns. These signal 132 | patterns make a difference. 133 | 134 | - Signal/power are obviously important, clients will roam from different 135 | the 2.4 and 5 GHz bands based on signal strength and distance to the AP 136 | 137 | - In an EAP downgrade attack, modify the eap.user file when running 138 | hostapd so that it downgrades the EAP connection to use EAP-GTC for 139 | authentication. 140 | (This will affect Android and iOS and Mac OS Supplicants to downgrade 141 | the victim's supplicant to use a less secure method. Let the clear 142 | text credentials rain in, aka getting some L00tbooty) 143 | 144 | - Karma exploits a behaviour of some Wi-Fi devices, combined with the 145 | lack of access point authentication in numerous WiFi protocols. It is 146 | a variant of the evil twin attack. Details of the attack were first 147 | published in 2004 by Dino dai Zovi and Shaun Macaulay. 148 | 149 | - Understand that you are not always trying to attack the access point, 150 | you must go after the user's device or PNL. (Preferred Network List) 151 | 152 | - When it comes to taxonomy and identifying clients, you may find that one 153 | program, or suite of tools is better than the other for certain set tasks. 154 | 155 | - Know Beacon Attack, "An attacker that can guess the ESSID of an 156 | open network in the victim device's Preferred Network List, will be 157 | able to broadcast the corresponding beacon frame and have that device 158 | automatically associate with an attacker-controlled access point" 159 | 160 | - Ever get access to a network that was hardened, but it was a guest 161 | network? Set up your own Evil Twin network, using the real guest WPA2 162 | PSK. This is a technique I use to show why companies should rotate their 163 | guest network passwords, it can affect offices globally. 164 | -------------------------------------------------------------------------------- /tests/data/.kb2/data/dir2/pt_wifi_tips: -------------------------------------------------------------------------------- 1 | # WiFi Pentesting Tips 2 | I've tried to consolidate information from a variety of sources to 3 | assist penetration testing during a wireless assessment. This list 4 | includes plausible tactics, techniques, and procedures (TTP). 5 | I've decided to publish these in an easy to read, and hopefully digestible 6 | blog fashion. Some of the items below will be updated occasionally, 7 | with new attack vectors. 8 | 9 | - Find a device, tool, software or environment that is reliable for RF 10 | testing. There are so many tools, wrappers and hardware to choose from 11 | 12 | - Uncovering Hidden SSIDS: "Hidden SSID is a configuration 13 | where the access point does not broadcast its SSID in beacon frames" 14 | 15 | - Building a Wireless Penetration Environment using Docker, "When 16 | Whales Fly" 17 | 18 | - Aircrack-ng is a complete suite of tools to assess WiFi network security 19 | 20 | - By using your Let's Encrypt certificate 21 | you can effectively avoid internal SSL certificate issues, by not 22 | relying on self-signed certificates. 23 | 24 | - There are a lot of devices you can use to test: "ESP32 WiFi Hash 25 | Monster, store EAPOL & PMKID packets in an SD card using a M5STACK / 26 | ESP32 device" 27 | 28 | - Buy a WiFi Pineapple, "The WiFi Pineapple® NANO and TETRA are the 29 | 6th generation pentest platforms from Hak5. 30 | 31 | - Learn how to use MANA (https://github.com/sensepost/hostapd-mana), 32 | "SensePost's modified hostapd for wifi attacks" 33 | 34 | - Download Wpa sycophant (https://github.com/sensepost/wpa_sycophant) 35 | for an EAP relay attack (https://w1f1.net/) 36 | 37 | - Cheap WiFi hacks (http://deauther.com/) 38 | 39 | - Wpa3-vuln: wpa_supplicant 2.7 vulnerable to Mathy's WPA3 bugs 40 | (https://github.com/sensepost/wpa3-vuln) 41 | 42 | - The default TX-Power of most USB wireless cards is 20 dBm, but by 43 | issuing two commands you can increase your transmission power. 44 | (Type "iw reg set BO" then "iwconfig wlan0 txpower 30") 45 | 46 | - Check out wiggle, there are a lot of of opportunities to use this data 47 | for offensive and defensive research. 48 | 49 | - Captive Portal Attack 50 | (https://github.com/FluxionNetwork/fluxion/wiki/Captive-Portal-Attack) 51 | "attack attempts to retrieve the target access point's WPA/WPA2 key by 52 | means of a rogue network with a border authentication captive portal" 53 | 54 | - crEAP is a python script that will identify WPA Enterprise mode EAP 55 | types and if insecure protocols are in use, will attempt to harvest 56 | usernames and/or handshakes) 57 | (https://www.shellntel.com/blog/2015/9/23/assessing-enterprise-wireless-network) 58 | 59 | - You can deuathenticate a client by using 60 | a command like this: "aireplay-ng -0 1 -a 00:14:6C:7E:40:80 -c 61 | 00:0F:B5:34:30:30 ath0" 62 | 63 | - Download Eaphammer, it allows targeted evil twin attacks against 64 | WPA2-Enterprise networks. Indirect wireless pivots using hostile 65 | portal attacks. 66 | 67 | - Hcxdumptool is a small tool to capture packets from wlan devices 68 | 69 | - Hcxtools is a portable solution for conversion of WiFi Hcxdumptool 70 | files to hashcat formats 71 | 72 | - Freeradius-wpe: Though dated, still may have a valid use case during 73 | a wireless assessment 74 | 75 | - Hostapd-wpe (The first hostapd Evil Twin implementation and my favorite 76 | tool for WPA2-Enterprise attacks) 77 | 78 | - Use the additional WPE command line "-s" which return EAP-Success 79 | messages after credentials are harvested (See Understanding PEAP In-Depth) 80 | (https://sensepost.com/blog/2019/understanding-peap-in-depth/) 81 | 82 | - Pwning WiFi-networks with bettercap and the PMKID client less attack 83 | (https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) 84 | 85 | - Build a pwnagtchi: Pwnagotchi is an A2C-based "AI" 86 | powered by bettercap and running on a Raspberry Pi Zero W that 87 | learns from its surrounding WiFi environment in order to maximize 88 | the crackable WPA key material it captures 89 | 90 | - When actively scanning the client endpoint searches for wireless 91 | networks by transmitting probes and listening for responses from the 92 | access points within range 93 | 94 | - During a passive scan, the client device waits for beacons, 95 | which are specific frames transmitted by the AP. (A probe response, 96 | also includes the configuration and capabilities of the wireless 97 | networks they service) 98 | 99 | - Wifite2: Automates attacks that you may need against WPS/WEP) 100 | 101 | - Some wireless penetration tests may require one to 102 | ask for credentials and log onto their guest network. This is okay 103 | and doesn't mean you have failed. 104 | 105 | - Always check Guest Network for Client Isolation issues. Can I get to 106 | sensitive areas of the "Corporate" network, or any wired network from 107 | the isolated network used by contractors, etc.? 108 | 109 | - Ensure that Client Isolation is on all wireless networks if possible. 110 | (If client isolation is off, then you can perform man-in-the-middle attacks.) 111 | 112 | - Look for rogue access points by detaching your omni antenna, or using 113 | a directional antenna for better results. 114 | 115 | - Look into Fluke gear if performing extensive rogue access point hunting 116 | or heat mapping. 117 | 118 | - Use KRACK to ensure a wireless device that has not been patched or 119 | updated is not vulnerable to known attacks. 120 | 121 | - TP-Link Archer C5 AC1200 is a Wireless Evil Twin Hardware Penetration 122 | Testing Router. 123 | 124 | - Test the companies WIPS/WIPS by connecting a hardware rogue access 125 | point to the corporate network. 126 | 127 | - Most "Ralink Wi-Fi chipset" cards work out the box with Kali .(The 128 | ALFA and Panda's in the gist below work out of the box with the latest 129 | Kali and can scan for 802.11ac clients) 130 | 131 | - Antennas do matter, they have different signal patterns. These signal 132 | patterns make a difference. 133 | 134 | - Signal/power are obviously important, clients will roam from different 135 | the 2.4 and 5 GHz bands based on signal strength and distance to the AP 136 | 137 | - In an EAP downgrade attack, modify the eap.user file when running 138 | hostapd so that it downgrades the EAP connection to use EAP-GTC for 139 | authentication. 140 | (This will affect Android and iOS and Mac OS Supplicants to downgrade 141 | the victim's supplicant to use a less secure method. Let the clear 142 | text credentials rain in, aka getting some L00tbooty) 143 | 144 | - Karma exploits a behaviour of some Wi-Fi devices, combined with the 145 | lack of access point authentication in numerous WiFi protocols. It is 146 | a variant of the evil twin attack. Details of the attack were first 147 | published in 2004 by Dino dai Zovi and Shaun Macaulay. 148 | 149 | - Understand that you are not always trying to attack the access point, 150 | you must go after the user's device or PNL. (Preferred Network List) 151 | 152 | - When it comes to taxonomy and identifying clients, you may find that one 153 | program, or suite of tools is better than the other for certain set tasks. 154 | 155 | - Know Beacon Attack, "An attacker that can guess the ESSID of an 156 | open network in the victim device's Preferred Network List, will be 157 | able to broadcast the corresponding beacon frame and have that device 158 | automatically associate with an attacker-controlled access point" 159 | 160 | - Ever get access to a network that was hardened, but it was a guest 161 | network? Set up your own Evil Twin network, using the real guest WPA2 162 | PSK. This is a technique I use to show why companies should rotate their 163 | guest network passwords, it can affect offices globally. 164 | -------------------------------------------------------------------------------- /tests/data/.kb2/data/pt_ipmi: -------------------------------------------------------------------------------- 1 | # IPMI 2 | 3 | http://www.tenable.com/plugins/index.php?view=single&id=68931 4 | 5 | Tools required: 6 | ipmitool 7 | freeipmi-tools 8 | 9 | ipmitool -I lanplus -H 192.168.0.1 -U Administrator -P notapassword user list 10 | 11 | # Specifying Cipher Suite Zero 12 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword user list 13 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword chassis status 14 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword help 15 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword shell 16 | ipmitool -I lanplus -C 0 -H 192.168.0.1 -U Administrator -P notapassword sensor 17 | 18 | -------------------------------------------------------------------------------- /tests/data/.kb2/data/pt_tls: -------------------------------------------------------------------------------- 1 | openssl s_client -host www.yahoo.com -port 443 2 | sslscan www.yahoo.com 3 | tlssled www.yahoo.com 443 4 | nmap --script sslv2 www.yahoo.com 5 | nmap --script ssl-cert www.yahoo.com 6 | nmap --script ssl-date www.yahoo.com 7 | nmap --script ssl-enum-ciphers www.yahoo.com 8 | nmap --script ssl-google-cert-catalog www.yahoo.com 9 | msf > use auxiliary/pro/web_ssl_scan 10 | msf > use auxiliary/scanner/ssl/openssl_heartbleed 11 | msf > use auxiliary/server/openssl_heartbeat_client_memory 12 | -------------------------------------------------------------------------------- /tests/data/.kb2/data/pt_wifi_tips: -------------------------------------------------------------------------------- 1 | # WiFi Pentesting Tips 2 | I've tried to consolidate information from a variety of sources to 3 | assist penetration testing during a wireless assessment. This list 4 | includes plausible tactics, techniques, and procedures (TTP). 5 | I've decided to publish these in an easy to read, and hopefully digestible 6 | blog fashion. Some of the items below will be updated occasionally, 7 | with new attack vectors. 8 | 9 | - Find a device, tool, software or environment that is reliable for RF 10 | testing. There are so many tools, wrappers and hardware to choose from 11 | 12 | - Uncovering Hidden SSIDS: "Hidden SSID is a configuration 13 | where the access point does not broadcast its SSID in beacon frames" 14 | 15 | - Building a Wireless Penetration Environment using Docker, "When 16 | Whales Fly" 17 | 18 | - Aircrack-ng is a complete suite of tools to assess WiFi network security 19 | 20 | - By using your Let's Encrypt certificate 21 | you can effectively avoid internal SSL certificate issues, by not 22 | relying on self-signed certificates. 23 | 24 | - There are a lot of devices you can use to test: "ESP32 WiFi Hash 25 | Monster, store EAPOL & PMKID packets in an SD card using a M5STACK / 26 | ESP32 device" 27 | 28 | - Buy a WiFi Pineapple, "The WiFi Pineapple® NANO and TETRA are the 29 | 6th generation pentest platforms from Hak5. 30 | 31 | - Learn how to use MANA (https://github.com/sensepost/hostapd-mana), 32 | "SensePost's modified hostapd for wifi attacks" 33 | 34 | - Download Wpa sycophant (https://github.com/sensepost/wpa_sycophant) 35 | for an EAP relay attack (https://w1f1.net/) 36 | 37 | - Cheap WiFi hacks (http://deauther.com/) 38 | 39 | - Wpa3-vuln: wpa_supplicant 2.7 vulnerable to Mathy's WPA3 bugs 40 | (https://github.com/sensepost/wpa3-vuln) 41 | 42 | - The default TX-Power of most USB wireless cards is 20 dBm, but by 43 | issuing two commands you can increase your transmission power. 44 | (Type "iw reg set BO" then "iwconfig wlan0 txpower 30") 45 | 46 | - Check out wiggle, there are a lot of of opportunities to use this data 47 | for offensive and defensive research. 48 | 49 | - Captive Portal Attack 50 | (https://github.com/FluxionNetwork/fluxion/wiki/Captive-Portal-Attack) 51 | "attack attempts to retrieve the target access point's WPA/WPA2 key by 52 | means of a rogue network with a border authentication captive portal" 53 | 54 | - crEAP is a python script that will identify WPA Enterprise mode EAP 55 | types and if insecure protocols are in use, will attempt to harvest 56 | usernames and/or handshakes) 57 | (https://www.shellntel.com/blog/2015/9/23/assessing-enterprise-wireless-network) 58 | 59 | - You can deuathenticate a client by using 60 | a command like this: "aireplay-ng -0 1 -a 00:14:6C:7E:40:80 -c 61 | 00:0F:B5:34:30:30 ath0" 62 | 63 | - Download Eaphammer, it allows targeted evil twin attacks against 64 | WPA2-Enterprise networks. Indirect wireless pivots using hostile 65 | portal attacks. 66 | 67 | - Hcxdumptool is a small tool to capture packets from wlan devices 68 | 69 | - Hcxtools is a portable solution for conversion of WiFi Hcxdumptool 70 | files to hashcat formats 71 | 72 | - Freeradius-wpe: Though dated, still may have a valid use case during 73 | a wireless assessment 74 | 75 | - Hostapd-wpe (The first hostapd Evil Twin implementation and my favorite 76 | tool for WPA2-Enterprise attacks) 77 | 78 | - Use the additional WPE command line "-s" which return EAP-Success 79 | messages after credentials are harvested (See Understanding PEAP In-Depth) 80 | (https://sensepost.com/blog/2019/understanding-peap-in-depth/) 81 | 82 | - Pwning WiFi-networks with bettercap and the PMKID client less attack 83 | (https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) 84 | 85 | - Build a pwnagtchi: Pwnagotchi is an A2C-based "AI" 86 | powered by bettercap and running on a Raspberry Pi Zero W that 87 | learns from its surrounding WiFi environment in order to maximize 88 | the crackable WPA key material it captures 89 | 90 | - When actively scanning the client endpoint searches for wireless 91 | networks by transmitting probes and listening for responses from the 92 | access points within range 93 | 94 | - During a passive scan, the client device waits for beacons, 95 | which are specific frames transmitted by the AP. (A probe response, 96 | also includes the configuration and capabilities of the wireless 97 | networks they service) 98 | 99 | - Wifite2: Automates attacks that you may need against WPS/WEP) 100 | 101 | - Some wireless penetration tests may require one to 102 | ask for credentials and log onto their guest network. This is okay 103 | and doesn't mean you have failed. 104 | 105 | - Always check Guest Network for Client Isolation issues. Can I get to 106 | sensitive areas of the "Corporate" network, or any wired network from 107 | the isolated network used by contractors, etc.? 108 | 109 | - Ensure that Client Isolation is on all wireless networks if possible. 110 | (If client isolation is off, then you can perform man-in-the-middle attacks.) 111 | 112 | - Look for rogue access points by detaching your omni antenna, or using 113 | a directional antenna for better results. 114 | 115 | - Look into Fluke gear if performing extensive rogue access point hunting 116 | or heat mapping. 117 | 118 | - Use KRACK to ensure a wireless device that has not been patched or 119 | updated is not vulnerable to known attacks. 120 | 121 | - TP-Link Archer C5 AC1200 is a Wireless Evil Twin Hardware Penetration 122 | Testing Router. 123 | 124 | - Test the companies WIPS/WIPS by connecting a hardware rogue access 125 | point to the corporate network. 126 | 127 | - Most "Ralink Wi-Fi chipset" cards work out the box with Kali .(The 128 | ALFA and Panda's in the gist below work out of the box with the latest 129 | Kali and can scan for 802.11ac clients) 130 | 131 | - Antennas do matter, they have different signal patterns. These signal 132 | patterns make a difference. 133 | 134 | - Signal/power are obviously important, clients will roam from different 135 | the 2.4 and 5 GHz bands based on signal strength and distance to the AP 136 | 137 | - In an EAP downgrade attack, modify the eap.user file when running 138 | hostapd so that it downgrades the EAP connection to use EAP-GTC for 139 | authentication. 140 | (This will affect Android and iOS and Mac OS Supplicants to downgrade 141 | the victim's supplicant to use a less secure method. Let the clear 142 | text credentials rain in, aka getting some L00tbooty) 143 | 144 | - Karma exploits a behaviour of some Wi-Fi devices, combined with the 145 | lack of access point authentication in numerous WiFi protocols. It is 146 | a variant of the evil twin attack. Details of the attack were first 147 | published in 2004 by Dino dai Zovi and Shaun Macaulay. 148 | 149 | - Understand that you are not always trying to attack the access point, 150 | you must go after the user's device or PNL. (Preferred Network List) 151 | 152 | - When it comes to taxonomy and identifying clients, you may find that one 153 | program, or suite of tools is better than the other for certain set tasks. 154 | 155 | - Know Beacon Attack, "An attacker that can guess the ESSID of an 156 | open network in the victim device's Preferred Network List, will be 157 | able to broadcast the corresponding beacon frame and have that device 158 | automatically associate with an attacker-controlled access point" 159 | 160 | - Ever get access to a network that was hardened, but it was a guest 161 | network? Set up your own Evil Twin network, using the real guest WPA2 162 | PSK. This is a technique I use to show why companies should rotate their 163 | guest network passwords, it can affect offices globally. 164 | -------------------------------------------------------------------------------- /tests/data/.kb2/data/pth: -------------------------------------------------------------------------------- 1 | # How to pass the hash with common tools 2 | 3 | # Execute WMI queries 4 | pth-wmic -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 "select Name from Win32_UserAccount" 5 | 6 | # Execute WMI commands 7 | pth-wims -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 "cmd.exe /c whoami > c:\temp\result.txt" 8 | 9 | # Connect with SMB shares 10 | pth-smbclient -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25/c$ 11 | 12 | # Connect with SMB shell 13 | pth-rpcclient -U WORKGROUP/Administrator%aad3b435b51404eeaad3b435b51404ee:C0F2E311D3F450A7FF2571BB59FBEDE5 //192.168.1.25 14 | -------------------------------------------------------------------------------- /tests/test_cl_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # kb test suite 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | import re 8 | import kb 9 | 10 | 11 | def test_true(): 12 | """Test if True is truthy.""" 13 | assert True 14 | 15 | 16 | def test_false(): 17 | """Test if False is falsey.""" 18 | assert not False 19 | 20 | 21 | def test_trueexpr(): 22 | """Test if this evaluates as True.""" 23 | assert 1 == 1 24 | 25 | 26 | def test_falseexpr(): 27 | """Test if this evaluates as False.""" 28 | assert 1 != 2 29 | 30 | 31 | def test_math(): 32 | """Test basic arithmetic skills of Python.""" 33 | assert 2 + 2 == 4 34 | assert 2 - 2 == 0 35 | assert 2 * 2 == 4 36 | assert 2 / 2 == 1 37 | assert 3 % 2 == 1 38 | 39 | 40 | def test_bitwise(): 41 | """Test bitwise operators.""" 42 | assert 0b11 ^ 0b10 == 0b01 43 | assert 0b100 | 0b010 == 0b110 44 | assert 0b101 & 0b011 == 0b001 45 | assert 0b10 << 2 == 0b1000 46 | assert 0b1111 >> 2 == 0b11 47 | 48 | def test_strings(): 49 | """ Test strings matching with regex. """ 50 | value = "111.111.111.111" 51 | assert re.match(r'\d+\.\d+\.\d+\.\d+', value) 52 | assert not re.match(r'(^192\.168\.)|(^10\.)|(^172\.)', value) 53 | 54 | 55 | def test_import(): 56 | """Test imports.""" 57 | kb 58 | -------------------------------------------------------------------------------- /tests/test_filesystem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # kb test suite 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | import os 8 | import kb 9 | import kb.filesystem as fs 10 | from pathlib import Path 11 | 12 | 13 | def test_list_files(): 14 | # Test number of files found 15 | files_kb = fs.list_files(Path("tests", "data", ".kb", "data")) 16 | assert len(files_kb) == 4 17 | 18 | files_kb2 = fs.list_files(Path("tests", "data", ".kb2", "data")) 19 | assert len(files_kb2) == 8 20 | assert not len(files_kb2) == 7 21 | 22 | 23 | # Test if files picked are right 24 | kb_list_of_files = ["pt_tls", 25 | "pt_ipmi", 26 | "pth", 27 | "pt_wifi_tips"] 28 | 29 | assert set(files_kb) == set(kb_list_of_files) 30 | 31 | kb2_list_of_files = [str(Path("dir2","dir3","pt_wifi2")), 32 | str(Path("dir2","pt_wifi_tips")), 33 | str(Path("pt_tls")), 34 | str(Path("pt_ipmi")), 35 | str(Path("pth")), 36 | str(Path("dir1","pt_ipmi")), 37 | str(Path("dir1","pth")), 38 | str(Path("pt_wifi_tips"))] 39 | 40 | assert set(files_kb2) == set(kb2_list_of_files) 41 | 42 | def test_list_dirs(): 43 | # Test number of directories found 44 | dir_kb = fs.list_dirs(Path("tests", "data", ".kb", "data")) 45 | assert len(dir_kb) == 0 46 | 47 | dir_kb2 = fs.list_dirs(Path("tests", "data", ".kb2", "data")) 48 | assert len(dir_kb2) == 3 49 | 50 | 51 | # Test if files picked are right 52 | kb_list_of_dirs = [] 53 | 54 | assert set(dir_kb) == set(kb_list_of_dirs) 55 | 56 | kb2_list_of_dirs = [str(Path("dir2")), 57 | str(Path("dir2/dir3")), 58 | str(Path("dir1"))] 59 | 60 | assert set(dir_kb2) == set(kb2_list_of_dirs) 61 | 62 | 63 | 64 | def test_copy_file(): 65 | filename_src = Path("tests","data","sample_data") 66 | filename_dst = Path("tests","data","sample_data_dest") 67 | 68 | with open(filename_src, 'w') as f: 69 | f.write('sample data\n') 70 | assert os.path.exists(filename_src) 71 | 72 | fs.copy_file(filename_src, filename_dst) 73 | assert os.path.exists(filename_dst) 74 | 75 | def test_move_file(): 76 | filename = Path("tests","data","sample_data") 77 | with open(filename, 'w') as f: 78 | f.write('sample data\n') 79 | 80 | assert os.path.exists(filename) 81 | 82 | filename_new = Path("tests","data","new_sample_data") 83 | fs.move_file(filename, filename_new ) 84 | assert not os.path.exists(filename) 85 | assert os.path.exists(filename_new) 86 | 87 | 88 | def test_remove_file(): 89 | filename = Path("tests","data","sample_data") 90 | with open(filename, 'w') as f: 91 | f.write('sample data\n') 92 | 93 | assert os.path.exists(filename) 94 | 95 | fs.remove_file(filename) 96 | assert not os.path.exists(filename) 97 | 98 | def test_get_basename(): 99 | filename = Path("tests","data","sample_data") 100 | with open(filename, 'w') as f: 101 | f.write('sample data\n') 102 | assert "sample_data" == fs.get_basename(filename) 103 | 104 | -------------------------------------------------------------------------------- /tests/test_sanity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # kb test suite 4 | # Copyright © 2020, gnc. 5 | # See /LICENSE for licensing information. 6 | 7 | 8 | import kb 9 | import kb.cl_parser 10 | 11 | 12 | def test_true(): 13 | """Test if True is truthy.""" 14 | assert True 15 | 16 | 17 | def test_false(): 18 | """Test if False is falsey.""" 19 | assert not False 20 | 21 | 22 | def test_trueexpr(): 23 | """Test if this evaluates as True.""" 24 | assert 1 == 1 25 | 26 | 27 | def test_falseexpr(): 28 | """Test if this evaluates as False.""" 29 | assert 1 != 2 30 | 31 | 32 | def test_math(): 33 | """Test basic arithmetic skills of Python.""" 34 | assert 2 + 2 == 4 35 | assert 2 - 2 == 0 36 | assert 2 * 2 == 4 37 | assert 2 / 2 == 1 38 | assert 3 % 2 == 1 39 | 40 | 41 | def test_bitwise(): 42 | """Test bitwise operators.""" 43 | assert 0b11 ^ 0b10 == 0b01 44 | assert 0b100 | 0b010 == 0b110 45 | assert 0b101 & 0b011 == 0b001 46 | assert 0b10 << 2 == 0b1000 47 | assert 0b1111 >> 2 == 0b11 48 | 49 | 50 | def test_import(): 51 | """Test imports.""" 52 | kb 53 | kb.cl_parser 54 | --------------------------------------------------------------------------------