├── .VERSION ├── _CI ├── bin │ └── .gitkeep ├── .VERSION ├── patches │ └── .gitkeep ├── files │ ├── logging_level.json │ ├── environment_variables.json │ └── prerequisites.json ├── __init__.py ├── scripts │ ├── __init__.py │ ├── bootstrap.py │ ├── reset.py │ ├── lint.py │ ├── lock.py │ ├── graph.py │ ├── _initialize_template.py │ ├── test.py │ ├── document.py │ ├── upload.py │ ├── build.py │ ├── update.py │ └── tag.py ├── configuration │ ├── __init__.py │ └── configuration.py └── library │ ├── __init__.py │ ├── core_library.py │ └── patch.py ├── graphs └── .gitkeep ├── docs ├── authors.rst ├── history.rst ├── readme.rst ├── usage.rst ├── contributing.rst ├── installation.rst ├── index.rst ├── Makefile └── conf.py ├── tests ├── README ├── __init__.py └── test_wikiseriesasorkunlib.py ├── setup.cfg ├── pylintrc ├── .prospector.yaml ├── HISTORY.rst ├── AUTHORS.rst ├── INSTALLATION.rst ├── .editorconfig ├── requirements.txt ├── MANIFEST.in ├── setup_aliases.ps1 ├── Pipfile ├── .gitlab-ci.yml ├── dev-requirements.txt ├── tox.ini ├── .gitignore ├── setup_aliases.sh ├── LICENSE ├── USAGE.rst ├── .cruft.json ├── CONTRIBUTING.rst ├── .github └── workflows │ └── main.yml ├── wikiseriesasorkunlib ├── wikiseriesasorkunlibexceptions.py ├── __init__.py ├── _version.py └── wikiseriesasorkunlib.py ├── setup.py └── README.rst /.VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 -------------------------------------------------------------------------------- /_CI/bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /graphs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_CI/.VERSION: -------------------------------------------------------------------------------- 1 | 0.0.0 2 | -------------------------------------------------------------------------------- /_CI/patches/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../USAGE.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../INSTALLATION.rst 2 | -------------------------------------------------------------------------------- /_CI/files/logging_level.json: -------------------------------------------------------------------------------- 1 | { 2 | "level": "info" 3 | } 4 | -------------------------------------------------------------------------------- /tests/README: -------------------------------------------------------------------------------- 1 | Please place testing code here. The name should be: test_.py 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [nosetests] 5 | verbosity = 3 6 | with-coverage=1 7 | cover-package=wikiseriesasorkunlib/ 8 | cover-erase=1 9 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | max-line-length=121 3 | disable=locally-disabled, locally-enabled, logging-format-interpolation 4 | 5 | [General] 6 | init-hook='import sys; sys.path.append("wikiseriesasorkunlib")' 7 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | pep257: 2 | disable: 3 | - D203 4 | - D212 5 | - D107 6 | - D105 7 | - D213 8 | - D406 9 | - D407 10 | ignore-paths: 11 | - _CI 12 | - build 13 | - docs 14 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.0.1 (26-04-2023) 7 | --------------------- 8 | 9 | * First code creation 10 | 11 | 12 | 0.1.0 (26-04-2023) 13 | ------------------ 14 | 15 | * 16 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Costas Tyfoxylos 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /_CI/files/environment_variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "PIPENV_VENV_IN_PROJECT": "true", 3 | "PIPENV_DEFAULT_PYTHON_VERSION": "3.9", 4 | "PYPI_URL": "https://upload.pypi.org/legacy/", 5 | "PROJECT_SLUG": "wikiseriesasorkunlib" 6 | } 7 | -------------------------------------------------------------------------------- /_CI/files/prerequisites.json: -------------------------------------------------------------------------------- 1 | { 2 | "executables": [ 3 | "pipenv", 4 | "make" 5 | ], 6 | "environment_variables": [ 7 | ], 8 | "upload_environment_variables": [ 9 | "PYPI_UPLOAD_USERNAME", 10 | "PYPI_UPLOAD_PASSWORD", 11 | "PYPI_URL" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /INSTALLATION.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ pip install wikiseriesasorkunlib 8 | 9 | Or, if you have virtualenvwrapper installed:: 10 | 11 | $ mkvirtualenv wikiseriesasorkunlib 12 | $ pip install wikiseriesasorkunlib 13 | 14 | Or, if you are using pipenv:: 15 | 16 | $ pipenv install wikiseriesasorkunlib 17 | 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Please do not manually update this file since the requirements are managed 3 | # by pipenv through Pipfile and Pipfile.lock . 4 | # 5 | # This file is created and managed automatically by the template and it is left 6 | # here only for backwards compatibility reasons with python's ecosystem. 7 | # 8 | # Please use Pipfile to update the requirements. 9 | # 10 | requests~=2.28.2 11 | bs4~=0.0.1 -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include .VERSION 2 | include AUTHORS.rst 3 | include CONTRIBUTING.rst 4 | include HISTORY.rst 5 | include LICENSE 6 | include README.rst 7 | include USAGE.rst 8 | include Pipfile 9 | include Pipfile.lock 10 | include requirements.txt 11 | include dev-requirements.txt 12 | include wikiseriesasorkunlib/.VERSION 13 | 14 | recursive-exclude * __pycache__ 15 | recursive-exclude * *.py[co] 16 | 17 | recursive-include docs *.rst conf.py Makefile 18 | recursive-include wikiseriesasorkunlib * 19 | -------------------------------------------------------------------------------- /setup_aliases.ps1: -------------------------------------------------------------------------------- 1 | $commands = $(Get-ChildItem ./_CI/scripts -Exclude "_*" |select Name |% {$_.name.split('.')[0]}) 2 | 3 | function New-Alias{ 4 | param ( 5 | $command 6 | ) 7 | $Path="_CI/scripts/$command.py" 8 | $CommandText = 'function _'+$command+'() { if (Test-Path '+$path+') {python '+$path+' $args} else{write-host "executable not found at"'+$path+' -ForegroundColor Red} }' 9 | 10 | Write-Output $CommandText 11 | } 12 | 13 | foreach ($command in $commands) { 14 | Invoke-Expression(New-Alias($command)) 15 | } 16 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | 8 | 9 | sphinx = ">=6.0,<7.0" 10 | sphinx-rtd-theme = ">=1.0,<2.0" 11 | prospector = ">=1.8,<2.0" 12 | coverage = ">=7,<8.0" 13 | nose = ">=1.3,<2.0" 14 | nose-htmloutput = ">=0.1,<1.0" 15 | tox = ">=4.0<5.0" 16 | betamax = ">=0.8,<1.0" 17 | betamax-serializers = "~=0.2,<1.0" 18 | semver = ">=2.0,<3.0" 19 | gitwrapperlib = ">=1.0,<2.0" 20 | twine = ">=4.0,<5.0" 21 | coloredlogs = ">=15.0,<16.0" 22 | emoji = ">=2.0,<3.0" 23 | toml = ">=0.1,<1.0" 24 | 25 | 26 | [packages] 27 | 28 | requests = "*" 29 | bs4 = "*" 30 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. wikiseriesasorkunlib documentation master file, created by 2 | sphinx-quickstart on 2023-04-26. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to wikiseriesasorkunlib's documentation! 7 | ================================================ 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | readme 15 | installation 16 | usage 17 | contributing 18 | modules 19 | authors 20 | history 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - lint 3 | - test 4 | - build 5 | - upload 6 | 7 | lint: 8 | tags: [docker] 9 | stage: lint 10 | image: IMAGE_WITH_PYTHON39_AND_PIPENV 11 | script: _CI/scripts/lint.py 12 | 13 | test: 14 | tags: [docker] 15 | stage: test 16 | image: IMAGE_WITH_PYTHON39_AND_PIPENV 17 | script: _CI/scripts/test.py 18 | 19 | build: 20 | tags: [docker] 21 | stage: build 22 | image: IMAGE_WITH_PYTHON39_AND_PIPENV 23 | script: _CI/scripts/build.py 24 | 25 | upload: 26 | tags: [docker] 27 | stage: upload 28 | image: IMAGE_WITH_PYTHON39_AND_PIPENV 29 | only: 30 | - tags 31 | except: 32 | - branches 33 | script: _CI/scripts/upload.py 34 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Please do not manually update this file since the requirements are managed 3 | # by pipenv through Pipfile and Pipfile.lock . 4 | # 5 | # This file is created and managed automatically by the template and it is left 6 | # here only for backwards compatibility reasons with python's ecosystem. 7 | # 8 | # Please use Pipfile to update the requirements. 9 | # 10 | sphinx>=6.2.1 11 | sphinx-rtd-theme>=1.2.0 12 | prospector>=1.9.0 13 | coverage>=7.2.3 14 | nose>=1.3.7 15 | nose-htmloutput>=0.6.0 16 | tox>=4.5.0 17 | betamax>=0.8.1 18 | betamax-serializers~=0.2.1 19 | semver>=2.13.0 20 | gitwrapperlib>=1.0.0 21 | twine>=4.0.2 22 | coloredlogs>=15.0.1 23 | emoji>=2.2.0 24 | toml>=0.10.2 -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # Tox (http://tox.testrun.org/) is a tool for running tests 3 | # in multiple virtualenvs. This configuration file will run the 4 | # test suite on all supported python versions. To use it, "pip install tox" 5 | # and then run "tox" from this directory. 6 | 7 | [tox] 8 | envlist = py39, 9 | 10 | [testenv] 11 | allowlist_externals = * 12 | commands = ./setup.py nosetests --with-coverage --cover-tests --cover-html --cover-html-dir=test-output/coverage --with-html --html-file test-output/nosetests.html 13 | deps = 14 | -rrequirements.txt 15 | -rdev-requirements.txt 16 | passenv = http_proxy,HTTP_PROXY,https_proxy,HTTPS_PROXY,no_proxy,NO_PROXY 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Packages 2 | *.egg 3 | *.egg-info 4 | /dist/ 5 | /build/ 6 | /_build/ 7 | .eggs/ 8 | /eggs/ 9 | /parts/ 10 | /var/ 11 | /sdist/ 12 | /develop-eggs/ 13 | .installed.cfg 14 | /lib/ 15 | /lib64/ 16 | 17 | *.py[cod] 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | test-output 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # Complexity 41 | output/*.html 42 | output/*/index.html 43 | 44 | # Sphinx 45 | docs/_build 46 | 47 | # pyCharm 48 | .idea 49 | 50 | # VirtualEnv 51 | env 52 | .venv 53 | 54 | # Mac 55 | .DS_Store 56 | 57 | # Variables file 58 | .env 59 | 60 | # pyenv local python version marker 61 | .python-version 62 | 63 | # Visual studio code 64 | .vscode 65 | -------------------------------------------------------------------------------- /setup_aliases.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Needs to be sourced 4 | # Sets up alias functions for the interface while keeping backwards compatibility with the old bash type 5 | 6 | for command in $(ls _CI/scripts/ | cut -d'.' -f1 | grep -v "^_") 7 | do 8 | eval "_$command() { if [ -f _CI/scripts/$command.py ]; then ./_CI/scripts/$command.py \"\$@\"; elif [ -f _CI/scripts/$command ]; then ./_CI/scripts/$command \"\$@\" ;else echo \"Command ./_CI/scripts/$command.py or ./_CI/scripts/$command not found\" ; fi }" 9 | done 10 | 11 | function _activate() { 12 | EXIT_CODE=false 13 | for path_ in '.venv/bin/activate' '_CI/files/.venv/bin/activate' 14 | do 15 | if [ -f "${path_}" ]; then 16 | . "${path_}" 17 | EXIT_CODE=true 18 | break 19 | fi 20 | done 21 | if [ "${EXIT_CODE}" = false ]; then 22 | echo Could not find virtual environment to activate 23 | fi 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Costas Tyfoxylos 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /USAGE.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | 6 | To develop on wikiseriesasorkunlib: 7 | 8 | .. code-block:: bash 9 | 10 | # The following commands require pipenv as a dependency 11 | 12 | # To lint the project 13 | _CI/scripts/lint.py 14 | 15 | # To execute the testing 16 | _CI/scripts/test.py 17 | 18 | # To create a graph of the package and dependency tree 19 | _CI/scripts/graph.py 20 | 21 | # To build a package of the project under the directory "dist/" 22 | _CI/scripts/build.py 23 | 24 | # To see the package version 25 | _CI/scripts/tag.py 26 | 27 | # To bump semantic versioning [--major|--minor|--patch] 28 | _CI/scripts/tag.py --major|--minor|--patch 29 | 30 | # To upload the project to a pypi repo if user and password are properly provided 31 | _CI/scripts/upload.py 32 | 33 | # To build the documentation of the project 34 | _CI/scripts/document.py 35 | 36 | 37 | To use wikiseriesasorkunlib in a project: 38 | 39 | .. code-block:: python 40 | 41 | from wikiseriesasorkunlib import Wikiseriesasorkunlib 42 | wikiseriesasorkunlib = Wikiseriesasorkunlib() 43 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/costastf/python_project.git", 3 | "commit": "5e7a9ebf062313dee14f2dd5c5117df76adef416", 4 | "checkout": null, 5 | "context": { 6 | "cookiecutter": { 7 | "project_type": "lib", 8 | "full_name": "Costas Tyfoxylos", 9 | "email": "asork@gmail.com", 10 | "repo_name": "Wiki Series Asorkun", 11 | "main_branch_name": "main", 12 | "project_slug": "wikiseriesasorkunlib", 13 | "project_short_description": "This my project", 14 | "git_url": "https://github.com/alisorkuncuk/wikiseriesasorkunlib", 15 | "pypi_repository": "https://upload.pypi.org/legacy/", 16 | "gitlab_runner_image": "IMAGE_WITH_PYTHON39_AND_PIPENV", 17 | "gitlab_runner_tags": "[docker]", 18 | "tags": "tv series", 19 | "development_status": "Alpha", 20 | "compatible_versions": "3.9", 21 | "documentation_url": "https://wikiseriesasorkunlib.readthedocs.org/en/latest", 22 | "release_date": "26-04-2023", 23 | "year": "2023", 24 | "license": "MIT", 25 | "_extensions": [ 26 | "jinja2_time.TimeExtension" 27 | ], 28 | "_template": "https://github.com/costastf/python_project.git" 29 | } 30 | }, 31 | "directory": null 32 | } 33 | -------------------------------------------------------------------------------- /_CI/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | -------------------------------------------------------------------------------- /_CI/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Submit Feedback 9 | ~~~~~~~~~~~~~~~ 10 | 11 | If you are proposing a feature: 12 | 13 | * Explain in detail how it would work. 14 | * Keep the scope as narrow as possible, to make it easier to implement. 15 | 16 | Get Started! 17 | ------------ 18 | 19 | Ready to contribute? Here's how to set up `wikiseriesasorkunlib` for local development. 20 | Using of pipenv is highly recommended. 21 | 22 | 1. Clone your fork locally:: 23 | 24 | $ git clone https://github.com/alisorkuncuk/wikiseriesasorkunlib 25 | 26 | 2. Install your local copy into a virtualenv. Assuming you have pipenv installed, this is how you set up your clone for local development:: 27 | 28 | $ cd wikiseriesasorkunlib/ 29 | $ pipenv install --ignore-pipfile 30 | 31 | 3. Create a branch for local development:: 32 | 33 | $ git checkout -b name-of-your-bugfix-or-feature 34 | 35 | Now you can make your changes locally. 36 | Do your development while using the CI capabilities and making sure the code passes lint, test, build and document stages. 37 | 38 | 39 | 4. Commit your changes and push your branch to the server:: 40 | 41 | $ git add . 42 | $ git commit -m "Your detailed description of your changes." 43 | $ git push origin name-of-your-bugfix-or-feature 44 | 45 | 5. Submit a merge request 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ 'main' ] 6 | tags: [ '*' ] 7 | pull_request: 8 | branches: [ 'main' ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.9.15 23 | 24 | - name: Install pipenv 25 | run: pip install pipenv 26 | 27 | - name: Lint 28 | run: _CI/scripts/lint.py 29 | 30 | - name: Test 31 | run: _CI/scripts/test.py 32 | 33 | - name: Build 34 | run: _CI/scripts/build.py 35 | 36 | release: 37 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 38 | needs: build 39 | runs-on: ubuntu-latest 40 | environment: 41 | name: release 42 | steps: 43 | - name: Checkout sources 44 | uses: actions/checkout@v3 45 | 46 | - name: Set up Python 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: 3.9.15 50 | 51 | - name: Install pipenv 52 | run: pip install pipenv 53 | 54 | - name: Upload 55 | run: _CI/scripts/upload.py 56 | 57 | env: 58 | 59 | PYPI_UPLOAD_USERNAME: ${{ secrets.PYPI_UPLOAD_USERNAME }} 60 | PYPI_UPLOAD_PASSWORD: ${{ secrets.PYPI_UPLOAD_PASSWORD }} 61 | 62 | -------------------------------------------------------------------------------- /_CI/configuration/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | from .configuration import (LOGGING_LEVEL, 26 | ENVIRONMENT_VARIABLES, 27 | PREREQUISITES, 28 | BUILD_REQUIRED_FILES, 29 | LOGGERS_TO_DISABLE, 30 | BRANCHES_SUPPORTED_FOR_TAG, 31 | PROJECT_SLUG) 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2023 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | .. _Google Python Style Guide: 28 | http://google.github.io/styleguide/pyguide.html 29 | """ 30 | 31 | __author__ = '''Costas Tyfoxylos ''' 32 | __docformat__ = '''google''' 33 | __date__ = '''26-04-2023''' 34 | __copyright__ = '''Copyright 2023, Costas Tyfoxylos''' 35 | __license__ = '''MIT''' 36 | __maintainer__ = '''Costas Tyfoxylos''' 37 | __email__ = '''''' 38 | __status__ = '''Development''' # "Prototype", "Development", "Production". 39 | -------------------------------------------------------------------------------- /_CI/scripts/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: bootstrap.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import logging 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from configuration import LOGGING_LEVEL 33 | from library import setup_logging 34 | 35 | # This is the main prefix used for logging 36 | LOGGER_BASENAME = '''_CI.bootstrap''' 37 | LOGGER = logging.getLogger(LOGGER_BASENAME) 38 | LOGGER.addHandler(logging.NullHandler()) 39 | 40 | 41 | def bootstrap(): 42 | setup_logging(os.environ.get("LOGGING_LEVEL") or LOGGING_LEVEL) 43 | 44 | 45 | if __name__ == '__main__': 46 | bootstrap() 47 | -------------------------------------------------------------------------------- /wikiseriesasorkunlib/wikiseriesasorkunlibexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: wikiseriesasorkunlibexceptions.py 4 | # 5 | # Copyright 2023 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Custom exception code for wikiseriesasorkunlib. 28 | 29 | .. _Google Python Style Guide: 30 | https://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | __author__ = '''Costas Tyfoxylos ''' 35 | __docformat__ = '''google''' 36 | __date__ = '''26-04-2023''' 37 | __copyright__ = '''Copyright 2023, Costas Tyfoxylos''' 38 | __credits__ = ["Costas Tyfoxylos"] 39 | __license__ = '''MIT''' 40 | __maintainer__ = '''Costas Tyfoxylos''' 41 | __email__ = '''''' 42 | __status__ = '''Development''' # "Prototype", "Development", "Production". 43 | -------------------------------------------------------------------------------- /wikiseriesasorkunlib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2023 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | wikiseriesasorkunlib package. 28 | 29 | Import all parts from wikiseriesasorkunlib here 30 | 31 | .. _Google Python Style Guide: 32 | https://google.github.io/styleguide/pyguide.html 33 | """ 34 | from ._version import __version__ 35 | 36 | __author__ = '''Costas Tyfoxylos ''' 37 | __docformat__ = '''google''' 38 | __date__ = '''26-04-2023''' 39 | __copyright__ = '''Copyright 2023, Costas Tyfoxylos''' 40 | __license__ = '''MIT''' 41 | __maintainer__ = '''Costas Tyfoxylos''' 42 | __email__ = '''''' 43 | __status__ = '''Development''' # "Prototype", "Development", "Production". 44 | 45 | # This is to 'use' the module(s), so lint doesn't complain 46 | assert __version__ 47 | -------------------------------------------------------------------------------- /_CI/library/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: __init__.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | from .core_library import (activate_template, 27 | execute_command, 28 | setup_logging, 29 | get_project_root_path, 30 | validate_binary_prerequisites, 31 | validate_environment_variable_prerequisites, 32 | is_venv_created, 33 | load_environment_variables, 34 | load_dot_env_file, 35 | clean_up, 36 | save_requirements, 37 | open_file, 38 | bump, 39 | activate_virtual_environment, 40 | tempdir, 41 | update_pipfile) 42 | -------------------------------------------------------------------------------- /_CI/scripts/reset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: reset.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import sys 28 | import shutil 29 | import stat 30 | import logging 31 | 32 | # this sets up everything and MUST be included before any third party module in every step 33 | import _initialize_template 34 | 35 | from configuration import ENVIRONMENT_VARIABLES 36 | from library import clean_up, get_project_root_path 37 | 38 | # This is the main prefix used for logging 39 | LOGGER_BASENAME = '''_CI.reset''' 40 | LOGGER = logging.getLogger(LOGGER_BASENAME) 41 | LOGGER.addHandler(logging.NullHandler()) 42 | 43 | 44 | def reset(environment_variables): 45 | pipfile_path = environment_variables.get('PIPENV_PIPFILE', 'Pipfile') 46 | venv = os.path.join(get_project_root_path(), os.path.dirname(pipfile_path), '.venv') 47 | clean_up(venv) 48 | 49 | 50 | if __name__ == '__main__': 51 | reset(ENVIRONMENT_VARIABLES) 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | try: 6 | from pipenv.project import Project 7 | from pipenv.utils import convert_deps_to_pip 8 | 9 | pfile = Project().parsed_pipfile 10 | requirements = convert_deps_to_pip(pfile['packages'], r=False) 11 | test_requirements = convert_deps_to_pip(pfile['dev-packages'], r=False) 12 | except ImportError: 13 | # get the requirements from the requirements.txt 14 | requirements = [line.strip() 15 | for line in open('requirements.txt').readlines() 16 | if line.strip() and not line.startswith('#')] 17 | # get the test requirements from the test_requirements.txt 18 | test_requirements = [line.strip() 19 | for line in 20 | open('dev-requirements.txt').readlines() 21 | if line.strip() and not line.startswith('#')] 22 | 23 | readme = open('README.rst').read() 24 | history = open('HISTORY.rst').read().replace('.. :changelog:', '') 25 | version = open('.VERSION').read() 26 | 27 | 28 | setup( 29 | name='''wikiseriesasorkunlib''', 30 | version=version, 31 | description='''This my project''', 32 | long_description=readme + '\n\n' + history, 33 | author='''Costas Tyfoxylos''', 34 | author_email='''asork@gmail.com''', 35 | url='''https://github.com/alisorkuncuk/wikiseriesasorkunlib''', 36 | packages=find_packages(where='.', exclude=('tests', 'hooks', '_CI*')), 37 | package_dir={'''wikiseriesasorkunlib''': 38 | '''wikiseriesasorkunlib'''}, 39 | include_package_data=True, 40 | install_requires=requirements, 41 | license='MIT', 42 | zip_safe=False, 43 | keywords='''wikiseriesasorkunlib tv series''', 44 | classifiers=[ 45 | 'Development Status :: 3 - Alpha', 46 | 'Intended Audience :: Developers', 47 | 'License :: OSI Approved :: MIT License', 48 | 'Natural Language :: English', 49 | 'Programming Language :: Python :: 3.9', 50 | ], 51 | test_suite='tests', 52 | tests_require=test_requirements 53 | ) 54 | -------------------------------------------------------------------------------- /_CI/scripts/lint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: lint.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | 28 | # this sets up everything and MUST be included before any third party module in every step 29 | import _initialize_template 30 | 31 | from bootstrap import bootstrap 32 | from emoji import emojize 33 | from library import execute_command 34 | 35 | 36 | # This is the main prefix used for logging 37 | LOGGER_BASENAME = '''_CI.lint''' 38 | LOGGER = logging.getLogger(LOGGER_BASENAME) 39 | LOGGER.addHandler(logging.NullHandler()) 40 | 41 | 42 | def lint(): 43 | bootstrap() 44 | success = execute_command('prospector -DFM --no-autodetect') 45 | if success: 46 | LOGGER.info('%s No linting errors found! %s', 47 | emojize(':check_mark_button:'), 48 | emojize(':thumbs_up:')) 49 | else: 50 | LOGGER.error('%s Linting errors found! %s', 51 | emojize(':cross_mark:'), 52 | emojize(':crying_face:')) 53 | raise SystemExit(0 if success else 1) 54 | 55 | 56 | if __name__ == '__main__': 57 | lint() 58 | -------------------------------------------------------------------------------- /_CI/scripts/lock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: rebuild_pipfile.py 4 | # 5 | # Copyright 2019 Ilija Matoski 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import argparse 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from bootstrap import bootstrap 33 | from library import update_pipfile 34 | 35 | # This is the main prefix used for logging 36 | LOGGER_BASENAME = '''_CI.build''' 37 | LOGGER = logging.getLogger(LOGGER_BASENAME) 38 | LOGGER.addHandler(logging.NullHandler()) 39 | 40 | def get_arguments(): 41 | parser = argparse.ArgumentParser(description='Regenerates Pipfile based on Pipfile.lock') 42 | parser.add_argument('--stdout', 43 | help='Output the Pipfile to stdout', 44 | action="store_true", 45 | default=False) 46 | args = parser.parse_args() 47 | return args 48 | 49 | 50 | def execute(): 51 | bootstrap() 52 | args = get_arguments() 53 | return update_pipfile(args.stdout) 54 | 55 | 56 | if __name__ == '__main__': 57 | raise SystemExit(not execute()) 58 | -------------------------------------------------------------------------------- /wikiseriesasorkunlib/_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: _version.py 4 | # 5 | # Copyright 2023 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Manages the version of the package. 28 | 29 | .. _Google Python Style Guide: 30 | https://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import os 35 | 36 | __author__ = '''Costas Tyfoxylos ''' 37 | __docformat__ = '''google''' 38 | __date__ = '''26-04-2023''' 39 | __copyright__ = '''Copyright 2023, Costas Tyfoxylos''' 40 | __license__ = '''MIT''' 41 | __maintainer__ = '''Costas Tyfoxylos''' 42 | __email__ = '''''' 43 | __status__ = '''Development''' # "Prototype", "Development", "Production". 44 | 45 | VERSION_FILE_PATH = os.path.abspath( 46 | os.path.join( 47 | os.path.dirname(__file__), 48 | '..', 49 | '.VERSION' 50 | ) 51 | ) 52 | 53 | LOCAL_VERSION_FILE_PATH = os.path.abspath( 54 | os.path.join( 55 | os.path.dirname(__file__), 56 | '.VERSION' 57 | ) 58 | ) 59 | 60 | try: 61 | with open(VERSION_FILE_PATH, encoding='utf8') as f: 62 | __version__ = f.read() 63 | except IOError: 64 | try: 65 | with open(LOCAL_VERSION_FILE_PATH, encoding='utf8') as f: 66 | __version__ = f.read() 67 | except IOError: 68 | __version__ = 'unknown' 69 | -------------------------------------------------------------------------------- /tests/test_wikiseriesasorkunlib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: test_wikiseriesasorkunlib.py 4 | # 5 | # Copyright 2023 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | test_wikiseriesasorkunlib 28 | ---------------------------------- 29 | Tests for `wikiseriesasorkunlib` module. 30 | 31 | .. _Google Python Style Guide: 32 | http://google.github.io/styleguide/pyguide.html 33 | 34 | """ 35 | 36 | from betamax.fixtures import unittest 37 | 38 | __author__ = '''Costas Tyfoxylos ''' 39 | __docformat__ = '''google''' 40 | __date__ = '''26-04-2023''' 41 | __copyright__ = '''Copyright 2023, Costas Tyfoxylos''' 42 | __credits__ = ["Costas Tyfoxylos"] 43 | __license__ = '''MIT''' 44 | __maintainer__ = '''Costas Tyfoxylos''' 45 | __email__ = '''''' 46 | __status__ = '''Development''' # "Prototype", "Development", "Production". 47 | 48 | 49 | class TestWikiseriesasorkunlib(unittest.BetamaxTestCase): 50 | 51 | def setUp(self): 52 | """ 53 | Test set up 54 | 55 | This is where you can setup things that you use throughout the tests. This method is called before every test. 56 | """ 57 | pass 58 | 59 | def tearDown(self): 60 | """ 61 | Test tear down 62 | 63 | This is where you should tear down what you've setup in setUp before. This method is called after every test. 64 | """ 65 | pass 66 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | wikiseriesasorkunlib 3 | ==================== 4 | 5 | This my project 6 | 7 | 8 | * Documentation: https://wikiseriesasorkunlib.readthedocs.org/en/latest 9 | 10 | 11 | Development Workflow 12 | ==================== 13 | 14 | The workflow supports the following steps 15 | 16 | * lint 17 | * test 18 | * build 19 | * document 20 | * upload 21 | * graph 22 | 23 | These actions are supported out of the box by the corresponding scripts under _CI/scripts directory with sane defaults based on best practices. 24 | Sourcing setup_aliases.ps1 for windows powershell or setup_aliases.sh in bash on Mac or Linux will provide with handy aliases for the shell of all those commands prepended with an underscore. 25 | 26 | The bootstrap script creates a .venv directory inside the project directory hosting the virtual environment. It uses pipenv for that. 27 | It is called by all other scripts before they do anything. So one could simple start by calling _lint and that would set up everything before it tried to actually lint the project 28 | 29 | Once the code is ready to be delivered the _tag script should be called accepting one of three arguments, patch, minor, major following the semantic versioning scheme. 30 | So for the initial delivery one would call 31 | 32 | $ _tag --minor 33 | 34 | which would bump the version of the project to 0.1.0 tag it in git and do a push and also ask for the change and automagically update HISTORY.rst with the version and the change provided. 35 | 36 | 37 | So the full workflow after git is initialized is: 38 | 39 | * repeat as necessary (of course it could be test - code - lint :) ) 40 | 41 | * code 42 | * lint 43 | * test 44 | * commit and push 45 | * develop more through the code-lint-test cycle 46 | * tag (with the appropriate argument) 47 | * build 48 | * upload (if you want to host your package in pypi) 49 | * document (of course this could be run at any point) 50 | 51 | 52 | Important Information 53 | ===================== 54 | 55 | This template is based on pipenv. In order to be compatible with requirements.txt so the actual created package can be used by any part of the existing python ecosystem some hacks were needed. 56 | So when building a package out of this **do not** simple call 57 | 58 | $ python setup.py sdist bdist_egg 59 | 60 | **as this will produce an unusable artifact with files missing.** 61 | Instead use the provided build and upload scripts that create all the necessary files in the artifact. 62 | 63 | 64 | 65 | Project Features 66 | ================ 67 | 68 | * TODO 69 | -------------------------------------------------------------------------------- /_CI/scripts/graph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: graph.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import logging 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from pathlib import Path 33 | from bootstrap import bootstrap 34 | from emoji import emojize 35 | from library import execute_command 36 | from configuration import PROJECT_SLUG 37 | 38 | # This is the main prefix used for logging 39 | LOGGER_BASENAME = '''_CI.graph''' 40 | LOGGER = logging.getLogger(LOGGER_BASENAME) 41 | LOGGER.addHandler(logging.NullHandler()) 42 | 43 | 44 | def graph(): 45 | bootstrap() 46 | os.chdir('graphs') 47 | create_graph_command = ('pyreverse ' 48 | '-o png ' 49 | '-A ' 50 | '-f PUB_ONLY ' 51 | f'-p graphs \"{Path("..", PROJECT_SLUG)}\"') 52 | success = execute_command(create_graph_command) 53 | if success: 54 | LOGGER.info('%s Successfully created graph images %s', 55 | emojize(':check_mark_button:'), 56 | emojize(':thumbs_up:')) 57 | else: 58 | LOGGER.error('%s Errors in creation of graph images found! %s', 59 | emojize(':cross_mark:'), 60 | emojize(':crying_face:')) 61 | raise SystemExit(0 if success else 1) 62 | 63 | 64 | if __name__ == '__main__': 65 | graph() 66 | -------------------------------------------------------------------------------- /_CI/scripts/_initialize_template.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | 6 | # This is the main prefix used for logging 7 | LOGGER_BASENAME = '''_CI._initialize_template''' 8 | LOGGER = logging.getLogger(LOGGER_BASENAME) 9 | LOGGER.addHandler(logging.NullHandler()) 10 | 11 | 12 | def add_ci_directory_to_path(): 13 | current_file_path = os.path.dirname(os.path.abspath(__file__)) 14 | ci_path = os.path.abspath(os.path.join(current_file_path, '..')) 15 | if ci_path not in sys.path: 16 | sys.path.append(ci_path) 17 | 18 | 19 | def initialize_template_environment(): 20 | from configuration import (LOGGING_LEVEL, 21 | ENVIRONMENT_VARIABLES, 22 | PREREQUISITES) 23 | from library import (setup_logging, 24 | validate_binary_prerequisites, 25 | validate_environment_variable_prerequisites, 26 | is_venv_created, 27 | execute_command, 28 | load_environment_variables, 29 | load_dot_env_file, 30 | activate_virtual_environment) 31 | load_environment_variables(ENVIRONMENT_VARIABLES) 32 | load_dot_env_file() 33 | if not validate_binary_prerequisites(PREREQUISITES.get('executables', [])): 34 | LOGGER.error('Prerequisite binary missing, cannot continue.') 35 | raise SystemExit(1) 36 | if not validate_environment_variable_prerequisites(PREREQUISITES.get('environment_variables', [])): 37 | LOGGER.error('Prerequisite environment variable missing, cannot continue.') 38 | raise SystemExit(1) 39 | 40 | if not is_venv_created(): 41 | LOGGER.debug('Trying to create virtual environment.') 42 | success = execute_command('pipenv install --dev --ignore-pipfile') 43 | if success: 44 | activate_virtual_environment() 45 | from emoji import emojize 46 | LOGGER.info('%s Successfully created virtual environment and loaded it! %s', 47 | emojize(':check_mark_button:'), 48 | emojize(':thumbs_up:')) 49 | else: 50 | LOGGER.error('Creation of virtual environment failed, cannot continue, ' 51 | 'please clean up .venv directory and try again...') 52 | raise SystemExit(1) 53 | setup_logging(os.environ.get('LOGGING_LEVEL') or LOGGING_LEVEL) 54 | 55 | 56 | def bootstrap_template(): 57 | add_ci_directory_to_path() 58 | from library import activate_template 59 | activate_template() 60 | initialize_template_environment() 61 | 62 | 63 | bootstrap_template() 64 | -------------------------------------------------------------------------------- /_CI/scripts/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: test.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import os 28 | from time import sleep 29 | 30 | # this sets up everything and MUST be included before any third party module in every step 31 | import _initialize_template 32 | 33 | from emoji import emojize 34 | from bootstrap import bootstrap 35 | from library import (open_file, 36 | clean_up, 37 | execute_command, 38 | save_requirements) 39 | 40 | # This is the main prefix used for logging 41 | LOGGER_BASENAME = '''_CI.test''' 42 | LOGGER = logging.getLogger(LOGGER_BASENAME) 43 | LOGGER.addHandler(logging.NullHandler()) 44 | 45 | 46 | def test(): 47 | bootstrap() 48 | clean_up('test-output') 49 | os.mkdir('test-output') 50 | save_requirements() 51 | success = execute_command('tox') 52 | try: 53 | open_file(os.path.join('test-output', 'coverage', 'index.html')) 54 | sleep(0.5) 55 | open_file(os.path.join('test-output', 'nosetests.html')) 56 | except Exception: 57 | LOGGER.warning('Could not execute UI portion. Maybe running headless?') 58 | if success: 59 | LOGGER.info('%s No testing errors found! %s', 60 | emojize(':check_mark_button:'), 61 | emojize(':thumbs_up:')) 62 | else: 63 | LOGGER.error('%s Testing errors found! %s', 64 | emojize(':cross_mark:'), 65 | emojize(':crying_face:')) 66 | raise SystemExit(0 if success else 1) 67 | 68 | 69 | if __name__ == '__main__': 70 | test() 71 | -------------------------------------------------------------------------------- /_CI/scripts/document.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: document.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import logging 28 | import shutil 29 | 30 | # this sets up everything and MUST be included before any third party module in every step 31 | import _initialize_template 32 | 33 | from bootstrap import bootstrap 34 | from emoji import emojize 35 | from library import open_file, clean_up, execute_command 36 | 37 | # This is the main prefix used for logging 38 | LOGGER_BASENAME = '''_CI.document''' 39 | LOGGER = logging.getLogger(LOGGER_BASENAME) 40 | LOGGER.addHandler(logging.NullHandler()) 41 | 42 | 43 | def document(): 44 | bootstrap() 45 | clean_up(('_build', 46 | os.path.join('docs', '_build'), 47 | os.path.join('docs', 'test_docs.rst'), 48 | os.path.join('docs', 'modules.rst'))) 49 | success = execute_command('make -C docs html') 50 | if success: 51 | shutil.move(os.path.join('docs', '_build'), '_build') 52 | try: 53 | open_file(os.path.join('_build', 'html', 'index.html')) 54 | except Exception: 55 | LOGGER.warning('Could not execute UI portion. Maybe running headless?') 56 | LOGGER.info('%s Successfully built documentation %s', 57 | emojize(':check_mark_button:'), 58 | emojize(':thumbs_up:')) 59 | else: 60 | LOGGER.error('%s Documentation creation errors found! %s', 61 | emojize(':cross_mark:'), 62 | emojize(':crying_face:')) 63 | raise SystemExit(0 if success else 1) 64 | 65 | 66 | if __name__ == '__main__': 67 | document() 68 | -------------------------------------------------------------------------------- /_CI/configuration/configuration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: configuration.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | import os 26 | import json 27 | 28 | current_file_path = os.path.dirname(os.path.abspath(__file__)) 29 | files_path = os.path.abspath(os.path.join(current_file_path, '..', 'files')) 30 | 31 | with open(os.path.join(files_path, 'logging_level.json'), 'r') as logging_level_file: 32 | LOGGING_LEVEL = json.loads(logging_level_file.read()).get('level').upper() 33 | logging_level_file.close() 34 | 35 | with open(os.path.join(files_path, 'environment_variables.json'), 'r') as environment_variables_file: 36 | ENVIRONMENT_VARIABLES = json.loads(environment_variables_file.read()) 37 | environment_variables_file.close() 38 | 39 | with open(os.path.join(files_path, 'prerequisites.json'), 'r') as prerequisites_file: 40 | PREREQUISITES = json.loads(prerequisites_file.read()) 41 | prerequisites_file.close() 42 | 43 | BUILD_REQUIRED_FILES = ('.VERSION', 44 | 'LICENSE', 45 | 'AUTHORS.rst', 46 | 'CONTRIBUTING.rst', 47 | 'HISTORY.rst', 48 | 'README.rst', 49 | 'USAGE.rst', 50 | 'Pipfile', 51 | 'Pipfile.lock', 52 | 'requirements.txt', 53 | 'dev-requirements.txt') 54 | 55 | LOGGERS_TO_DISABLE = ['sh.command', 56 | 'sh.command.process', 57 | 'sh.command.process.streamreader', 58 | 'sh.streamreader', 59 | 'sh.stream_bufferer'] 60 | 61 | BRANCHES_SUPPORTED_FOR_TAG = ['main'] 62 | 63 | PROJECT_SLUG = ENVIRONMENT_VARIABLES.get('PROJECT_SLUG') 64 | -------------------------------------------------------------------------------- /_CI/scripts/upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: upload.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import os 28 | 29 | # this sets up everything and MUST be included before any third party module in every step 30 | import _initialize_template 31 | 32 | from emoji import emojize 33 | from build import build 34 | from library import execute_command, validate_environment_variable_prerequisites 35 | from configuration import PREREQUISITES 36 | 37 | # This is the main prefix used for logging 38 | LOGGER_BASENAME = '''_CI.upload''' 39 | LOGGER = logging.getLogger(LOGGER_BASENAME) 40 | LOGGER.addHandler(logging.NullHandler()) 41 | 42 | 43 | def upload(): 44 | success = build() 45 | if not success: 46 | LOGGER.error('Errors caught on building the artifact, bailing out...') 47 | raise SystemExit(1) 48 | if not validate_environment_variable_prerequisites(PREREQUISITES.get('upload_environment_variables', [])): 49 | LOGGER.error('Prerequisite environment variable for upload missing, cannot continue.') 50 | raise SystemExit(1) 51 | upload_command = ('twine upload dist/* ' 52 | f'-u {os.environ.get("PYPI_UPLOAD_USERNAME")} ' 53 | f'-p {os.environ.get("PYPI_UPLOAD_PASSWORD")} ' 54 | '--skip-existing ' 55 | f'--repository-url {os.environ.get("PYPI_URL")}') 56 | LOGGER.info('Trying to upload built artifact...') 57 | success = execute_command(upload_command) 58 | if success: 59 | LOGGER.info('%s Successfully uploaded artifact! %s', 60 | emojize(':check_mark_button:'), 61 | emojize(':thumbs_up:')) 62 | else: 63 | LOGGER.error('%s Errors found in uploading artifact! %s', 64 | emojize(':cross_mark:'), 65 | emojize(':crying_face:')) 66 | raise SystemExit(0 if success else 1) 67 | 68 | 69 | if __name__ == '__main__': 70 | upload() 71 | -------------------------------------------------------------------------------- /_CI/scripts/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: build.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import logging 27 | import os 28 | import shutil 29 | 30 | # this sets up everything and MUST be included before any third party module in every step 31 | import _initialize_template 32 | 33 | from bootstrap import bootstrap 34 | from emoji import emojize 35 | from configuration import BUILD_REQUIRED_FILES, LOGGING_LEVEL, PROJECT_SLUG 36 | from library import execute_command, clean_up, save_requirements 37 | 38 | # This is the main prefix used for logging 39 | LOGGER_BASENAME = '''_CI.build''' 40 | LOGGER = logging.getLogger(LOGGER_BASENAME) 41 | LOGGER.addHandler(logging.NullHandler()) 42 | 43 | 44 | def build(): 45 | bootstrap() 46 | clean_up(('build', 'dist')) 47 | success = execute_command('pipenv lock') 48 | if success: 49 | LOGGER.info('Successfully created lock file %s %s', 50 | emojize(':check_mark_button:'), 51 | emojize(':thumbs_up:')) 52 | else: 53 | LOGGER.error('%s Errors creating lock file! %s', 54 | emojize(':cross_mark:'), 55 | emojize(':crying_face:')) 56 | raise SystemExit(1) 57 | save_requirements() 58 | for file in BUILD_REQUIRED_FILES: 59 | shutil.copy(file, os.path.join(f'{PROJECT_SLUG}', file)) 60 | success = execute_command('python setup.py sdist bdist_egg') 61 | if success: 62 | LOGGER.info('%s Successfully built artifact %s', 63 | emojize(':check_mark_button:'), 64 | emojize(':thumbs_up:')) 65 | else: 66 | LOGGER.error('%s Errors building artifact! %s', 67 | emojize(':cross_mark:'), 68 | emojize(':crying_face:')) 69 | clean_up([os.path.join(f'{PROJECT_SLUG}', file) 70 | for file in BUILD_REQUIRED_FILES]) 71 | return True if success else False 72 | 73 | 74 | if __name__ == '__main__': 75 | raise SystemExit(0 if build() else 1) 76 | -------------------------------------------------------------------------------- /wikiseriesasorkunlib/wikiseriesasorkunlib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: wikiseriesasorkunlib.py 4 | # 5 | # Copyright 2023 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | """ 27 | Main code for wikiseriesasorkunlib. 28 | 29 | .. _Google Python Style Guide: 30 | https://google.github.io/styleguide/pyguide.html 31 | 32 | """ 33 | 34 | import logging 35 | import requests 36 | from bs4 import BeautifulSoup as Bfs 37 | 38 | __author__ = '''Costas Tyfoxylos ''' 39 | __docformat__ = '''google''' 40 | __date__ = '''26-04-2023''' 41 | __copyright__ = '''Copyright 2023, Costas Tyfoxylos''' 42 | __credits__ = ["Costas Tyfoxylos"] 43 | __license__ = '''MIT''' 44 | __maintainer__ = '''Costas Tyfoxylos''' 45 | __email__ = '''''' 46 | __status__ = '''Development''' # "Prototype", "Development", "Production". 47 | 48 | 49 | # This is the main prefix used for logging 50 | LOGGER_BASENAME = '''wikiseriesasorkunlib''' 51 | LOGGER = logging.getLogger(LOGGER_BASENAME) 52 | LOGGER.addHandler(logging.NullHandler()) 53 | 54 | 55 | def search_series(name): 56 | api_url = 'https://en.wikipedia.org/w/api.php' 57 | limit = 10 58 | term = f'List_of_{name}_episodes' 59 | parameters = {'action': 'opensearch', 60 | 'format': 'json', 61 | 'formatversion': '1', 62 | 'namespace': '0', 63 | 'limit': limit, 64 | 'search': term} 65 | search_response = requests.get(api_url, params=parameters, timeout=5) 66 | series_url = search_response.json()[3][0] 67 | series_response = requests.get(series_url, timeout=5) 68 | soup = Bfs(series_response.text, features="html.parser") 69 | season_table = soup.find('table', class_='wikitable') 70 | seasons_numbers = [item.text for item in season_table.find_all('span', class_='nowrap')] 71 | season_episodes = soup.find_all('table', class_='wikiepisodetable') 72 | return {f'Season {key}': [entry.text.split('"')[1] 73 | for entry in value.find_all('td', class_='summary')] 74 | for key, value in dict(zip(seasons_numbers, season_episodes)).items()} 75 | -------------------------------------------------------------------------------- /_CI/scripts/update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: update.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import os 27 | import sys 28 | import tempfile 29 | from glob import glob 30 | from dataclasses import dataclass 31 | 32 | # this sets up everything and MUST be included before any third party module in every step 33 | import _initialize_template 34 | 35 | from library import clean_up 36 | from patch import fromfile, setdebug 37 | 38 | 39 | @dataclass() 40 | class Project: 41 | name: str 42 | full_path: str 43 | parent_directory_full_path: str 44 | 45 | 46 | class PatchFailure(Exception): 47 | """The patch process failed""" 48 | 49 | 50 | def get_current_version(): 51 | with open(os.path.join('_CI', '.VERSION'), 'r') as version_file: 52 | version = version_file.read().strip() 53 | version_file.close() 54 | print(f'Got current template version {version}') 55 | return version 56 | 57 | 58 | def apply_patch(file_path, project_parent_path): 59 | patcher = fromfile(file_path) 60 | return patcher.apply(0, project_parent_path) 61 | 62 | 63 | def get_patches_to_apply(current_version): 64 | patches = [] 65 | for patch_file in glob(os.path.join('_CI', 'patches', '*.patch')): 66 | version = patch_file.rpartition(os.path.sep)[2].split('.patch')[0] 67 | if version > current_version: 68 | patches.append(patch_file) 69 | return sorted(patches) 70 | 71 | 72 | def get_interpolated_temp_patch_file(patch_file, project_name): 73 | patch_diff = open(patch_file, 'r').read() 74 | temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False) 75 | temp_file.write(patch_diff.replace('{{cookiecutter.project_slug}}', project_name)) 76 | temp_file.close() 77 | return temp_file.name 78 | 79 | 80 | def initialize(): 81 | current_file_path = os.path.dirname(os.path.abspath(__file__)) 82 | project_root = os.path.abspath(os.path.join(current_file_path, '..', '..')) 83 | project_parent_path, _, parent_directory_name = project_root.rpartition(os.path.sep) 84 | os.chdir(project_root) 85 | sys.path.append(os.path.join(project_root, '_CI/library')) 86 | setdebug() 87 | return Project(parent_directory_name, project_root, project_parent_path) 88 | 89 | 90 | def apply_patches(patches, project): 91 | for diff_patch in patches: 92 | print(f'Interpolating project name "{project.name}" in patch {diff_patch}') 93 | patch_file = get_interpolated_temp_patch_file(diff_patch, project.name) 94 | success = apply_patch(patch_file, project.parent_directory_full_path) 95 | print(f'Removing temporary file "{patch_file}"') 96 | clean_up(patch_file) 97 | if success: 98 | print(f'Successfully applied patch {diff_patch}') 99 | else: 100 | print(f'Failed applying patch {diff_patch}') 101 | raise PatchFailure(diff_patch) 102 | 103 | 104 | if __name__ == '__main__': 105 | project = initialize() 106 | current_version = get_current_version() 107 | patches_to_apply = get_patches_to_apply(current_version) 108 | try: 109 | apply_patches(patches_to_apply, project) 110 | except PatchFailure: 111 | SystemExit(1) 112 | raise SystemExit(0) 113 | -------------------------------------------------------------------------------- /_CI/scripts/tag.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: tag.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | 26 | import argparse 27 | import logging 28 | 29 | from datetime import datetime 30 | 31 | # this sets up everything and MUST be included before any third party module in every step 32 | import _initialize_template 33 | 34 | from emoji import emojize 35 | from bootstrap import bootstrap 36 | from gitwrapperlib import Git 37 | from library import bump 38 | from configuration import BRANCHES_SUPPORTED_FOR_TAG 39 | 40 | 41 | # This is the main prefix used for logging 42 | LOGGER_BASENAME = '''_CI.tag''' 43 | LOGGER = logging.getLogger(LOGGER_BASENAME) 44 | LOGGER.addHandler(logging.NullHandler()) 45 | 46 | 47 | def check_branch(): 48 | git = Git() 49 | if git.get_current_branch() not in BRANCHES_SUPPORTED_FOR_TAG: 50 | accepted_branches = ', '.join(BRANCHES_SUPPORTED_FOR_TAG) 51 | print("Tagging is only supported on {} " 52 | "you should not tag any other branch, exiting!".format(accepted_branches)) 53 | raise SystemExit(1) 54 | 55 | 56 | def push(current_version): 57 | git = Git() 58 | git.commit('Updated history file with changelog', 'HISTORY.rst') 59 | git.commit(f'Set version to {current_version}', '.VERSION') 60 | git.add_tag(current_version) 61 | git.push() 62 | git.push('origin', current_version) 63 | return current_version 64 | 65 | 66 | def _get_user_input(version): 67 | print(f'Enter/Paste your history changelog for version {version}.\n' 68 | f'Each comment can be a different line.\n\n' 69 | f'Ctrl-D ( Mac | Linux ) or Ctrl-Z ( windows ) to save it.\n') 70 | contents = [] 71 | while True: 72 | try: 73 | line = input() 74 | except EOFError: 75 | break 76 | contents.append(line) 77 | return contents 78 | 79 | 80 | def _get_changelog(contents, version): 81 | header = f'{version} ({datetime.today().strftime("%d-%m-%Y")})' 82 | underline = '-' * len(header) 83 | return (f'\n\n{header}\n' 84 | f'{underline}\n\n* ' + '\n* '.join([line for line in contents if line]) + '\n') 85 | 86 | 87 | def update_history_file(version): 88 | comments = _get_user_input(version) 89 | update_text = _get_changelog(comments, version) 90 | with open('HISTORY.rst', 'a') as history_file: 91 | history_file.write(update_text) 92 | history_file.close() 93 | 94 | 95 | def get_arguments(): 96 | parser = argparse.ArgumentParser(description='Handles bumping of the artifact version') 97 | group = parser.add_mutually_exclusive_group() 98 | group.add_argument('--major', help='Bump the major version', action='store_true') 99 | group.add_argument('--minor', help='Bump the minor version', action='store_true') 100 | group.add_argument('--patch', help='Bump the patch version', action='store_true') 101 | args = parser.parse_args() 102 | return args 103 | 104 | 105 | def tag(): 106 | bootstrap() 107 | args = get_arguments() 108 | check_branch() 109 | if args.major: 110 | version = bump('major') 111 | update_history_file(version) 112 | elif args.minor: 113 | version = bump('minor') 114 | update_history_file(version) 115 | elif args.patch: 116 | version = bump('patch') 117 | update_history_file(version) 118 | else: 119 | version = bump() 120 | print(version) 121 | raise SystemExit(0) 122 | version = push(version) 123 | print(version) 124 | 125 | 126 | if __name__ == '__main__': 127 | tag() 128 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/wikiseriesasorkunlib.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/wikiseriesasorkunlib.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/wikiseriesasorkunlib" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/wikiseriesasorkunlib" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # wikiseriesasorkunlib documentation build configuration file, created by 5 | # sphinx-quickstart 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import sphinx_rtd_theme 19 | 20 | # If extensions (or modules to document with autodoc) are in another 21 | # directory, add these directories to sys.path here. If the directory is 22 | # relative to the documentation root, use os.path.abspath to make it 23 | # absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # Get the project root dir, which is the parent dir of this 27 | cwd = os.getcwd() 28 | project_root = os.path.dirname(cwd) 29 | 30 | # Run apidoc to traverse the project directory and add all modules to the docs 31 | import sphinx.ext.apidoc 32 | 33 | sphinx.ext.apidoc.main(argv=['-f', '-o', os.path.join(project_root, 'docs'), 34 | os.path.join(project_root, '''wikiseriesasorkunlib''')]) 35 | 36 | # Insert the project root dir as the first element in the PYTHONPATH. 37 | # This lets us ensure that the source package is imported, and that its 38 | # version is used. 39 | sys.path.insert(0, project_root) 40 | 41 | import wikiseriesasorkunlib 42 | 43 | # -- General configuration --------------------------------------------- 44 | 45 | # If your documentation needs a minimal Sphinx version, state it here. 46 | #needs_sphinx = '1.0' 47 | 48 | # Add any Sphinx extension module names here, as strings. They can be 49 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 50 | extensions = [ 51 | 'sphinx.ext.autodoc', 52 | 'sphinx.ext.viewcode', 53 | 'sphinx.ext.napoleon', 54 | 'sphinx.ext.todo' 55 | ] 56 | 57 | napoleon_google_docstring = True 58 | 59 | # Add any paths that contain templates here, relative to this directory. 60 | templates_path = ['_templates'] 61 | 62 | # The suffix of source filenames. 63 | source_suffix = '.rst' 64 | 65 | # The encoding of source files. 66 | #source_encoding = 'utf-8-sig' 67 | 68 | # The master toctree document. 69 | master_doc = 'index' 70 | 71 | # General information about the project. 72 | project = u'''wikiseriesasorkunlib''' 73 | copyright = u'''2023, (Author : Costas Tyfoxylos)''' 74 | 75 | # The version info for the project you're documenting, acts as replacement 76 | # for |version| and |release|, also used in various other places throughout 77 | # the built documents. 78 | # 79 | # The short X.Y version. 80 | version = wikiseriesasorkunlib.__version__ 81 | # The full version, including alpha/beta/rc tags. 82 | release = wikiseriesasorkunlib.__version__ 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | #language = None 87 | 88 | # There are two options for replacing |today|: either, you set today to 89 | # some non-false value, then it is used: 90 | #today = '' 91 | # Else, today_fmt is used as the format for a strftime call. 92 | #today_fmt = '%B %d, %Y' 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | exclude_patterns = ['_build'] 97 | 98 | # The reST default role (used for this markup: `text`) to use for all 99 | # documents. 100 | #default_role = None 101 | 102 | # If true, '()' will be appended to :func: etc. cross-reference text. 103 | #add_function_parentheses = True 104 | 105 | # If true, the current module name will be prepended to all description 106 | # unit titles (such as .. function::). 107 | #add_module_names = True 108 | 109 | # If true, sectionauthor and moduleauthor directives will be shown in the 110 | # output. They are ignored by default. 111 | #show_authors = False 112 | 113 | # The name of the Pygments (syntax highlighting) style to use. 114 | pygments_style = 'sphinx' 115 | 116 | # A list of ignored prefixes for module index sorting. 117 | #modindex_common_prefix = [] 118 | 119 | # If true, keep warnings as "system message" paragraphs in the built 120 | # documents. 121 | #keep_warnings = False 122 | 123 | 124 | # -- Options for HTML output ------------------------------------------- 125 | 126 | # The theme to use for HTML and HTML Help pages. See the documentation for 127 | # a list of builtin themes. 128 | html_theme = 'sphinx_rtd_theme' 129 | 130 | # Theme options are theme-specific and customize the look and feel of a 131 | # theme further. For a list of options available for each theme, see the 132 | # documentation. 133 | #html_theme_options = {} 134 | 135 | # Add any paths that contain custom themes here, relative to this directory. 136 | #html_theme_path = [] 137 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 138 | 139 | # The name for this set of Sphinx documents. If None, it defaults to 140 | # " v documentation". 141 | #html_title = None 142 | 143 | # A shorter title for the navigation bar. Default is the same as 144 | # html_title. 145 | #html_short_title = None 146 | 147 | # The name of an image file (relative to this directory) to place at the 148 | # top of the sidebar. 149 | #html_logo = None 150 | 151 | # The name of an image file (within the static path) to use as favicon 152 | # of the docs. This file should be a Windows icon file (.ico) being 153 | # 16x16 or 32x32 pixels large. 154 | #html_favicon = None 155 | 156 | # Add any paths that contain custom static files (such as style sheets) 157 | # here, relative to this directory. They are copied after the builtin 158 | # static files, so a file named "default.css" will overwrite the builtin 159 | # "default.css". 160 | #html_static_path = ['_static'] 161 | 162 | # If not '', a 'Last updated on:' timestamp is inserted at every page 163 | # bottom, using the given strftime format. 164 | #html_last_updated_fmt = '%b %d, %Y' 165 | 166 | # If true, SmartyPants will be used to convert quotes and dashes to 167 | # typographically correct entities. 168 | #html_use_smartypants = True 169 | 170 | # Custom sidebar templates, maps document names to template names. 171 | #html_sidebars = {} 172 | 173 | # Additional templates that should be rendered to pages, maps page names 174 | # to template names. 175 | #html_additional_pages = {} 176 | 177 | # If false, no module index is generated. 178 | #html_domain_indices = True 179 | 180 | # If false, no index is generated. 181 | #html_use_index = True 182 | 183 | # If true, the index is split into individual pages for each letter. 184 | #html_split_index = False 185 | 186 | # If true, links to the reST sources are added to the pages. 187 | #html_show_sourcelink = True 188 | 189 | # If true, "Created using Sphinx" is shown in the HTML footer. 190 | # Default is True. 191 | #html_show_sphinx = True 192 | 193 | # If true, "(C) Copyright ..." is shown in the HTML footer. 194 | # Default is True. 195 | #html_show_copyright = True 196 | 197 | # If true, an OpenSearch description file will be output, and all pages 198 | # will contain a tag referring to it. The value of this option 199 | # must be the base URL from which the finished HTML is served. 200 | #html_use_opensearch = '' 201 | 202 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 203 | #html_file_suffix = None 204 | 205 | # Output file base name for HTML help builder. 206 | htmlhelp_basename = '''wikiseriesasorkunlibdoc''' 207 | 208 | 209 | # -- Options for LaTeX output ------------------------------------------ 210 | 211 | latex_elements = { 212 | # The paper size ('letterpaper' or 'a4paper'). 213 | #'papersize': 'letterpaper', 214 | 215 | # The font size ('10pt', '11pt' or '12pt'). 216 | #'pointsize': '10pt', 217 | 218 | # Additional stuff for the LaTeX preamble. 219 | #'preamble': '', 220 | } 221 | 222 | # Grouping the document tree into LaTeX files. List of tuples 223 | # (source start file, target name, title, author, documentclass 224 | # [howto/manual]). 225 | latex_documents = [ 226 | ('index', '''wikiseriesasorkunlib.tex''', 227 | u'''wikiseriesasorkunlib Documentation''', 228 | u'''Costas Tyfoxylos''', 'manual'), 229 | ] 230 | 231 | # The name of an image file (relative to this directory) to place at 232 | # the top of the title page. 233 | #latex_logo = None 234 | 235 | # For "manual" documents, if this is true, then toplevel headings 236 | # are parts, not chapters. 237 | #latex_use_parts = False 238 | 239 | # If true, show page references after internal links. 240 | #latex_show_pagerefs = False 241 | 242 | # If true, show URL addresses after external links. 243 | #latex_show_urls = False 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #latex_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #latex_domain_indices = True 250 | 251 | 252 | # -- Options for manual page output ------------------------------------ 253 | 254 | # One entry per manual page. List of tuples 255 | # (source start file, name, description, authors, manual section). 256 | man_pages = [ 257 | ('index', '''wikiseriesasorkunlib''', 258 | u'''wikiseriesasorkunlib Documentation''', 259 | [u'''Costas Tyfoxylos'''], 1) 260 | ] 261 | 262 | # If true, show URL addresses after external links. 263 | #man_show_urls = False 264 | 265 | 266 | # -- Options for Texinfo output ---------------------------------------- 267 | 268 | # Grouping the document tree into Texinfo files. List of tuples 269 | # (source start file, target name, title, author, 270 | # dir menu entry, description, category) 271 | texinfo_documents = [ 272 | ('index', '''wikiseriesasorkunlib''', 273 | u'''wikiseriesasorkunlib Documentation''', 274 | u'''Costas Tyfoxylos''', 275 | '''wikiseriesasorkunlib''', 276 | 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | -------------------------------------------------------------------------------- /_CI/library/core_library.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # File: core_library.py 4 | # 5 | # Copyright 2018 Costas Tyfoxylos 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to 9 | # deal in the Software without restriction, including without limitation the 10 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | # sell copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | # DEALINGS IN THE SOFTWARE. 24 | # 25 | import json 26 | import logging 27 | import os 28 | import shlex 29 | import shutil 30 | import sys 31 | import stat 32 | import tempfile 33 | import warnings 34 | from contextlib import contextmanager 35 | from dataclasses import field 36 | from subprocess import Popen, PIPE, check_output, CalledProcessError 37 | 38 | from pipenv.project import Project 39 | from configuration import LOGGERS_TO_DISABLE, ENVIRONMENT_VARIABLES, LOGGING_LEVEL 40 | 41 | # Provides possible python2.7 compatibility, not really a goal 42 | try: 43 | FileNotFoundError 44 | except NameError: 45 | FileNotFoundError = IOError 46 | 47 | # This is the main prefix used for logging 48 | LOGGER_BASENAME = '''_CI.library''' 49 | LOGGER = logging.getLogger(LOGGER_BASENAME) 50 | LOGGER.addHandler(logging.NullHandler()) 51 | 52 | 53 | class Package: 54 | def __init__(self, 55 | name: str, 56 | version: str, 57 | index: str = '', 58 | markers: str = '', 59 | hashes: list = field(default=list)) -> None: 60 | self.name = name 61 | self.index = index 62 | self.markers = markers 63 | self.hashes = hashes 64 | self.comparator, self.version = self._decompose_full_version(version) 65 | 66 | @staticmethod 67 | def _decompose_full_version(full_version: str) -> (str, str): 68 | comparator = '' 69 | version = '*' 70 | if full_version == '*': 71 | return comparator, version 72 | # We need to check for the most common pinning cases 73 | # >, <, <=, >=, ~=, == 74 | # So we can know where the pin starts and where it ends, 75 | # iteration should start from 2 character then backwards 76 | operators = ['<=', '>=', '~=', '==', '<', '>'] 77 | for operator in operators: 78 | if full_version.startswith(operator): 79 | break 80 | else: 81 | raise ValueError(f'Could not find where the comparator pin ends in {full_version}') 82 | version = full_version[len(operator):] 83 | return operator, version 84 | 85 | @property 86 | def full_version(self): 87 | return f'{self.comparator}{self.version}' 88 | 89 | @full_version.setter 90 | def full_version(self, full_version): 91 | self.comparator, self.version = self._decompose_full_version(full_version) 92 | 93 | def compare_versions(self, pipfile_full_version, pipfile_lock_full_version): 94 | """Processes the two versions both in Pipfile and Pipfile.lock 95 | 96 | Matches the pinning from the Pipfile and the exact version from the Pipfile.lock 97 | 98 | Args: 99 | pipfile_full_version (str): The string of the full version specified in the Pipfile 100 | pipfile_lock_full_version (str): The string of the full version specified in the Pipfile.lock file 101 | 102 | Returns: 103 | 104 | """ 105 | pipfile_comparator, pipfile_version = self._decompose_full_version(pipfile_full_version) 106 | pipfile_lock_comparator, pipfile_lock_version = self._decompose_full_version(pipfile_lock_full_version) 107 | self.comparator = pipfile_comparator if pipfile_comparator else '~=' 108 | self.version = pipfile_lock_version 109 | 110 | 111 | REQUIREMENTS_HEADER = """# 112 | # Please do not manually update this file since the requirements are managed 113 | # by pipenv through Pipfile and Pipfile.lock . 114 | # 115 | # This file is created and managed automatically by the template and it is left 116 | # here only for backwards compatibility reasons with python's ecosystem. 117 | # 118 | # Please use Pipfile to update the requirements. 119 | # 120 | """ 121 | 122 | 123 | def activate_template(): 124 | logging_level = os.environ.get('LOGGING_LEVEL', '').upper() or LOGGING_LEVEL 125 | if logging_level == 'DEBUG': 126 | print(f'Current executing python version is {sys.version_info}') 127 | # needed to support alternate .venv path if PIPENV_PIPFILE is set 128 | # Loading PIPENV related variables early, but not overriding if already loaded. 129 | for name, value in ENVIRONMENT_VARIABLES.items(): 130 | if name.startswith('PIPENV_'): 131 | if not os.environ.get(name): 132 | if logging_level == 'DEBUG': 133 | print(f'Loading PIPENV related variable {name} : {value}') 134 | os.environ[name] = value 135 | else: 136 | if logging_level == 'DEBUG': 137 | print(f'Variable {name} already loaded, not overwriting...') 138 | 139 | # After this everything is executed inside a virtual environment 140 | if not is_venv_active(): 141 | activate_virtual_environment() 142 | try: 143 | import coloredlogs 144 | colored_logs = True 145 | except ImportError: 146 | colored_logs = False 147 | 148 | 149 | # The sequence here is important because it makes sure 150 | # that the virtual environment is loaded as soon as possible 151 | def is_venv_created(): 152 | warnings.simplefilter('ignore', ResourceWarning) 153 | dev_null = open(os.devnull, 'w') 154 | venv = Popen(['pipenv', '--venv'], stdout=PIPE, stderr=dev_null).stdout.read().strip() 155 | return True if venv else False 156 | 157 | 158 | def is_venv_active(): 159 | return hasattr(sys, 'real_prefix') 160 | 161 | 162 | def get_project_root_path(): 163 | current_file_path = os.path.dirname(os.path.abspath(__file__)) 164 | return os.path.abspath(os.path.join(current_file_path, '..', '..')) 165 | 166 | 167 | def get_venv_parent_path(): 168 | alternate_pipefile_location = os.environ.get('PIPENV_PIPFILE', None) 169 | if alternate_pipefile_location: 170 | venv_parent = os.path.abspath(os.path.dirname(alternate_pipefile_location)) 171 | else: 172 | venv_parent = os.path.abspath('.') 173 | return venv_parent 174 | 175 | 176 | def activate_virtual_environment(): 177 | os.chdir(get_project_root_path()) 178 | activation_script_directory = 'Scripts' if sys.platform == 'win32' else 'bin' 179 | venv_parent = get_venv_parent_path() 180 | activation_file = os.path.join(venv_parent, '.venv', activation_script_directory, 'activate_this.py') 181 | if is_venv_created(): 182 | if sys.version_info[0] == 3: 183 | with open(activation_file) as f: 184 | exec(f.read(), {'__file__': activation_file}) 185 | elif sys.version_info[0] == 2: 186 | execfile(activation_file, dict(__file__=activation_file)) 187 | 188 | 189 | def setup_logging(level): 190 | try: 191 | import coloredlogs 192 | coloredlogs.install(level=level.upper()) 193 | except ImportError: 194 | LOGGER = logging.getLogger() 195 | handler = logging.StreamHandler() 196 | handler.setLevel(level.upper()) 197 | formatter = logging.Formatter(('%(asctime)s - ' 198 | '%(name)s - ' 199 | '%(levelname)s - ' 200 | '%(message)s')) 201 | handler.setFormatter(formatter) 202 | LOGGER.addHandler(handler) 203 | LOGGER.setLevel(level.upper()) 204 | for logger in LOGGERS_TO_DISABLE: 205 | logging.getLogger(logger).disabled = True 206 | 207 | 208 | # TODO extend debug logging in the following methods 209 | 210 | def load_environment_variables(environment_variables): 211 | LOGGER.debug('Loading environment variables') 212 | for name, value in environment_variables.items(): 213 | if name in os.environ.keys(): 214 | LOGGER.debug('Environment variable "%s" already loaded, not overriding', name) 215 | else: 216 | LOGGER.debug('Loading environment variable "%s" with value "%s"', name, value) 217 | os.environ[name] = value 218 | 219 | 220 | def load_dot_env_file(): 221 | if os.path.isfile('.env'): 222 | LOGGER.info('Loading environment variables from .env file') 223 | variables = {} 224 | for line in open('.env', 'r').read().splitlines(): 225 | if line.strip().startswith('export '): 226 | line = line.replace('export ', '') 227 | try: 228 | key, value = line.strip().split('=', 1) 229 | except ValueError: 230 | LOGGER.error('Invalid .env file entry, please check line %s', line) 231 | raise SystemExit(1) 232 | variables[key.strip()] = value.strip() 233 | load_environment_variables(variables) 234 | 235 | 236 | def get_binary_path(executable, logging_level='INFO'): 237 | """Gets the software name and returns the path of the binary.""" 238 | if sys.platform == 'win32': 239 | if executable == 'start': 240 | return executable 241 | executable = executable + '.exe' 242 | if executable in os.listdir('.'): 243 | binary = os.path.join(os.getcwd(), executable) 244 | else: 245 | binary = next((os.path.join(path, executable) 246 | for path in os.environ['PATH'].split(os.pathsep) 247 | if os.path.isfile(os.path.join(path, executable))), None) 248 | else: 249 | venv_parent = get_venv_parent_path() 250 | venv_bin_path = os.path.join(venv_parent, '.venv', 'bin') 251 | if not venv_bin_path in os.environ.get('PATH'): 252 | if logging_level == 'DEBUG': 253 | print(f'Adding path {venv_bin_path} to environment PATH variable') 254 | os.environ['PATH'] = os.pathsep.join([os.environ['PATH'], venv_bin_path]) 255 | binary = shutil.which(executable) 256 | return binary if binary else None 257 | 258 | 259 | def validate_binary_prerequisites(software_list): 260 | LOGGER.debug('Trying to validate binary prerequisites') 261 | success = True 262 | for executable in software_list: 263 | if not get_binary_path(executable): 264 | success = False 265 | LOGGER.error('Executable "%s" not found', executable) 266 | else: 267 | LOGGER.debug('Executable "%s" found in the path!', executable) 268 | return success 269 | 270 | 271 | def validate_environment_variable_prerequisites(variable_list): 272 | LOGGER.debug('Trying to validate prerequisites') 273 | success = True 274 | for variable in variable_list: 275 | if not os.environ.get(variable): 276 | success = False 277 | LOGGER.error('Environment variable "%s" not found', variable) 278 | else: 279 | LOGGER.debug('Environment variable "%s" found in the path!', variable) 280 | return success 281 | 282 | 283 | def interpolate_executable(command): 284 | command_list = command.split() 285 | if len(command_list) == 1: 286 | command_list = [command_list[0], ] 287 | try: 288 | LOGGER.debug(f'Getting executable path for {command_list[0]}') 289 | command_list[0] = get_binary_path(command_list[0]) 290 | command = ' '.join(command_list) 291 | except IndexError: 292 | pass 293 | return command 294 | 295 | 296 | def execute_command(command, filter_method=None): 297 | LOGGER.debug('Executing command "%s"', command) 298 | command = interpolate_executable(command) 299 | if filter_method: 300 | if not callable(filter_method): 301 | raise ValueError('Argument is not a valid callable method') 302 | try: 303 | if sys.platform != 'win32': 304 | command = shlex.split(command) 305 | LOGGER.debug('running command %s', command) 306 | command_output = Popen(command,stdout=PIPE) 307 | while command_output.poll() is None: 308 | filter_method(command_output.stdout.readline().rstrip().decode('utf-8')) 309 | success = True if command_output.returncode == 0 else False 310 | except CalledProcessError as command_output: 311 | LOGGER.error('Error running command %s', command) 312 | filter_method(command_output.stderr.decode('utf-8')) 313 | success = False 314 | return success 315 | else: 316 | if sys.platform == 'win32': 317 | process = Popen(command, shell=True, bufsize=1) 318 | else: 319 | command = shlex.split(command) 320 | LOGGER.debug('Command split to %s for posix shell', command) 321 | LOGGER.debug('Command Output is not being filtered') 322 | process = Popen(command, bufsize=1) 323 | process.communicate() 324 | return True if not process.returncode else False 325 | 326 | 327 | def execute_command_with_returned_output(command, filter_method=None): 328 | LOGGER.debug('Executing command "%s"', command) 329 | command = interpolate_executable(command) 330 | stdout = '' 331 | stderr = '' 332 | if filter_method: 333 | if not callable(filter_method): 334 | raise ValueError('Argument is not a valid callable method') 335 | try: 336 | if sys.platform != 'win32': 337 | command = shlex.split(command) 338 | LOGGER.debug('running command %s', command) 339 | command_execution = check_output(command) 340 | stdout = filter_method(command_execution.decode('utf-8')) 341 | except CalledProcessError as command_execution: 342 | LOGGER.error('Error running command %s', command) 343 | stderr = filter_method(command_execution.stderr.decode('utf-8')) 344 | success = True if not command_execution.returncode else False 345 | else: 346 | if sys.platform == 'win32': 347 | process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, bufsize=1) 348 | else: 349 | command = shlex.split(command) 350 | LOGGER.debug('Command split to %s for posix shell', command) 351 | LOGGER.debug('Command Output is not being filtered') 352 | process = Popen(command, stdout=PIPE, stderr=PIPE, bufsize=1) 353 | stdout, stderr = process.communicate() 354 | success = True if not process.returncode else False 355 | return success, stdout.decode('utf-8'), stderr.decode('utf-8') 356 | 357 | 358 | def open_file(path): 359 | open_programs = {'darwin': 'open', 360 | 'linux': 'xdg-open', 361 | 'win32': 'start'} 362 | executable = get_binary_path(open_programs.get(sys.platform)) 363 | command = f'{executable} {path}' 364 | return execute_command(command) 365 | 366 | 367 | def on_error(func, path, exc_info): # pylint: disable=unused-argument 368 | """ 369 | Error handler for ``shutil.rmtree``. 370 | 371 | If the error is due to an access error (read only file) 372 | it attempts to add write permission and then retries. 373 | 374 | If the error is for another reason it re-raises the error. 375 | 376 | Usage : ``shutil.rmtree(path, onerror=onerror)`` 377 | 378 | # 2007/11/08 379 | # Version 0.2.6 380 | # pathutils.py 381 | # Functions useful for working with files and paths. 382 | # http://www.voidspace.org.uk/python/recipebook.shtml#utils 383 | 384 | # Copyright Michael Foord 2004 385 | # Released subject to the BSD License 386 | # Please see http://www.voidspace.org.uk/python/license.shtml 387 | 388 | # For information about bugfixes, updates and support, please join the Pythonutils mailing list. 389 | # http://groups.google.com/group/pythonutils/ 390 | # Comments, suggestions and bug reports welcome. 391 | # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml 392 | # E-mail fuzzyman@voidspace.org.uk 393 | """ 394 | if not os.access(path, os.W_OK): 395 | # Is the error an access error ? 396 | os.chmod(path, stat.S_IWUSR) 397 | func(path) 398 | else: 399 | raise # pylint: disable=misplaced-bare-raise 400 | 401 | 402 | def clean_up(items, on_error=on_error): 403 | if not isinstance(items, (list, tuple)): 404 | items = [items] 405 | success = True 406 | for item in items: 407 | if os.path.isdir(item): 408 | LOGGER.debug('Trying to remove directory "%s"', item) 409 | shutil.rmtree(item, onerror=on_error) 410 | elif os.path.isfile(item): 411 | LOGGER.debug('Trying to remove file "%s"', item) 412 | os.unlink(item) 413 | else: 414 | success = False 415 | LOGGER.warning('Unable to remove file or directory "%s"', item) 416 | return success 417 | 418 | 419 | def get_top_level_dependencies(): 420 | pip_packages = Project().parsed_pipfile.get('packages', {}).items() 421 | packages = [Package(name_, version_) if isinstance(version_, str) else Package(name_, **version_) 422 | for name_, version_ in pip_packages] 423 | pip_dev_packages = Project().parsed_pipfile.get('dev-packages', {}).items() 424 | dev_packages =[Package(name_, version_) if isinstance(version_, str) else Package(name_, **version_) 425 | for name_, version_ in pip_dev_packages] 426 | LOGGER.debug(f'Packages in Pipfile: {packages}') 427 | LOGGER.debug(f'Development packages in Pipfile: {dev_packages}') 428 | return packages, dev_packages 429 | 430 | 431 | def get_all_packages(): 432 | try: 433 | venv_parent = get_venv_parent_path() 434 | lock_file = os.path.join(venv_parent, 'Pipfile.lock') 435 | with open(lock_file, 'r') as lock: 436 | all_packages = json.loads(lock.read()) 437 | except FileNotFoundError: 438 | LOGGER.error('Could not open Pipfile.lock, so cannot get dependencies, exiting...') 439 | raise SystemExit(1) 440 | packages = [Package(package_name, 441 | data.get('version'), 442 | data.get('index'), 443 | data.get('markers'), 444 | data.get('hashes', [])) 445 | for package_name, data in all_packages.get('default').items()] 446 | dev_packages = [Package(package_name, 447 | data.get('version'), 448 | data.get('index'), 449 | data.get('markers'), 450 | data.get('hashes', [])) 451 | for package_name, data in all_packages.get('develop').items()] 452 | return packages, dev_packages 453 | 454 | 455 | def format_marker(marker): 456 | return f' ; {marker}' if marker else '' 457 | 458 | 459 | def _get_packages(top_level_packages, packages): 460 | pkg = [] 461 | for top_level_package in top_level_packages: 462 | package = next((item for item in packages if item.name == top_level_package.name), None) 463 | if not package: 464 | raise ValueError(f'Package name "{top_level_package.name}" not found in Pipfile.lock') 465 | package.compare_versions(top_level_package.full_version, package.full_version) 466 | pkg.append(package) 467 | return pkg 468 | 469 | 470 | def save_requirements(): 471 | top_level_packages, top_level_dev_packages = get_top_level_dependencies() 472 | all_packages, all_dev_packages = get_all_packages() 473 | venv_parent = get_venv_parent_path() 474 | requirements_file = os.path.join(venv_parent, 'requirements.txt') 475 | with open(requirements_file, 'w') as f: 476 | requirements = '\n'.join([f'{package.name}{package.full_version}{format_marker(package.markers)}' 477 | for package in _get_packages(top_level_packages, all_packages)]) 478 | 479 | f.write(REQUIREMENTS_HEADER + requirements) 480 | dev_requirements_file = os.path.join(venv_parent, 'dev-requirements.txt') 481 | with open(dev_requirements_file, 'w') as f: 482 | dev_requirements = '\n'.join( 483 | [f'{package.name}{package.full_version}{format_marker(package.markers)}' 484 | for package in _get_packages(top_level_dev_packages, all_dev_packages)]) 485 | 486 | f.write(REQUIREMENTS_HEADER + dev_requirements) 487 | 488 | 489 | def get_version_file_path(): 490 | return os.path.abspath(os.path.join(os.path.dirname(__file__), 491 | '..', 492 | '..', 493 | '.VERSION')) 494 | 495 | 496 | def bump(segment=None, version_file=None): 497 | import semver 498 | if not version_file: 499 | version_file = get_version_file_path() 500 | try: 501 | with open(version_file) as version: 502 | version_text = version.read().strip() 503 | old_version = semver.Version.parse(version_text) 504 | except FileNotFoundError: 505 | LOGGER.error('Could not find .VERSION file') 506 | raise SystemExit(1) 507 | except ValueError: 508 | LOGGER.error('Invalid version found in .VERSION file "%s"', version_text) 509 | raise SystemExit(1) 510 | if segment: 511 | if segment not in ('major', 'minor', 'patch'): 512 | LOGGER.error('Invalid segment "%s" was provided for semantic versioning, exiting...') 513 | raise SystemExit(1) 514 | new_version = getattr(old_version, f'next_{segment}').text 515 | with open(version_file, 'w') as vfile: 516 | vfile.write(new_version) 517 | return new_version 518 | else: 519 | return version_text 520 | 521 | 522 | @contextmanager 523 | def cd(new_directory, clean_up=lambda: True): # pylint: disable=invalid-name 524 | """Changes into a given directory and cleans up after it is done 525 | 526 | Args: 527 | new_directory: The directory to change to 528 | clean_up: A method to clean up the working directory once done 529 | 530 | """ 531 | previous_directory = os.getcwd() 532 | os.chdir(os.path.expanduser(new_directory)) 533 | try: 534 | yield 535 | finally: 536 | os.chdir(previous_directory) 537 | clean_up() 538 | 539 | 540 | @contextmanager 541 | def tempdir(): 542 | """Creates a temporary directory""" 543 | directory_path = tempfile.mkdtemp() 544 | 545 | def clean_up(): # pylint: disable=missing-docstring 546 | shutil.rmtree(directory_path, onerror=on_error) 547 | 548 | with cd(directory_path, clean_up): 549 | yield directory_path 550 | 551 | 552 | class Pushd(object): 553 | """Implements bash pushd capabilities""" 554 | 555 | cwd = None 556 | original_dir = None 557 | 558 | def __init__(self, directory_name): 559 | self.cwd = os.path.realpath(directory_name) 560 | 561 | def __enter__(self): 562 | self.original_dir = os.getcwd() 563 | os.chdir(self.cwd) 564 | return self 565 | 566 | def __exit__(self, exception_type, exception_value, traceback): 567 | os.chdir(self.original_dir) 568 | 569 | 570 | def update_pipfile(stdout: bool): 571 | import toml 572 | project = Project() 573 | LOGGER.debug(f"Processing {project.pipfile_location}") 574 | 575 | top_level_packages, top_level_dev_packages = get_top_level_dependencies() 576 | all_packages, all_dev_packages = get_all_packages() 577 | 578 | pipfile = toml.load(project.pipfile_location) 579 | configuration = [{'section': 'packages', 580 | 'top_level': top_level_packages, 581 | 'all_packages': all_packages}, 582 | {'section': 'dev-packages', 583 | 'top_level': top_level_dev_packages, 584 | 'all_packages': all_dev_packages}] 585 | for config in configuration: 586 | pipfile[config.get('section')] = {package.name: package.full_version 587 | for package in _get_packages(config.get('top_level'), 588 | config.get('all_packages'))} 589 | 590 | if stdout: 591 | LOGGER.debug(f'Outputting Pipfile on stdout') 592 | print(toml.dumps(pipfile)) 593 | else: 594 | LOGGER.debug(f'Outputting Pipfile top {project.pipfile_location}') 595 | with open(project.pipfile_location, 'w') as writer: 596 | writer.write(toml.dumps(pipfile)) 597 | 598 | return True 599 | -------------------------------------------------------------------------------- /_CI/library/patch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Patch utility to apply unified diffs 4 | 5 | Brute-force line-by-line non-recursive parsing 6 | 7 | Copyright (c) 2008-2016 anatoly techtonik 8 | Available under the terms of MIT license 9 | 10 | """ 11 | from __future__ import print_function 12 | 13 | __author__ = "anatoly techtonik " 14 | __version__ = "1.16" 15 | __license__ = "MIT" 16 | __url__ = "https://github.com/techtonik/python-patch" 17 | 18 | import copy 19 | import logging 20 | import re 21 | 22 | # cStringIO doesn't support unicode in 2.5 23 | try: 24 | from StringIO import StringIO 25 | except ImportError: 26 | from io import BytesIO as StringIO # python 3 27 | try: 28 | import urllib2 as urllib_request 29 | except ImportError: 30 | import urllib.request as urllib_request 31 | 32 | from os.path import exists, isfile, abspath 33 | import os 34 | import posixpath 35 | import shutil 36 | import sys 37 | 38 | 39 | PY3K = sys.version_info >= (3, 0) 40 | 41 | # PEP 3114 42 | if not PY3K: 43 | compat_next = lambda gen: gen.next() 44 | else: 45 | compat_next = lambda gen: gen.__next__() 46 | 47 | def tostr(b): 48 | """ Python 3 bytes encoder. Used to print filename in 49 | diffstat output. Assumes that filenames are in utf-8. 50 | """ 51 | if not PY3K: 52 | return b 53 | 54 | # [ ] figure out how to print non-utf-8 filenames without 55 | # information loss 56 | return b.decode('utf-8') 57 | 58 | 59 | #------------------------------------------------ 60 | # Logging is controlled by logger named after the 61 | # module name (e.g. 'patch' for patch.py module) 62 | 63 | logger = logging.getLogger(__name__) 64 | 65 | debug = logger.debug 66 | info = logger.info 67 | warning = logger.warning 68 | 69 | class NullHandler(logging.Handler): 70 | """ Copied from Python 2.7 to avoid getting 71 | `No handlers could be found for logger "patch"` 72 | http://bugs.python.org/issue16539 73 | """ 74 | def handle(self, record): 75 | pass 76 | def emit(self, record): 77 | pass 78 | def createLock(self): 79 | self.lock = None 80 | 81 | streamhandler = logging.StreamHandler() 82 | 83 | # initialize logger itself 84 | logger.addHandler(NullHandler()) 85 | 86 | debugmode = False 87 | 88 | def setdebug(): 89 | global debugmode, streamhandler 90 | 91 | debugmode = True 92 | loglevel = logging.DEBUG 93 | logformat = "%(levelname)8s %(message)s" 94 | logger.setLevel(loglevel) 95 | 96 | if streamhandler not in logger.handlers: 97 | # when used as a library, streamhandler is not added 98 | # by default 99 | logger.addHandler(streamhandler) 100 | 101 | streamhandler.setFormatter(logging.Formatter(logformat)) 102 | 103 | 104 | #------------------------------------------------ 105 | # Constants for Patch/PatchSet types 106 | 107 | DIFF = PLAIN = "plain" 108 | GIT = "git" 109 | HG = MERCURIAL = "mercurial" 110 | SVN = SUBVERSION = "svn" 111 | # mixed type is only actual when PatchSet contains 112 | # Patches of different type 113 | MIXED = MIXED = "mixed" 114 | 115 | 116 | #------------------------------------------------ 117 | # Helpers (these could come with Python stdlib) 118 | 119 | # x...() function are used to work with paths in 120 | # cross-platform manner - all paths use forward 121 | # slashes even on Windows. 122 | 123 | def xisabs(filename): 124 | """ Cross-platform version of `os.path.isabs()` 125 | Returns True if `filename` is absolute on 126 | Linux, OS X or Windows. 127 | """ 128 | if filename.startswith(b'/'): # Linux/Unix 129 | return True 130 | elif filename.startswith(b'\\'): # Windows 131 | return True 132 | elif re.match(b'\\w:[\\\\/]', filename): # Windows 133 | return True 134 | return False 135 | 136 | def xnormpath(path): 137 | """ Cross-platform version of os.path.normpath """ 138 | # replace escapes and Windows slashes 139 | normalized = posixpath.normpath(path).replace(b'\\', b'/') 140 | # fold the result 141 | return posixpath.normpath(normalized) 142 | 143 | def xstrip(filename): 144 | """ Make relative path out of absolute by stripping 145 | prefixes used on Linux, OS X and Windows. 146 | 147 | This function is critical for security. 148 | """ 149 | while xisabs(filename): 150 | # strip windows drive with all slashes 151 | if re.match(b'\\w:[\\\\/]', filename): 152 | filename = re.sub(b'^\\w+:[\\\\/]+', b'', filename) 153 | # strip all slashes 154 | elif re.match(b'[\\\\/]', filename): 155 | filename = re.sub(b'^[\\\\/]+', b'', filename) 156 | return filename 157 | 158 | #----------------------------------------------- 159 | # Main API functions 160 | 161 | def fromfile(filename): 162 | """ Parse patch file. If successful, returns 163 | PatchSet() object. Otherwise returns False. 164 | """ 165 | patchset = PatchSet() 166 | debug("reading %s" % filename) 167 | fp = open(filename, "rb") 168 | res = patchset.parse(fp) 169 | fp.close() 170 | if res == True: 171 | return patchset 172 | return False 173 | 174 | 175 | def fromstring(s): 176 | """ Parse text string and return PatchSet() 177 | object (or False if parsing fails) 178 | """ 179 | ps = PatchSet( StringIO(s) ) 180 | if ps.errors == 0: 181 | return ps 182 | return False 183 | 184 | 185 | def fromurl(url): 186 | """ Parse patch from an URL, return False 187 | if an error occured. Note that this also 188 | can throw urlopen() exceptions. 189 | """ 190 | ps = PatchSet( urllib_request.urlopen(url) ) 191 | if ps.errors == 0: 192 | return ps 193 | return False 194 | 195 | 196 | # --- Utility functions --- 197 | # [ ] reuse more universal pathsplit() 198 | def pathstrip(path, n): 199 | """ Strip n leading components from the given path """ 200 | pathlist = [path] 201 | while os.path.dirname(pathlist[0]) != b'': 202 | pathlist[0:1] = os.path.split(pathlist[0]) 203 | return b'/'.join(pathlist[n:]) 204 | # --- /Utility function --- 205 | 206 | 207 | class Hunk(object): 208 | """ Parsed hunk data container (hunk starts with @@ -R +R @@) """ 209 | 210 | def __init__(self): 211 | self.startsrc=None #: line count starts with 1 212 | self.linessrc=None 213 | self.starttgt=None 214 | self.linestgt=None 215 | self.invalid=False 216 | self.desc='' 217 | self.text=[] 218 | 219 | # def apply(self, estream): 220 | # """ write hunk data into enumerable stream 221 | # return strings one by one until hunk is 222 | # over 223 | # 224 | # enumerable stream are tuples (lineno, line) 225 | # where lineno starts with 0 226 | # """ 227 | # pass 228 | 229 | 230 | class Patch(object): 231 | """ Patch for a single file. 232 | If used as an iterable, returns hunks. 233 | """ 234 | def __init__(self): 235 | self.source = None 236 | self.target = None 237 | self.hunks = [] 238 | self.hunkends = [] 239 | self.header = [] 240 | 241 | self.type = None 242 | 243 | def __iter__(self): 244 | for h in self.hunks: 245 | yield h 246 | 247 | 248 | class PatchSet(object): 249 | """ PatchSet is a patch parser and container. 250 | When used as an iterable, returns patches. 251 | """ 252 | 253 | def __init__(self, stream=None): 254 | # --- API accessible fields --- 255 | 256 | # name of the PatchSet (filename or ...) 257 | self.name = None 258 | # patch set type - one of constants 259 | self.type = None 260 | 261 | # list of Patch objects 262 | self.items = [] 263 | 264 | self.errors = 0 # fatal parsing errors 265 | self.warnings = 0 # non-critical warnings 266 | # --- /API --- 267 | 268 | if stream: 269 | self.parse(stream) 270 | 271 | def __len__(self): 272 | return len(self.items) 273 | 274 | def __iter__(self): 275 | for i in self.items: 276 | yield i 277 | 278 | def parse(self, stream): 279 | """ parse unified diff 280 | return True on success 281 | """ 282 | lineends = dict(lf=0, crlf=0, cr=0) 283 | nexthunkno = 0 #: even if index starts with 0 user messages number hunks from 1 284 | 285 | p = None 286 | hunk = None 287 | # hunkactual variable is used to calculate hunk lines for comparison 288 | hunkactual = dict(linessrc=None, linestgt=None) 289 | 290 | 291 | class wrapumerate(enumerate): 292 | """Enumerate wrapper that uses boolean end of stream status instead of 293 | StopIteration exception, and properties to access line information. 294 | """ 295 | 296 | def __init__(self, *args, **kwargs): 297 | # we don't call parent, it is magically created by __new__ method 298 | 299 | self._exhausted = False 300 | self._lineno = False # after end of stream equal to the num of lines 301 | self._line = False # will be reset to False after end of stream 302 | 303 | def next(self): 304 | """Try to read the next line and return True if it is available, 305 | False if end of stream is reached.""" 306 | if self._exhausted: 307 | return False 308 | 309 | try: 310 | self._lineno, self._line = compat_next(super(wrapumerate, self)) 311 | except StopIteration: 312 | self._exhausted = True 313 | self._line = False 314 | return False 315 | return True 316 | 317 | @property 318 | def is_empty(self): 319 | return self._exhausted 320 | 321 | @property 322 | def line(self): 323 | return self._line 324 | 325 | @property 326 | def lineno(self): 327 | return self._lineno 328 | 329 | # define states (possible file regions) that direct parse flow 330 | headscan = True # start with scanning header 331 | filenames = False # lines starting with --- and +++ 332 | 333 | hunkhead = False # @@ -R +R @@ sequence 334 | hunkbody = False # 335 | hunkskip = False # skipping invalid hunk mode 336 | 337 | hunkparsed = False # state after successfully parsed hunk 338 | 339 | # regexp to match start of hunk, used groups - 1,3,4,6 340 | re_hunk_start = re.compile(b"^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@") 341 | 342 | self.errors = 0 343 | # temp buffers for header and filenames info 344 | header = [] 345 | srcname = None 346 | tgtname = None 347 | 348 | # start of main cycle 349 | # each parsing block already has line available in fe.line 350 | fe = wrapumerate(stream) 351 | while fe.next(): 352 | 353 | # -- deciders: these only switch state to decide who should process 354 | # -- line fetched at the start of this cycle 355 | if hunkparsed: 356 | hunkparsed = False 357 | if re_hunk_start.match(fe.line): 358 | hunkhead = True 359 | elif fe.line.startswith(b"--- "): 360 | filenames = True 361 | else: 362 | headscan = True 363 | # -- ------------------------------------ 364 | 365 | # read out header 366 | if headscan: 367 | while not fe.is_empty and not fe.line.startswith(b"--- "): 368 | header.append(fe.line) 369 | fe.next() 370 | if fe.is_empty: 371 | if p == None: 372 | debug("no patch data found") # error is shown later 373 | self.errors += 1 374 | else: 375 | info("%d unparsed bytes left at the end of stream" % len(b''.join(header))) 376 | self.warnings += 1 377 | # TODO check for \No new line at the end.. 378 | # TODO test for unparsed bytes 379 | # otherwise error += 1 380 | # this is actually a loop exit 381 | continue 382 | 383 | headscan = False 384 | # switch to filenames state 385 | filenames = True 386 | 387 | line = fe.line 388 | lineno = fe.lineno 389 | 390 | 391 | # hunkskip and hunkbody code skipped until definition of hunkhead is parsed 392 | if hunkbody: 393 | # [x] treat empty lines inside hunks as containing single space 394 | # (this happens when diff is saved by copy/pasting to editor 395 | # that strips trailing whitespace) 396 | if line.strip(b"\r\n") == b"": 397 | debug("expanding empty line in a middle of hunk body") 398 | self.warnings += 1 399 | line = b' ' + line 400 | 401 | # process line first 402 | if re.match(b"^[- \\+\\\\]", line): 403 | # gather stats about line endings 404 | if line.endswith(b"\r\n"): 405 | p.hunkends["crlf"] += 1 406 | elif line.endswith(b"\n"): 407 | p.hunkends["lf"] += 1 408 | elif line.endswith(b"\r"): 409 | p.hunkends["cr"] += 1 410 | 411 | if line.startswith(b"-"): 412 | hunkactual["linessrc"] += 1 413 | elif line.startswith(b"+"): 414 | hunkactual["linestgt"] += 1 415 | elif not line.startswith(b"\\"): 416 | hunkactual["linessrc"] += 1 417 | hunkactual["linestgt"] += 1 418 | hunk.text.append(line) 419 | # todo: handle \ No newline cases 420 | else: 421 | warning("invalid hunk no.%d at %d for target file %s" % (nexthunkno, lineno+1, p.target)) 422 | # add hunk status node 423 | hunk.invalid = True 424 | p.hunks.append(hunk) 425 | self.errors += 1 426 | # switch to hunkskip state 427 | hunkbody = False 428 | hunkskip = True 429 | 430 | # check exit conditions 431 | if hunkactual["linessrc"] > hunk.linessrc or hunkactual["linestgt"] > hunk.linestgt: 432 | warning("extra lines for hunk no.%d at %d for target %s" % (nexthunkno, lineno+1, p.target)) 433 | # add hunk status node 434 | hunk.invalid = True 435 | p.hunks.append(hunk) 436 | self.errors += 1 437 | # switch to hunkskip state 438 | hunkbody = False 439 | hunkskip = True 440 | elif hunk.linessrc == hunkactual["linessrc"] and hunk.linestgt == hunkactual["linestgt"]: 441 | # hunk parsed successfully 442 | p.hunks.append(hunk) 443 | # switch to hunkparsed state 444 | hunkbody = False 445 | hunkparsed = True 446 | 447 | # detect mixed window/unix line ends 448 | ends = p.hunkends 449 | if ((ends["cr"]!=0) + (ends["crlf"]!=0) + (ends["lf"]!=0)) > 1: 450 | warning("inconsistent line ends in patch hunks for %s" % p.source) 451 | self.warnings += 1 452 | if debugmode: 453 | debuglines = dict(ends) 454 | debuglines.update(file=p.target, hunk=nexthunkno) 455 | debug("crlf: %(crlf)d lf: %(lf)d cr: %(cr)d\t - file: %(file)s hunk: %(hunk)d" % debuglines) 456 | # fetch next line 457 | continue 458 | 459 | if hunkskip: 460 | if re_hunk_start.match(line): 461 | # switch to hunkhead state 462 | hunkskip = False 463 | hunkhead = True 464 | elif line.startswith(b"--- "): 465 | # switch to filenames state 466 | hunkskip = False 467 | filenames = True 468 | if debugmode and len(self.items) > 0: 469 | debug("- %2d hunks for %s" % (len(p.hunks), p.source)) 470 | 471 | if filenames: 472 | if line.startswith(b"--- "): 473 | if srcname != None: 474 | # XXX testcase 475 | warning("skipping false patch for %s" % srcname) 476 | srcname = None 477 | # XXX header += srcname 478 | # double source filename line is encountered 479 | # attempt to restart from this second line 480 | re_filename = b"^--- ([^\t]+)" 481 | match = re.match(re_filename, line) 482 | # todo: support spaces in filenames 483 | if match: 484 | srcname = match.group(1).strip() 485 | else: 486 | warning("skipping invalid filename at line %d" % (lineno+1)) 487 | self.errors += 1 488 | # XXX p.header += line 489 | # switch back to headscan state 490 | filenames = False 491 | headscan = True 492 | elif not line.startswith(b"+++ "): 493 | if srcname != None: 494 | warning("skipping invalid patch with no target for %s" % srcname) 495 | self.errors += 1 496 | srcname = None 497 | # XXX header += srcname 498 | # XXX header += line 499 | else: 500 | # this should be unreachable 501 | warning("skipping invalid target patch") 502 | filenames = False 503 | headscan = True 504 | else: 505 | if tgtname != None: 506 | # XXX seems to be a dead branch 507 | warning("skipping invalid patch - double target at line %d" % (lineno+1)) 508 | self.errors += 1 509 | srcname = None 510 | tgtname = None 511 | # XXX header += srcname 512 | # XXX header += tgtname 513 | # XXX header += line 514 | # double target filename line is encountered 515 | # switch back to headscan state 516 | filenames = False 517 | headscan = True 518 | else: 519 | re_filename = b"^\+\+\+ ([^\t]+)" 520 | match = re.match(re_filename, line) 521 | if not match: 522 | warning("skipping invalid patch - no target filename at line %d" % (lineno+1)) 523 | self.errors += 1 524 | srcname = None 525 | # switch back to headscan state 526 | filenames = False 527 | headscan = True 528 | else: 529 | if p: # for the first run p is None 530 | self.items.append(p) 531 | p = Patch() 532 | p.source = srcname 533 | srcname = None 534 | p.target = match.group(1).strip() 535 | p.header = header 536 | header = [] 537 | # switch to hunkhead state 538 | filenames = False 539 | hunkhead = True 540 | nexthunkno = 0 541 | p.hunkends = lineends.copy() 542 | continue 543 | 544 | if hunkhead: 545 | match = re.match(b"^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@(.*)", line) 546 | if not match: 547 | if not p.hunks: 548 | warning("skipping invalid patch with no hunks for file %s" % p.source) 549 | self.errors += 1 550 | # XXX review switch 551 | # switch to headscan state 552 | hunkhead = False 553 | headscan = True 554 | continue 555 | else: 556 | # TODO review condition case 557 | # switch to headscan state 558 | hunkhead = False 559 | headscan = True 560 | else: 561 | hunk = Hunk() 562 | hunk.startsrc = int(match.group(1)) 563 | hunk.linessrc = 1 564 | if match.group(3): hunk.linessrc = int(match.group(3)) 565 | hunk.starttgt = int(match.group(4)) 566 | hunk.linestgt = 1 567 | if match.group(6): hunk.linestgt = int(match.group(6)) 568 | hunk.invalid = False 569 | hunk.desc = match.group(7)[1:].rstrip() 570 | hunk.text = [] 571 | 572 | hunkactual["linessrc"] = hunkactual["linestgt"] = 0 573 | 574 | # switch to hunkbody state 575 | hunkhead = False 576 | hunkbody = True 577 | nexthunkno += 1 578 | continue 579 | 580 | # /while fe.next() 581 | 582 | if p: 583 | self.items.append(p) 584 | 585 | if not hunkparsed: 586 | if hunkskip: 587 | warning("warning: finished with errors, some hunks may be invalid") 588 | elif headscan: 589 | if len(self.items) == 0: 590 | warning("error: no patch data found!") 591 | return False 592 | else: # extra data at the end of file 593 | pass 594 | else: 595 | warning("error: patch stream is incomplete!") 596 | self.errors += 1 597 | if len(self.items) == 0: 598 | return False 599 | 600 | if debugmode and len(self.items) > 0: 601 | debug("- %2d hunks for %s" % (len(p.hunks), p.source)) 602 | 603 | # XXX fix total hunks calculation 604 | debug("total files: %d total hunks: %d" % (len(self.items), 605 | sum(len(p.hunks) for p in self.items))) 606 | 607 | # ---- detect patch and patchset types ---- 608 | for idx, p in enumerate(self.items): 609 | self.items[idx].type = self._detect_type(p) 610 | 611 | types = set([p.type for p in self.items]) 612 | if len(types) > 1: 613 | self.type = MIXED 614 | else: 615 | self.type = types.pop() 616 | # -------- 617 | 618 | self._normalize_filenames() 619 | 620 | return (self.errors == 0) 621 | 622 | def _detect_type(self, p): 623 | """ detect and return type for the specified Patch object 624 | analyzes header and filenames info 625 | 626 | NOTE: must be run before filenames are normalized 627 | """ 628 | 629 | # check for SVN 630 | # - header starts with Index: 631 | # - next line is ===... delimiter 632 | # - filename is followed by revision number 633 | # TODO add SVN revision 634 | if (len(p.header) > 1 and p.header[-2].startswith(b"Index: ") 635 | and p.header[-1].startswith(b"="*67)): 636 | return SVN 637 | 638 | # common checks for both HG and GIT 639 | DVCS = ((p.source.startswith(b'a/') or p.source == b'/dev/null') 640 | and (p.target.startswith(b'b/') or p.target == b'/dev/null')) 641 | 642 | # GIT type check 643 | # - header[-2] is like "diff --git a/oldname b/newname" 644 | # - header[-1] is like "index .. " 645 | # TODO add git rename diffs and add/remove diffs 646 | # add git diff with spaced filename 647 | # TODO http://www.kernel.org/pub/software/scm/git/docs/git-diff.html 648 | 649 | # Git patch header len is 2 min 650 | if len(p.header) > 1: 651 | # detect the start of diff header - there might be some comments before 652 | for idx in reversed(range(len(p.header))): 653 | if p.header[idx].startswith(b"diff --git"): 654 | break 655 | if p.header[idx].startswith(b'diff --git a/'): 656 | if (idx+1 < len(p.header) 657 | and re.match(b'index \\w{7}..\\w{7} \\d{6}', p.header[idx+1])): 658 | if DVCS: 659 | return GIT 660 | 661 | # HG check 662 | # 663 | # - for plain HG format header is like "diff -r b2d9961ff1f5 filename" 664 | # - for Git-style HG patches it is "diff --git a/oldname b/newname" 665 | # - filename starts with a/, b/ or is equal to /dev/null 666 | # - exported changesets also contain the header 667 | # # HG changeset patch 668 | # # User name@example.com 669 | # ... 670 | # TODO add MQ 671 | # TODO add revision info 672 | if len(p.header) > 0: 673 | if DVCS and re.match(b'diff -r \\w{12} .*', p.header[-1]): 674 | return HG 675 | if DVCS and p.header[-1].startswith(b'diff --git a/'): 676 | if len(p.header) == 1: # native Git patch header len is 2 677 | return HG 678 | elif p.header[0].startswith(b'# HG changeset patch'): 679 | return HG 680 | 681 | return PLAIN 682 | 683 | 684 | def _normalize_filenames(self): 685 | """ sanitize filenames, normalizing paths, i.e.: 686 | 1. strip a/ and b/ prefixes from GIT and HG style patches 687 | 2. remove all references to parent directories (with warning) 688 | 3. translate any absolute paths to relative (with warning) 689 | 690 | [x] always use forward slashes to be crossplatform 691 | (diff/patch were born as a unix utility after all) 692 | 693 | return None 694 | """ 695 | if debugmode: 696 | debug("normalize filenames") 697 | for i,p in enumerate(self.items): 698 | # if debugmode: 699 | # debug(" patch type = " + p.type) 700 | # debug(" source = " + p.source.) 701 | # debug(" target = " + p.target) 702 | if p.type in (HG, GIT): 703 | # TODO: figure out how to deal with /dev/null entries 704 | debug("stripping a/ and b/ prefixes") 705 | if p.source != '/dev/null': 706 | if not p.source.startswith(b"a/"): 707 | warning("invalid source filename") 708 | else: 709 | p.source = p.source[2:] 710 | if p.target != '/dev/null': 711 | if not p.target.startswith(b"b/"): 712 | warning("invalid target filename") 713 | else: 714 | p.target = p.target[2:] 715 | 716 | p.source = xnormpath(p.source) 717 | p.target = xnormpath(p.target) 718 | 719 | sep = b'/' # sep value can be hardcoded, but it looks nice this way 720 | 721 | # references to parent are not allowed 722 | if p.source.startswith(b".." + sep): 723 | warning("error: stripping parent path for source file patch no.%d" % (i+1)) 724 | self.warnings += 1 725 | while p.source.startswith(b".." + sep): 726 | p.source = p.source.partition(sep)[2] 727 | if p.target.startswith(b".." + sep): 728 | warning("error: stripping parent path for target file patch no.%d" % (i+1)) 729 | self.warnings += 1 730 | while p.target.startswith(b".." + sep): 731 | p.target = p.target.partition(sep)[2] 732 | # absolute paths are not allowed 733 | if xisabs(p.source) or xisabs(p.target): 734 | warning("error: absolute paths are not allowed - file no.%d" % (i+1)) 735 | self.warnings += 1 736 | if xisabs(p.source): 737 | warning("stripping absolute path from source name '%s'" % p.source) 738 | p.source = xstrip(p.source) 739 | if xisabs(p.target): 740 | warning("stripping absolute path from target name '%s'" % p.target) 741 | p.target = xstrip(p.target) 742 | 743 | self.items[i].source = p.source 744 | self.items[i].target = p.target 745 | 746 | 747 | def diffstat(self): 748 | """ calculate diffstat and return as a string 749 | Notes: 750 | - original diffstat ouputs target filename 751 | - single + or - shouldn't escape histogram 752 | """ 753 | names = [] 754 | insert = [] 755 | delete = [] 756 | delta = 0 # size change in bytes 757 | namelen = 0 758 | maxdiff = 0 # max number of changes for single file 759 | # (for histogram width calculation) 760 | for patch in self.items: 761 | i,d = 0,0 762 | for hunk in patch.hunks: 763 | for line in hunk.text: 764 | if line.startswith(b'+'): 765 | i += 1 766 | delta += len(line)-1 767 | elif line.startswith(b'-'): 768 | d += 1 769 | delta -= len(line)-1 770 | names.append(patch.target) 771 | insert.append(i) 772 | delete.append(d) 773 | namelen = max(namelen, len(patch.target)) 774 | maxdiff = max(maxdiff, i+d) 775 | output = '' 776 | statlen = len(str(maxdiff)) # stats column width 777 | for i,n in enumerate(names): 778 | # %-19s | %-4d %s 779 | format = " %-" + str(namelen) + "s | %" + str(statlen) + "s %s\n" 780 | 781 | hist = '' 782 | # -- calculating histogram -- 783 | width = len(format % ('', '', '')) 784 | histwidth = max(2, 80 - width) 785 | if maxdiff < histwidth: 786 | hist = "+"*insert[i] + "-"*delete[i] 787 | else: 788 | iratio = (float(insert[i]) / maxdiff) * histwidth 789 | dratio = (float(delete[i]) / maxdiff) * histwidth 790 | 791 | # make sure every entry gets at least one + or - 792 | iwidth = 1 if 0 < iratio < 1 else int(iratio) 793 | dwidth = 1 if 0 < dratio < 1 else int(dratio) 794 | #print(iratio, dratio, iwidth, dwidth, histwidth) 795 | hist = "+"*int(iwidth) + "-"*int(dwidth) 796 | # -- /calculating +- histogram -- 797 | output += (format % (tostr(names[i]), str(insert[i] + delete[i]), hist)) 798 | 799 | output += (" %d files changed, %d insertions(+), %d deletions(-), %+d bytes" 800 | % (len(names), sum(insert), sum(delete), delta)) 801 | return output 802 | 803 | 804 | def findfile(self, old, new): 805 | """ return name of file to be patched or None """ 806 | if exists(old): 807 | return old 808 | elif exists(new): 809 | return new 810 | else: 811 | # [w] Google Code generates broken patches with its online editor 812 | debug("broken patch from Google Code, stripping prefixes..") 813 | if old.startswith(b'a/') and new.startswith(b'b/'): 814 | old, new = old[2:], new[2:] 815 | debug(" %s" % old) 816 | debug(" %s" % new) 817 | if exists(old): 818 | return old 819 | elif exists(new): 820 | return new 821 | return None 822 | 823 | 824 | def apply(self, strip=0, root=None): 825 | """ Apply parsed patch, optionally stripping leading components 826 | from file paths. `root` parameter specifies working dir. 827 | return True on success 828 | """ 829 | if root: 830 | prevdir = os.getcwd() 831 | os.chdir(root) 832 | 833 | total = len(self.items) 834 | errors = 0 835 | if strip: 836 | # [ ] test strip level exceeds nesting level 837 | # [ ] test the same only for selected files 838 | # [ ] test if files end up being on the same level 839 | try: 840 | strip = int(strip) 841 | except ValueError: 842 | errors += 1 843 | warning("error: strip parameter '%s' must be an integer" % strip) 844 | strip = 0 845 | 846 | #for fileno, filename in enumerate(self.source): 847 | for i,p in enumerate(self.items): 848 | if strip: 849 | debug("stripping %s leading component(s) from:" % strip) 850 | debug(" %s" % p.source) 851 | debug(" %s" % p.target) 852 | old = pathstrip(p.source, strip) 853 | new = pathstrip(p.target, strip) 854 | else: 855 | old, new = p.source, p.target 856 | 857 | filename = self.findfile(old, new) 858 | 859 | if not filename: 860 | warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new)) 861 | errors += 1 862 | continue 863 | if not isfile(filename): 864 | warning("not a file - %s" % filename) 865 | errors += 1 866 | continue 867 | 868 | # [ ] check absolute paths security here 869 | debug("processing %d/%d:\t %s" % (i+1, total, filename)) 870 | 871 | # validate before patching 872 | f2fp = open(filename, 'rb') 873 | hunkno = 0 874 | hunk = p.hunks[hunkno] 875 | hunkfind = [] 876 | hunkreplace = [] 877 | validhunks = 0 878 | canpatch = False 879 | for lineno, line in enumerate(f2fp): 880 | if lineno+1 < hunk.startsrc: 881 | continue 882 | elif lineno+1 == hunk.startsrc: 883 | hunkfind = [x[1:].rstrip(b"\r\n") for x in hunk.text if x[0] in b" -"] 884 | hunkreplace = [x[1:].rstrip(b"\r\n") for x in hunk.text if x[0] in b" +"] 885 | #pprint(hunkreplace) 886 | hunklineno = 0 887 | 888 | # todo \ No newline at end of file 889 | 890 | # check hunks in source file 891 | if lineno+1 < hunk.startsrc+len(hunkfind)-1: 892 | if line.rstrip(b"\r\n") == hunkfind[hunklineno]: 893 | hunklineno+=1 894 | else: 895 | info("file %d/%d:\t %s" % (i+1, total, filename)) 896 | info(" hunk no.%d doesn't match source file at line %d" % (hunkno+1, lineno+1)) 897 | info(" expected: %s" % hunkfind[hunklineno]) 898 | info(" actual : %s" % line.rstrip(b"\r\n")) 899 | # not counting this as error, because file may already be patched. 900 | # check if file is already patched is done after the number of 901 | # invalid hunks if found 902 | # TODO: check hunks against source/target file in one pass 903 | # API - check(stream, srchunks, tgthunks) 904 | # return tuple (srcerrs, tgterrs) 905 | 906 | # continue to check other hunks for completeness 907 | hunkno += 1 908 | if hunkno < len(p.hunks): 909 | hunk = p.hunks[hunkno] 910 | continue 911 | else: 912 | break 913 | 914 | # check if processed line is the last line 915 | if lineno+1 == hunk.startsrc+len(hunkfind)-1: 916 | debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1, filename)) 917 | hunkno+=1 918 | validhunks+=1 919 | if hunkno < len(p.hunks): 920 | hunk = p.hunks[hunkno] 921 | else: 922 | if validhunks == len(p.hunks): 923 | # patch file 924 | canpatch = True 925 | break 926 | else: 927 | if hunkno < len(p.hunks): 928 | warning("premature end of source file %s at hunk %d" % (filename, hunkno+1)) 929 | errors += 1 930 | 931 | f2fp.close() 932 | 933 | if validhunks < len(p.hunks): 934 | if self._match_file_hunks(filename, p.hunks): 935 | warning("already patched %s" % filename) 936 | else: 937 | warning("source file is different - %s" % filename) 938 | errors += 1 939 | if canpatch: 940 | backupname = filename+b".orig" 941 | if exists(backupname): 942 | warning("can't backup original file to %s - aborting" % backupname) 943 | else: 944 | import shutil 945 | shutil.move(filename, backupname) 946 | if self.write_hunks(backupname, filename, p.hunks): 947 | info("successfully patched %d/%d:\t %s" % (i+1, total, filename)) 948 | os.unlink(backupname) 949 | else: 950 | errors += 1 951 | warning("error patching file %s" % filename) 952 | shutil.copy(filename, filename+".invalid") 953 | warning("invalid version is saved to %s" % filename+".invalid") 954 | # todo: proper rejects 955 | shutil.move(backupname, filename) 956 | 957 | if root: 958 | os.chdir(prevdir) 959 | 960 | # todo: check for premature eof 961 | return (errors == 0) 962 | 963 | 964 | def _reverse(self): 965 | """ reverse patch direction (this doesn't touch filenames) """ 966 | for p in self.items: 967 | for h in p.hunks: 968 | h.startsrc, h.starttgt = h.starttgt, h.startsrc 969 | h.linessrc, h.linestgt = h.linestgt, h.linessrc 970 | for i,line in enumerate(h.text): 971 | # need to use line[0:1] here, because line[0] 972 | # returns int instead of bytes on Python 3 973 | if line[0:1] == b'+': 974 | h.text[i] = b'-' + line[1:] 975 | elif line[0:1] == b'-': 976 | h.text[i] = b'+' +line[1:] 977 | 978 | def revert(self, strip=0, root=None): 979 | """ apply patch in reverse order """ 980 | reverted = copy.deepcopy(self) 981 | reverted._reverse() 982 | return reverted.apply(strip, root) 983 | 984 | 985 | def can_patch(self, filename): 986 | """ Check if specified filename can be patched. Returns None if file can 987 | not be found among source filenames. False if patch can not be applied 988 | clearly. True otherwise. 989 | 990 | :returns: True, False or None 991 | """ 992 | filename = abspath(filename) 993 | for p in self.items: 994 | if filename == abspath(p.source): 995 | return self._match_file_hunks(filename, p.hunks) 996 | return None 997 | 998 | 999 | def _match_file_hunks(self, filepath, hunks): 1000 | matched = True 1001 | fp = open(abspath(filepath), 'rb') 1002 | 1003 | class NoMatch(Exception): 1004 | pass 1005 | 1006 | lineno = 1 1007 | line = fp.readline() 1008 | hno = None 1009 | try: 1010 | for hno, h in enumerate(hunks): 1011 | # skip to first line of the hunk 1012 | while lineno < h.starttgt: 1013 | if not len(line): # eof 1014 | debug("check failed - premature eof before hunk: %d" % (hno+1)) 1015 | raise NoMatch 1016 | line = fp.readline() 1017 | lineno += 1 1018 | for hline in h.text: 1019 | if hline.startswith(b"-"): 1020 | continue 1021 | if not len(line): 1022 | debug("check failed - premature eof on hunk: %d" % (hno+1)) 1023 | # todo: \ No newline at the end of file 1024 | raise NoMatch 1025 | if line.rstrip(b"\r\n") != hline[1:].rstrip(b"\r\n"): 1026 | debug("file is not patched - failed hunk: %d" % (hno+1)) 1027 | raise NoMatch 1028 | line = fp.readline() 1029 | lineno += 1 1030 | 1031 | except NoMatch: 1032 | matched = False 1033 | # todo: display failed hunk, i.e. expected/found 1034 | 1035 | fp.close() 1036 | return matched 1037 | 1038 | 1039 | def patch_stream(self, instream, hunks): 1040 | """ Generator that yields stream patched with hunks iterable 1041 | 1042 | Converts lineends in hunk lines to the best suitable format 1043 | autodetected from input 1044 | """ 1045 | 1046 | # todo: At the moment substituted lineends may not be the same 1047 | # at the start and at the end of patching. Also issue a 1048 | # warning/throw about mixed lineends (is it really needed?) 1049 | 1050 | hunks = iter(hunks) 1051 | 1052 | srclineno = 1 1053 | 1054 | lineends = {b'\n':0, b'\r\n':0, b'\r':0} 1055 | def get_line(): 1056 | """ 1057 | local utility function - return line from source stream 1058 | collecting line end statistics on the way 1059 | """ 1060 | line = instream.readline() 1061 | # 'U' mode works only with text files 1062 | if line.endswith(b"\r\n"): 1063 | lineends[b"\r\n"] += 1 1064 | elif line.endswith(b"\n"): 1065 | lineends[b"\n"] += 1 1066 | elif line.endswith(b"\r"): 1067 | lineends[b"\r"] += 1 1068 | return line 1069 | 1070 | for hno, h in enumerate(hunks): 1071 | debug("hunk %d" % (hno+1)) 1072 | # skip to line just before hunk starts 1073 | while srclineno < h.startsrc: 1074 | yield get_line() 1075 | srclineno += 1 1076 | 1077 | for hline in h.text: 1078 | # todo: check \ No newline at the end of file 1079 | if hline.startswith(b"-") or hline.startswith(b"\\"): 1080 | get_line() 1081 | srclineno += 1 1082 | continue 1083 | else: 1084 | if not hline.startswith(b"+"): 1085 | get_line() 1086 | srclineno += 1 1087 | line2write = hline[1:] 1088 | # detect if line ends are consistent in source file 1089 | if sum([bool(lineends[x]) for x in lineends]) == 1: 1090 | newline = [x for x in lineends if lineends[x] != 0][0] 1091 | yield line2write.rstrip(b"\r\n")+newline 1092 | else: # newlines are mixed 1093 | yield line2write 1094 | 1095 | for line in instream: 1096 | yield line 1097 | 1098 | 1099 | def write_hunks(self, srcname, tgtname, hunks): 1100 | src = open(srcname, "rb") 1101 | tgt = open(tgtname, "wb") 1102 | 1103 | debug("processing target file %s" % tgtname) 1104 | 1105 | tgt.writelines(self.patch_stream(src, hunks)) 1106 | 1107 | tgt.close() 1108 | src.close() 1109 | # [ ] TODO: add test for permission copy 1110 | shutil.copymode(srcname, tgtname) 1111 | return True 1112 | 1113 | 1114 | def dump(self): 1115 | for p in self.items: 1116 | for headline in p.header: 1117 | print(headline.rstrip('\n')) 1118 | print('--- ' + p.source) 1119 | print('+++ ' + p.target) 1120 | for h in p.hunks: 1121 | print('@@ -%s,%s +%s,%s @@' % (h.startsrc, h.linessrc, h.starttgt, h.linestgt)) 1122 | for line in h.text: 1123 | print(line.rstrip('\n')) 1124 | 1125 | 1126 | def main(): 1127 | from optparse import OptionParser 1128 | from os.path import exists 1129 | import sys 1130 | 1131 | opt = OptionParser(usage="1. %prog [options] unified.diff\n" 1132 | " 2. %prog [options] http://host/patch\n" 1133 | " 3. %prog [options] -- < unified.diff", 1134 | version="python-patch %s" % __version__) 1135 | opt.add_option("-q", "--quiet", action="store_const", dest="verbosity", 1136 | const=0, help="print only warnings and errors", default=1) 1137 | opt.add_option("-v", "--verbose", action="store_const", dest="verbosity", 1138 | const=2, help="be verbose") 1139 | opt.add_option("--debug", action="store_true", dest="debugmode", help="debug mode") 1140 | opt.add_option("--diffstat", action="store_true", dest="diffstat", 1141 | help="print diffstat and exit") 1142 | opt.add_option("-d", "--directory", metavar='DIR', 1143 | help="specify root directory for applying patch") 1144 | opt.add_option("-p", "--strip", type="int", metavar='N', default=0, 1145 | help="strip N path components from filenames") 1146 | opt.add_option("--revert", action="store_true", 1147 | help="apply patch in reverse order (unpatch)") 1148 | (options, args) = opt.parse_args() 1149 | 1150 | if not args and sys.argv[-1:] != ['--']: 1151 | opt.print_version() 1152 | opt.print_help() 1153 | sys.exit() 1154 | readstdin = (sys.argv[-1:] == ['--'] and not args) 1155 | 1156 | verbosity_levels = {0:logging.WARNING, 1:logging.INFO, 2:logging.DEBUG} 1157 | loglevel = verbosity_levels[options.verbosity] 1158 | logformat = "%(message)s" 1159 | logger.setLevel(loglevel) 1160 | streamhandler.setFormatter(logging.Formatter(logformat)) 1161 | 1162 | if options.debugmode: 1163 | setdebug() # this sets global debugmode variable 1164 | 1165 | if readstdin: 1166 | patch = PatchSet(sys.stdin) 1167 | else: 1168 | patchfile = args[0] 1169 | urltest = patchfile.split(':')[0] 1170 | if (':' in patchfile and urltest.isalpha() 1171 | and len(urltest) > 1): # one char before : is a windows drive letter 1172 | patch = fromurl(patchfile) 1173 | else: 1174 | if not exists(patchfile) or not isfile(patchfile): 1175 | sys.exit("patch file does not exist - %s" % patchfile) 1176 | patch = fromfile(patchfile) 1177 | 1178 | if options.diffstat: 1179 | print(patch.diffstat()) 1180 | sys.exit(0) 1181 | 1182 | #pprint(patch) 1183 | if options.revert: 1184 | patch.revert(options.strip, root=options.directory) or sys.exit(-1) 1185 | else: 1186 | patch.apply(options.strip, root=options.directory) or sys.exit(-1) 1187 | 1188 | # todo: document and test line ends handling logic - patch.py detects proper line-endings 1189 | # for inserted hunks and issues a warning if patched file has incosistent line ends 1190 | 1191 | 1192 | if __name__ == "__main__": 1193 | main() 1194 | 1195 | # Legend: 1196 | # [ ] - some thing to be done 1197 | # [w] - official wart, external or internal that is unlikely to be fixed 1198 | 1199 | # [ ] API break (2.x) wishlist 1200 | # PatchSet.items --> PatchSet.patches 1201 | 1202 | # [ ] run --revert test for all dataset items 1203 | # [ ] run .parse() / .dump() test for dataset 1204 | --------------------------------------------------------------------------------