diff --git a/.bandit.yml b/.bandit.yml new file mode 100644 index 0000000..fac45de --- /dev/null +++ b/.bandit.yml @@ -0,0 +1,404 @@ + +### This config may optionally select a subset of tests to run or skip by +### filling out the 'tests' and 'skips' lists given below. If no tests are +### specified for inclusion then it is assumed all tests are desired. The skips +### set will remove specific tests from the include set. This can be controlled +### using the -t/-s CLI options. Note that the same test ID should not appear +### in both 'tests' and 'skips', this would be nonsensical and is detected by +### Bandit at runtime. + +# Available tests: +# B101 : assert_used +# B102 : exec_used +# B103 : set_bad_file_permissions +# B104 : hardcoded_bind_all_interfaces +# B105 : hardcoded_password_string +# B106 : hardcoded_password_funcarg +# B107 : hardcoded_password_default +# B108 : hardcoded_tmp_directory +# B110 : try_except_pass +# B112 : try_except_continue +# B113 : request_without_timeout +# B201 : flask_debug_true +# B202 : tarfile_unsafe_members +# B301 : pickle +# B302 : marshal +# B303 : md5 +# B304 : ciphers +# B305 : cipher_modes +# B306 : mktemp_q +# B307 : eval +# B308 : mark_safe +# B310 : urllib_urlopen +# B311 : random +# B312 : telnetlib +# B313 : xml_bad_cElementTree +# B314 : xml_bad_ElementTree +# B315 : xml_bad_expatreader +# B316 : xml_bad_expatbuilder +# B317 : xml_bad_sax +# B318 : xml_bad_minidom +# B319 : xml_bad_pulldom +# B320 : xml_bad_etree +# B321 : ftplib +# B323 : unverified_context +# B324 : hashlib_insecure_functions +# B401 : import_telnetlib +# B402 : import_ftplib +# B403 : import_pickle +# B404 : import_subprocess +# B405 : import_xml_etree +# B406 : import_xml_sax +# B407 : import_xml_expat +# B408 : import_xml_minidom +# B409 : import_xml_pulldom +# B410 : import_lxml +# B411 : import_xmlrpclib +# B412 : import_httpoxy +# B413 : import_pycrypto +# B415 : import_pyghmi +# B501 : request_with_no_cert_validation +# B502 : ssl_with_bad_version +# B503 : ssl_with_bad_defaults +# B504 : ssl_with_no_version +# B505 : weak_cryptographic_key +# B506 : yaml_load +# B507 : ssh_no_host_key_verification +# B508 : snmp_insecure_version +# B509 : snmp_weak_cryptography +# B601 : paramiko_calls +# B602 : subprocess_popen_with_shell_equals_true +# B603 : subprocess_without_shell_equals_true +# B604 : any_other_function_with_shell_equals_true +# B605 : start_process_with_a_shell +# B606 : start_process_with_no_shell +# B607 : start_process_with_partial_path +# B608 : hardcoded_sql_expressions +# B609 : linux_commands_wildcard_injection +# B610 : django_extra_used +# B611 : django_rawsql_used +# B612 : logging_config_insecure_listen +# B701 : jinja2_autoescape_false +# B702 : use_of_mako_templates +# B703 : django_mark_safe + +# (optional) list included test IDs here, eg '[B101, B406]': +tests: + +# (optional) list skipped test IDs here, eg '[B101, B406]': +skips: + +### (optional) plugin settings - some test plugins require configuration data +### that may be given here, per-plugin. All bandit test plugins have a built in +### set of sensible defaults and these will be used if no configuration is +### provided. It is not necessary to provide settings for every (or any) plugin +### if the defaults are acceptable. + +any_other_function_with_shell_equals_true: + no_shell: + - os.execl + - os.execle + - os.execlp + - os.execlpe + - os.execv + - os.execve + - os.execvp + - os.execvpe + - os.spawnl + - os.spawnle + - os.spawnlp + - os.spawnlpe + - os.spawnv + - os.spawnve + - os.spawnvp + - os.spawnvpe + - os.startfile + shell: + - os.system + - os.popen + - os.popen2 + - os.popen3 + - os.popen4 + - popen2.popen2 + - popen2.popen3 + - popen2.popen4 + - popen2.Popen3 + - popen2.Popen4 + - commands.getoutput + - commands.getstatusoutput + subprocess: + - subprocess.Popen + - subprocess.call + - subprocess.check_call + - subprocess.check_output + - subprocess.run +assert_used: + skips: + - tests/*.py + - ./tests/*.py +hardcoded_tmp_directory: + tmp_dirs: + - /tmp + - /var/tmp + - /dev/shm +linux_commands_wildcard_injection: + no_shell: + - os.execl + - os.execle + - os.execlp + - os.execlpe + - os.execv + - os.execve + - os.execvp + - os.execvpe + - os.spawnl + - os.spawnle + - os.spawnlp + - os.spawnlpe + - os.spawnv + - os.spawnve + - os.spawnvp + - os.spawnvpe + - os.startfile + shell: + - os.system + - os.popen + - os.popen2 + - os.popen3 + - os.popen4 + - popen2.popen2 + - popen2.popen3 + - popen2.popen4 + - popen2.Popen3 + - popen2.Popen4 + - commands.getoutput + - commands.getstatusoutput + subprocess: + - subprocess.Popen + - subprocess.call + - subprocess.check_call + - subprocess.check_output + - subprocess.run +ssl_with_bad_defaults: + bad_protocol_versions: + - PROTOCOL_SSLv2 + - SSLv2_METHOD + - SSLv23_METHOD + - PROTOCOL_SSLv3 + - PROTOCOL_TLSv1 + - SSLv3_METHOD + - TLSv1_METHOD + - PROTOCOL_TLSv1_1 + - TLSv1_1_METHOD +ssl_with_bad_version: + bad_protocol_versions: + - PROTOCOL_SSLv2 + - SSLv2_METHOD + - SSLv23_METHOD + - PROTOCOL_SSLv3 + - PROTOCOL_TLSv1 + - SSLv3_METHOD + - TLSv1_METHOD + - PROTOCOL_TLSv1_1 + - TLSv1_1_METHOD +start_process_with_a_shell: + no_shell: + - os.execl + - os.execle + - os.execlp + - os.execlpe + - os.execv + - os.execve + - os.execvp + - os.execvpe + - os.spawnl + - os.spawnle + - os.spawnlp + - os.spawnlpe + - os.spawnv + - os.spawnve + - os.spawnvp + - os.spawnvpe + - os.startfile + shell: + - os.system + - os.popen + - os.popen2 + - os.popen3 + - os.popen4 + - popen2.popen2 + - popen2.popen3 + - popen2.popen4 + - popen2.Popen3 + - popen2.Popen4 + - commands.getoutput + - commands.getstatusoutput + subprocess: + - subprocess.Popen + - subprocess.call + - subprocess.check_call + - subprocess.check_output + - subprocess.run +start_process_with_no_shell: + no_shell: + - os.execl + - os.execle + - os.execlp + - os.execlpe + - os.execv + - os.execve + - os.execvp + - os.execvpe + - os.spawnl + - os.spawnle + - os.spawnlp + - os.spawnlpe + - os.spawnv + - os.spawnve + - os.spawnvp + - os.spawnvpe + - os.startfile + shell: + - os.system + - os.popen + - os.popen2 + - os.popen3 + - os.popen4 + - popen2.popen2 + - popen2.popen3 + - popen2.popen4 + - popen2.Popen3 + - popen2.Popen4 + - commands.getoutput + - commands.getstatusoutput + subprocess: + - subprocess.Popen + - subprocess.call + - subprocess.check_call + - subprocess.check_output + - subprocess.run +start_process_with_partial_path: + no_shell: + - os.execl + - os.execle + - os.execlp + - os.execlpe + - os.execv + - os.execve + - os.execvp + - os.execvpe + - os.spawnl + - os.spawnle + - os.spawnlp + - os.spawnlpe + - os.spawnv + - os.spawnve + - os.spawnvp + - os.spawnvpe + - os.startfile + shell: + - os.system + - os.popen + - os.popen2 + - os.popen3 + - os.popen4 + - popen2.popen2 + - popen2.popen3 + - popen2.popen4 + - popen2.Popen3 + - popen2.Popen4 + - commands.getoutput + - commands.getstatusoutput + subprocess: + - subprocess.Popen + - subprocess.call + - subprocess.check_call + - subprocess.check_output + - subprocess.run +subprocess_popen_with_shell_equals_true: + no_shell: + - os.execl + - os.execle + - os.execlp + - os.execlpe + - os.execv + - os.execve + - os.execvp + - os.execvpe + - os.spawnl + - os.spawnle + - os.spawnlp + - os.spawnlpe + - os.spawnv + - os.spawnve + - os.spawnvp + - os.spawnvpe + - os.startfile + shell: + - os.system + - os.popen + - os.popen2 + - os.popen3 + - os.popen4 + - popen2.popen2 + - popen2.popen3 + - popen2.popen4 + - popen2.Popen3 + - popen2.Popen4 + - commands.getoutput + - commands.getstatusoutput + subprocess: + - subprocess.Popen + - subprocess.call + - subprocess.check_call + - subprocess.check_output + - subprocess.run +subprocess_without_shell_equals_true: + no_shell: + - os.execl + - os.execle + - os.execlp + - os.execlpe + - os.execv + - os.execve + - os.execvp + - os.execvpe + - os.spawnl + - os.spawnle + - os.spawnlp + - os.spawnlpe + - os.spawnv + - os.spawnve + - os.spawnvp + - os.spawnvpe + - os.startfile + shell: + - os.system + - os.popen + - os.popen2 + - os.popen3 + - os.popen4 + - popen2.popen2 + - popen2.popen3 + - popen2.popen4 + - popen2.Popen3 + - popen2.Popen4 + - commands.getoutput + - commands.getstatusoutput + subprocess: + - subprocess.Popen + - subprocess.call + - subprocess.check_call + - subprocess.check_output + - subprocess.run +try_except_continue: + check_typed_exception: false +try_except_pass: + check_typed_exception: false +weak_cryptographic_key: + weak_key_size_dsa_high: 1024 + weak_key_size_dsa_medium: 2048 + weak_key_size_ec_high: 160 + weak_key_size_ec_medium: 224 + weak_key_size_rsa_high: 1024 + weak_key_size_rsa_medium: 2048 diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..70bd741 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,6 @@ +version: "2" +plugins: + bandit: + enabled: true + sonar-python: + enabled: true diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 2f2bee2..0000000 --- a/.coveragerc +++ /dev/null @@ -1,11 +0,0 @@ -[run] -branch = True -source = sqlalchemy_bind_manager -concurrency = multiprocessing -parallel = true - -[report] -exclude_lines = - pragma: no cover - pass - \.\.\. diff --git a/.github/workflows/github-pages-dev.yml b/.github/workflows/github-pages-dev.yml new file mode 100644 index 0000000..83e3053 --- /dev/null +++ b/.github/workflows/github-pages-dev.yml @@ -0,0 +1,16 @@ +name: Deploy static content to Pages + +on: + push: + branches: ["main"] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + + +jobs: + site: + permissions: + contents: write + uses: ./.github/workflows/reusable-github-pages.yml + with: + site-version: "dev" diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml deleted file mode 100644 index 884ab66..0000000 --- a/.github/workflows/github-pages.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Deploy static content to Pages - -on: - push: - branches: ["main"] - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Pages - uses: actions/configure-pages@v3 - - name: Set up Python 3.12 - uses: actions/setup-python@v3 - with: - python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry - poetry config virtualenvs.create false - poetry install --no-root --with dev - - name: Build static pages - run: make docs-build - - name: Upload artifact - uses: actions/upload-pages-artifact@v1 - with: - path: './site' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v2 diff --git a/.github/workflows/python-3.10.yml b/.github/workflows/python-3.10.yml deleted file mode 100644 index 723d52f..0000000 --- a/.github/workflows/python-3.10.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Python 3.10 - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry - poetry config virtualenvs.create false - poetry install --no-root --with dev - - name: Test with pytest - run: | - make ci-test - - name: Check typing - run: | - make typing diff --git a/.github/workflows/python-3.11.yml b/.github/workflows/python-3.11.yml deleted file mode 100644 index d4c8fbb..0000000 --- a/.github/workflows/python-3.11.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Python 3.11 - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry - poetry config virtualenvs.create false - poetry install --no-root --with dev - - name: Test with pytest - run: | - make ci-test - - name: Check typing - run: | - make typing diff --git a/.github/workflows/python-3.8.yml b/.github/workflows/python-3.8.yml deleted file mode 100644 index 4b5782b..0000000 --- a/.github/workflows/python-3.8.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Python 3.8 - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v3 - with: - python-version: "3.8" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry - poetry config virtualenvs.create false - poetry install --no-root --with dev - - name: Test with pytest - run: | - make ci-test - - name: Check typing - run: | - make typing diff --git a/.github/workflows/python-3.9.yml b/.github/workflows/python-3.9.yml deleted file mode 100644 index 3cae29b..0000000 --- a/.github/workflows/python-3.9.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Python 3.9 - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v3 - with: - python-version: "3.9" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry - poetry config virtualenvs.create false - poetry install --no-root --with dev - - name: Test with pytest - run: | - make ci-test - - name: Check typing - run: | - make typing diff --git a/.github/workflows/python-bandit.yml b/.github/workflows/python-bandit.yml new file mode 100644 index 0000000..62524b7 --- /dev/null +++ b/.github/workflows/python-bandit.yml @@ -0,0 +1,32 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Bandit checks + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + bandit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Security check - Bandit + uses: ioggstream/bandit-report-artifacts@v0.0.2 + with: + project_path: . + config_file: .bandit.yml + + # This is optional + - name: Security check report artifacts + uses: actions/upload-artifact@v1 + with: + name: Security report + path: output/security_report.txt diff --git a/.github/workflows/python-code-style.yml b/.github/workflows/python-code-style.yml index a23ff76..6eafe3a 100644 --- a/.github/workflows/python-code-style.yml +++ b/.github/workflows/python-code-style.yml @@ -8,6 +8,8 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: quality: @@ -16,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml index a95d325..2aef7d3 100644 --- a/.github/workflows/python-lint.yml +++ b/.github/workflows/python-lint.yml @@ -8,6 +8,8 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: quality: @@ -16,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 841b314..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,32 +0,0 @@ -# This workflow will publish a python package on pypi, when a release is created -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Python publish - -on: - release: - types: [published] - -permissions: - contents: read - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.12 - uses: actions/setup-python@v3 - with: - python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry poetry-dynamic-versioning - - name: Publish - env: - POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }} - run: | - poetry build - poetry publish diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index f003e65..a32b2f0 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -8,6 +8,8 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: quality: @@ -16,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/python-3.12.yml b/.github/workflows/python-tests.yml similarity index 65% rename from .github/workflows/python-3.12.yml rename to .github/workflows/python-tests.yml index 7057be2..b12d476 100644 --- a/.github/workflows/python-3.12.yml +++ b/.github/workflows/python-tests.yml @@ -1,24 +1,29 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: Python 3.12 +name: Python tests on: push: branches: [ "main" ] pull_request: branches: [ "main" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: test: - runs-on: ubuntu-latest - + strategy: + matrix: + version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - - name: Set up Python 3.12 - uses: actions/setup-python@v3 + - name: Set up Python ${{ matrix.version }} + uses: actions/setup-python@v4 with: - python-version: "3.12" + python-version: "${{ matrix.version }}" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6066fbe --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,86 @@ +# This workflow will publish a python package on pypi, when a release is created + +name: release + +on: + release: + types: [ published ] + +jobs: + build: + outputs: + version: ${{ steps.docs-version-step.outputs.version }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install poetry poetry-dynamic-versioning + + - name: Build package + run: | + poetry build + + - name: Archive the dist folder + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist + retention-days: 1 + + - name: Export version for site docs + id: docs-version-step + run: | + ./scripts/docs-version.sh + echo "Identified version: $(./scripts/docs-version.sh)" + echo "version=$(./scripts/docs-version.sh)" + echo "version=$(./scripts/docs-version.sh)" >> $GITHUB_OUTPUT + + publish: + runs-on: ubuntu-latest + needs: build + permissions: + contents: write + id-token: write + + steps: + - name: Download the dist folder from the build job + uses: actions/download-artifact@v3 + with: + name: dist + path: dist + + - name: Upload binaries to release + uses: shogo82148/actions-upload-release-asset@v1 + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: dist/* + + - name: Publish package distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + ################################ + # REMOVE CUSTOM REPOSITORY TO # + # PUBLISH ON OFFICIAL PYPI # + ################################ + with: + repository-url: https://test.pypi.org/legacy/ + + site: + needs: build + uses: ./.github/workflows/reusable-github-pages.yml + permissions: + contents: write + with: + site-version: ${{ needs.build.outputs.version }} + version-alias: "stable" + set-default: true diff --git a/.github/workflows/reusable-github-pages.yml b/.github/workflows/reusable-github-pages.yml new file mode 100644 index 0000000..bde5894 --- /dev/null +++ b/.github/workflows/reusable-github-pages.yml @@ -0,0 +1,82 @@ +on: + workflow_call: + inputs: + site-version: + required: true + type: string + version-alias: + required: false + type: string + default: "" + branch: + required: false + type: string + default: "gh-pages" + set-default: + required: false + type: boolean + default: false + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +# NOTE: There's no option to not cancel pending jobs, but we should be able to avoid race conditions on +# the published gh-pages branch anyway. The expectation is to have at maximum one running process +# (after merging to main) and one release process waiting. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build_deploy_pages: + runs-on: ubuntu-latest + environment: + name: github-pages + + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install poetry + poetry config virtualenvs.create false + poetry install --no-root --with dev + + - name: Configure Git user + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + - name: Make sure previous versions are available to mike + run: | + git fetch origin gh-pages --depth=1 + + - name: Build and deploy static pages + run: | + mike deploy ${{ inputs.site-version }} ${{ inputs.version-alias }} --update-aliases --push --branch ${{ inputs.branch }} + + - name: Set default site version + if: ${{ inputs.set-default }} + run: | + mike set-default ${{ inputs.site-version }} --push --branch ${{ inputs.branch }} + + # `mike` is specifically built to be used together with GitHub pages. + # To upload the website to another service (i.e. AWS S3) uncomment + # the following step to download the rendered HTML documentation to ./site directory. + # You'll need to implement the upload steps for your provider. + +# - name: Download artifact to ./site +# run: | +# rm -rf ./site +# git archive -o site.tar ${{ inputs.branch }} +# mkdir -p ./site +# tar xf site.tar -C ./site diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ae11d1a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Contributing + +Hi there! I'm thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. + +Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the project's open source license. + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. + +## Submitting a pull request + +0. Fork and clone the repository +0. Install poetry: `pip install -g poetry` +0. Configure and install the dependencies: `make dev-dependencies` +0. Make sure the tests pass on your machine: `make check` +0. Create a new branch: `git checkout -b my-branch-name` +0. Make your change, add tests, and make sure the tests still pass +0. Push to your fork and submit a pull request +0. Pat your self on the back and wait for your pull request to be reviewed and merged. + +Here are a few things you can do that will increase the likelihood of your pull request being accepted: + +- Follow standards for style and code quality. +- Write tests. +- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. +- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). + +## Resources + +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) +- [GitHub Help](https://help.github.com) diff --git a/LICENSE b/LICENSE index 79f403a..c891365 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Federico Busetti +Copyright (c) 2024 Federico Busetti Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index c987b6c..791d4d6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: docs docs-build +.PHONY: docs test: poetry run pytest -n auto --cov @@ -18,6 +18,9 @@ format: lint: poetry run ruff . +bandit: + poetry run bandit -c .bandit.yml -r . + format-fix: poetry run black . @@ -25,13 +28,13 @@ lint-fix: poetry run ruff . --fix dev-dependencies: + poetry install --with dev --no-root + +update-dependencies: poetry update --with dev fix: format-fix lint-fix -check: typing test format lint +check: typing format lint test bandit docs: poetry run mkdocs serve - -docs-build: - poetry run mkdocs build diff --git a/README.md b/README.md index 6d1b1d6..7267a8f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@ # SQLAlchemy bind manager +![Static Badge](https://img.shields.io/badge/Python-3.8_%7C_3.9_%7C_3.10_%7C_3.11-blue?logo=python&logoColor=white) [![Stable Version](https://img.shields.io/pypi/v/sqlalchemy-bind-manager?color=blue)](https://pypi.org/project/sqlalchemy-bind-manager/) [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta) -[![Python 3.8](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.8.yml/badge.svg?event=push)](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.8.yml) -[![Python 3.9](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.9.yml/badge.svg?event=push)](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.9.yml) -[![Python 3.10](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.10.yml/badge.svg?event=push)](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.10.yml) -[![Python 3.11](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.11.yml/badge.svg?event=push)](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.11.yml) -[![Python 3.12](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.12.yml/badge.svg?event=push)](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-3.12.yml) - +[![Python tests](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-tests.yml/badge.svg?branch=main)](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-tests.yml) +[![Bandit checks](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-bandit.yml/badge.svg?branch=main)](https://github.com/febus982/sqlalchemy-bind-manager/actions/workflows/python-bandit.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/0140f7f4e559ae806887/maintainability)](https://codeclimate.com/github/febus982/sqlalchemy-bind-manager/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/0140f7f4e559ae806887/test_coverage)](https://codeclimate.com/github/febus982/sqlalchemy-bind-manager/test_coverage) + [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json)](https://github.com/charliermarsh/ruff) +[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit) This package provides an easy way to configure and use SQLAlchemy engines and sessions without depending on frameworks. diff --git a/docs/.pages b/docs/.pages new file mode 100644 index 0000000..49a7e49 --- /dev/null +++ b/docs/.pages @@ -0,0 +1,12 @@ +nav: + - Home: index.md + - Bind manager: + - Configuration: manager/config.md + - Models setup: manager/models.md + - Session usage: manager/session.md + - Alembic integration: manager/alembic.md + - Repository: + - Repository usage: repository/usage.md + - Unit of work: repository/uow.md + - Components life cycle: lifecycle.md + - ... diff --git a/mkdocs-overrides/main.html b/mkdocs-overrides/main.html new file mode 100644 index 0000000..0af326a --- /dev/null +++ b/mkdocs-overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block outdated %} + You're not viewing the latest version. + + Click here to go to latest. + +{% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index d18a1e4..52d3224 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,20 @@ docs_dir: docs/ repo_name: 'febus982/sqlalchemy-bind-manager' repo_url: 'https://github.com/febus982/sqlalchemy-bind-manager' +plugins: + - search + - awesome-pages + - mike + - gen-files: + scripts: + - scripts/gen_pages.py # or any other name or path + - mkdocstrings: + handlers: + python: + options: + docstring_style: sphinx + docstring_section_style: spacy + theme: name: material features: @@ -15,21 +29,28 @@ theme: - content.code.copy palette: - - # Palette toggle for light mode - - scheme: default - primary: teal + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" toggle: - icon: material/brightness-7 + icon: material/brightness-auto name: Switch to dark mode # Palette toggle for dark mode - scheme: slate + media: "(prefers-color-scheme: dark)" primary: teal toggle: icon: material/brightness-4 name: Switch to light mode + # Palette toggle for light mode + - scheme: default + media: "(prefers-color-scheme: light)" + primary: teal + toggle: + icon: material/brightness-7 + name: Switch to auto mode + extra: social: - icon: fontawesome/brands/github @@ -37,17 +58,6 @@ extra: - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/federico-b-a0b78232 -nav: - - Home: index.md - - Bind manager: - - Configuration: manager/config.md - - Models setup: manager/models.md - - Session usage: manager/session.md - - Alembic integration: manager/alembic.md - - Repository: - - Repository usage: repository/usage.md - - Unit of work: repository/uow.md - - Components life cycle: lifecycle.md markdown_extensions: - pymdownx.details diff --git a/pyproject.toml b/pyproject.toml index e1f4140..f13691f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,18 +47,23 @@ optional = true [tool.poetry.group.dev.dependencies] aiosqlite = ">=0.18.0" +bandit = ">=1.7.7" coverage = ">=6.5.0" black = ">=22.10.0" +mike = ">=2.0.0" mkdocs = ">=1.4.3" +mkdocstrings = { version = ">=0.24.0", extras = ["python"] } +mkdocs-gen-files = ">=0.5.0" mkdocs-material = ">=9.1.16" mypy = ">=0.990" +pymdown-extensions = ">=10.0.1" pytest = ">=7.2.0" pytest-asyncio = ">=0.20.3" pytest-cov = ">=4.0.0" pytest-factoryboy = ">=2.5.0" pytest-xdist = ">=3.0.2" ruff = ">=0.0.263" -pymdown-extensions = ">=10.0.1" +mkdocs-awesome-pages-plugin = "^2.9.2" [tool.pytest.ini_options] asyncio_mode = "auto" @@ -68,6 +73,20 @@ testpaths = [ "tests", ] +[tool.coverage.run] +branch = true +source = ["sqlalchemy_bind_manager"] +concurrency = ["multiprocessing"] +parallel = true + +[tool.coverage.report] +fail_under = 100 +exclude_also = [ + "pragma: no cover", + "pass", + "\\.\\.\\.", + ] + [tool.mypy] files = "sqlalchemy_bind_manager" plugins = "pydantic.mypy" @@ -81,12 +100,7 @@ extend-exclude = ["docs"] "repository.py" = ["F401"] [tool.black] -files = ''' -( - sqlalchemy_bind_manager - tests -) -''' +target-version = ["py38", "py39", "py310", "py311", "py312"] extend-exclude = ''' ( /docs diff --git a/scripts/docs-version.sh b/scripts/docs-version.sh new file mode 100755 index 0000000..a94be72 --- /dev/null +++ b/scripts/docs-version.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +VERSION=$(poetry version -s) +SEMVER=( ${VERSION//./ } ) +echo "${SEMVER[0]}.${SEMVER[1]}" diff --git a/scripts/gen_pages.py b/scripts/gen_pages.py new file mode 100644 index 0000000..29cf4ca --- /dev/null +++ b/scripts/gen_pages.py @@ -0,0 +1,51 @@ +# -----------------------------------------------------# +# Library imports # +# -----------------------------------------------------# +from pathlib import Path + +import mkdocs_gen_files + +# -----------------------------------------------------# +# Configuration # +# -----------------------------------------------------# +# Package source code relative path +src_dir = "sqlalchemy_bind_manager" +# Generated pages will be grouped in this nav folder +nav_pages_path = "API-Reference" + + +# -----------------------------------------------------# +# Runner # +# -----------------------------------------------------# +""" Generate code reference pages and navigation + + Based on the recipe of mkdocstrings: + https://github.com/mkdocstrings/mkdocstrings + https://github.com/mkdocstrings/mkdocstrings/issues/389#issuecomment-1100735216 + + Credits: + Timothée Mazzucotelli + https://github.com/pawamoy +""" +# Iterate over each Python file +for path in sorted(Path(src_dir).rglob("*.py")): + # Get path in module, documentation and absolute + module_path = path.relative_to(src_dir).with_suffix("") + doc_path = path.relative_to(src_dir).with_suffix(".md") + full_doc_path = Path(nav_pages_path, doc_path) + + # Handle edge cases + parts = (src_dir,) + tuple(module_path.parts) + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1] == "__main__": + continue + + # Write docstring documentation to disk via parser + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"::: {ident}") + # Update parser + mkdocs_gen_files.set_edit_path(full_doc_path, path) diff --git a/sqlalchemy_bind_manager/_repository/result_presenters.py b/sqlalchemy_bind_manager/_repository/result_presenters.py index 2047b43..6bb2fe6 100644 --- a/sqlalchemy_bind_manager/_repository/result_presenters.py +++ b/sqlalchemy_bind_manager/_repository/result_presenters.py @@ -121,18 +121,22 @@ def _build_before_cursor_result( total_items=total_items_count, has_previous_page=has_previous_page, has_next_page=has_next_page, - start_cursor=CursorReference( - column=reference_column, - value=getattr(result_items[0], reference_column), - ) - if result_items - else None, - end_cursor=CursorReference( - column=reference_column, - value=getattr(result_items[-1], reference_column), - ) - if result_items - else None, + start_cursor=( + CursorReference( + column=reference_column, + value=getattr(result_items[0], reference_column), + ) + if result_items + else None + ), + end_cursor=( + CursorReference( + column=reference_column, + value=getattr(result_items[-1], reference_column), + ) + if result_items + else None + ), ), ) @@ -164,18 +168,22 @@ def _build_after_cursor_result( total_items=total_items_count, has_previous_page=has_previous_page, has_next_page=has_next_page, - start_cursor=CursorReference( - column=reference_column, - value=getattr(result_items[0], reference_column), - ) - if result_items - else None, - end_cursor=CursorReference( - column=reference_column, - value=getattr(result_items[-1], reference_column), - ) - if result_items - else None, + start_cursor=( + CursorReference( + column=reference_column, + value=getattr(result_items[0], reference_column), + ) + if result_items + else None + ), + end_cursor=( + CursorReference( + column=reference_column, + value=getattr(result_items[-1], reference_column), + ) + if result_items + else None + ), ), ) diff --git a/tests/repository/test_repository_lifecycle.py b/tests/repository/test_repository_lifecycle.py index c1a005f..466e0cb 100644 --- a/tests/repository/test_repository_lifecycle.py +++ b/tests/repository/test_repository_lifecycle.py @@ -27,11 +27,9 @@ def test_repository_fails_if_both_bind_and_session(repository_class, model_class def test_repository_fails_if_no_model_or_wrong_model(repository_class, sa_bind): - class ExtendedClassRepo(repository_class): - ... + class ExtendedClassRepo(repository_class): ... - class SomeObject: - ... + class SomeObject: ... with pytest.raises(InvalidModel): ExtendedClassRepo(bind=sa_bind)