From f043f2a22da8ee7daa5daadcc9ef6bfe461c4150 Mon Sep 17 00:00:00 2001 From: Wolf Date: Wed, 22 May 2024 16:27:52 +0100 Subject: [PATCH] The initial commit --- .github/CODEOWNERS | 5 + .github/CODE_OF_CONDUCT.md | 75 +++++++ .github/CONTRIBUTING.md | 14 ++ .github/FUNDING.yml | 4 + .github/ISSUE_TEMPLATE/ask_question.yml | 22 +++ .github/ISSUE_TEMPLATE/bug_report.yml | 58 ++++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 43 ++++ .github/PULL_REQUEST_TEMPLATE.md | 36 ++++ .github/SECURITY.md | 40 ++++ .github/dependabot.yml | 33 ++++ .github/scripts/check-jobs.sh | 38 ++++ .github/workflows/cicd.yml | 178 +++++++++++++++++ .github/workflows/citation-validation.yml | 84 ++++++++ .github/workflows/codeql.yml | 70 +++++++ .../workflows/delete-old-workflow-runs.yml | 75 +++++++ .github/workflows/dependabot.yml | 53 +++++ .github/workflows/document-validation.yml | 102 ++++++++++ .github/workflows/generate-release.yml | 109 +++++++++++ .github/workflows/generate-test-release.yml | 108 +++++++++++ .github/workflows/greetings.yml | 27 +++ .../purge-deprecated-workflow-runs.yml | 47 +++++ .github/workflows/repository-validation.yml | 100 ++++++++++ .github/workflows/security-hardening.yml | 33 ++++ .github/workflows/stale.yml | 57 ++++++ .gitignore | 183 ++++++++++++++++++ .yamllint | 26 +++ CITATION.cff | 15 ++ LICENSE.md | 25 +++ README.md | 149 ++++++++++++++ requirements.txt | 4 + setup.cfg | 33 ++++ setup.py | 61 ++++++ wolfsoftware/ttfb/__init.py__ | 11 ++ wolfsoftware/ttfb/cli.py | 138 +++++++++++++ wolfsoftware/ttfb/config.py | 57 ++++++ wolfsoftware/ttfb/constants.py | 32 +++ wolfsoftware/ttfb/exceptions.py | 21 ++ wolfsoftware/ttfb/globals.py | 36 ++++ wolfsoftware/ttfb/main.py | 42 ++++ wolfsoftware/ttfb/notify.py | 81 ++++++++ wolfsoftware/ttfb/process.py | 91 +++++++++ wolfsoftware/ttfb/results.py | 37 ++++ wolfsoftware/ttfb/utils.py | 87 +++++++++ 44 files changed, 2548 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/ask_question.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100755 .github/scripts/check-jobs.sh create mode 100644 .github/workflows/cicd.yml create mode 100644 .github/workflows/citation-validation.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/delete-old-workflow-runs.yml create mode 100644 .github/workflows/dependabot.yml create mode 100644 .github/workflows/document-validation.yml create mode 100644 .github/workflows/generate-release.yml create mode 100644 .github/workflows/generate-test-release.yml create mode 100644 .github/workflows/greetings.yml create mode 100644 .github/workflows/purge-deprecated-workflow-runs.yml create mode 100644 .github/workflows/repository-validation.yml create mode 100644 .github/workflows/security-hardening.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .gitignore create mode 100644 .yamllint create mode 100644 CITATION.cff create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 wolfsoftware/ttfb/__init.py__ create mode 100644 wolfsoftware/ttfb/cli.py create mode 100644 wolfsoftware/ttfb/config.py create mode 100644 wolfsoftware/ttfb/constants.py create mode 100644 wolfsoftware/ttfb/exceptions.py create mode 100644 wolfsoftware/ttfb/globals.py create mode 100644 wolfsoftware/ttfb/main.py create mode 100644 wolfsoftware/ttfb/notify.py create mode 100644 wolfsoftware/ttfb/process.py create mode 100644 wolfsoftware/ttfb/results.py create mode 100644 wolfsoftware/ttfb/utils.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..50be806 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# +# These owners will be the default owners for everything in the repo. +# +* @TGWolf + diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5ffa3a8 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behaviour that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behaviour by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behaviour and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behaviour. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org), version 1.4, +available at + +For answers to common questions about this code of conduct, see + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..145a864 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +Please refer to the +[contributing](https://github.com/WolfSoftware/contributing) +documentation. + +## Important + +ALL commits must be signed to ensure the identity of the developer, any pull +requests that are made with unsigned commits will be rejected as a matter of +course. + +> This project has a [code of conduct](CODE_OF_CONDUCT.md). By interacting +with this repository, organization, or community you agree to abide by its terms. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b7a1e2f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# Funding +# https://help.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository + +github: [WolfSoftware,TGWolf] diff --git a/.github/ISSUE_TEMPLATE/ask_question.yml b/.github/ISSUE_TEMPLATE/ask_question.yml new file mode 100644 index 0000000..5216f98 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask_question.yml @@ -0,0 +1,22 @@ +name: Ask a question +description: If you don't have a specific issue or bug to report you can still ask us questions and we will do our best to answer them +title: "[Question]: " +labels: ["type: question", "state: triage"] +assignees: + - tgwolf +body: + - type: textarea + id: question + attributes: + label: What is your question? + description: Please give us time to review your question and formulate an answer. + validations: + required: true + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlatformEngineersToolbox/time-to-first-byte-package/blob/master/.github/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..cb3e06b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: Report a bug +description: Found a bug? Let us knonw what the issue is and we will attempt to fix it +title: "[Bug]: " +labels: ["type: bug", "state: triage"] +assignees: + - tgwolf +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: How do we reproduct the bug? + description: What are the steps we need to take to reproduce the behavior? + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: false + - type: textarea + id: screenshoots + attributes: + label: Screeenshots + description: Upload any screenshots that might help demonstrate the bug. + validations: + required: false + - type: textarea + id: additional-information + attributes: + label: Additional information + description: Please provide any additional information that you think will help us to resolve this bug. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlatformEngineersToolbox/time-to-first-byte-package/blob/master/.github/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..cabd7e8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support us + url: https://ko-fi.com/wolfsoftware + about: Show your support + - name: Visit our website + url: https://wolfsoftware.com/ + about: Visit the Wolf Software website and see what else we do and what services we offer diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..74f78e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +name: Request a new feature +description: Got an idea for a new feature? Let us know what you want and we will see if we can add it +title: "[Feature Request]: " +labels: ["type: feature", "state: triage"] +assignees: + - tgwolf +body: + - type: textarea + id: releated-to + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of what the problem is. E.g. I'm always frustrated when ... + validations: + required: true + - type: textarea + id: suggested-solution + attributes: + label: Suggested Solution + description: A clear and concise description of what you want to see implemented. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + id: additional-information + attributes: + label: Additional information + description: Please provide any additional information that you think will help us to resolve this bug. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlatformEngineersToolbox/time-to-first-byte-package/blob/master/.github/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0e99242 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ +# Thank You + +Thanks for submitting a pull request! Please provide enough information so that +others can review your pull request: + +## Summary + + + +This PR fixes/implements the following **bugs/features** + +* [ ] Bug 1 +* [ ] Feature 1 +* [ ] Breaking changes + + + +Explain the **motivation** for making this change. What existing problem does +the pull request solve? + + + +## Test plan (required) + +Demonstrate the code is solid. Example: The exact commands you ran and their +output, screenshots help greatly. + + + +## Closing issues + + +Fixes # diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..b46843b --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for this project. + +* [Reporting a Bug](#reporting-a-bug) +* [Disclosure Policy](#disclosure-policy) +* [Comments on this Policy](#comments-on-this-policy) + +## Reporting a Bug + +We take **ALL** security related bugs and issues very seriously. + +If you think you have identified a security related issue, please +[report it immediately](mailto:disclose@wolfsoftware.com) and include +the word "SECURITY" in the subject line. If you are not sure, don’t worry. +Better safe than sorry – just send an email. + +* Please provide as much information as you can. +* Please do not open issues related to any security concerns publicly. +* Please do not include anyone else on the disclosure email. + +Report security bugs in third-party modules to the person or team maintaining +the module. + +## Disclosure Policy + +When a security report is received, we will carry out the following steps: + +* Confirm the problem and determine the affected versions. +* Audit code to find any potential similar problems. +* Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible. + +We will endeavour to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1cc4d72 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 10 + commit-message: + prefix: "chore:" + labels: + - "dependabot: ecosystem : github actions" + - "dependabot: dependencies" + assignees: + - "TGWolf" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + time: "04:00" + day: "monday" + open-pull-requests-limit: 10 + commit-message: + prefix: "chore:" + labels: + - "dependabot: ecosystem : python" + - "dependabot: dependencies" + assignees: + - "TGWolf" + diff --git a/.github/scripts/check-jobs.sh b/.github/scripts/check-jobs.sh new file mode 100755 index 0000000..e35587c --- /dev/null +++ b/.github/scripts/check-jobs.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# This script receives a JSON string containing job results and checks for any failures. + +# Check if jq is available +if ! command -v jq &> /dev/null; then + echo "jq could not be found, please install jq to run this script." + exit 1 +fi + +# Read the JSON string from the first script argument +job_results_json=$1 + +# Check if the job results JSON is not empty +if [[ -z "$job_results_json" ]]; then + echo "No job results JSON provided." + exit 1 +fi + +# Set default state +failed_jobs=false + +# Use jq to parse the JSON and check each job's result +while IFS= read -r line; do + job_name=$(echo "$line" | awk '{print $1}') + result=$(echo "$line" | awk '{print $3}') + + if [ "$result" != "success" ]; then + echo "$job_name failed." + failed_jobs=true + else + echo "$job_name succeed." + fi +done <<< "$( echo "$job_results_json" | jq -r 'to_entries[] | "\(.key) result: \(.value.result)"' )" + +if [ "$failed_jobs" = true ] ; then + exit 1 +fi diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..de6b440 --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,178 @@ +name: CI/CD Pipeline + +on: + push: + branches-ignore: + - 'dependabot/**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + + pull_request: + branches: + - '**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-python-versions: + name: Get Python Versions (>= 3.9) + runs-on: ubuntu-latest + outputs: + version-matrix: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Versions + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "python" + min-version: 3.9 + remove-patch-version: true + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Perform ShellCheck Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/shellcheck/master/pipeline.sh) + + bandit: + name: Bandit + needs: get-python-versions + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-versions: ${{ fromJson(needs.get-python-versions.outputs.version-matrix) }} + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Python ${{ matrix.python-versions }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ matrix.python-versions }} + + - name: Perform Bandit Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/bandit/master/pipeline.sh) + + pycodestyle: + name: Pycodestyle + needs: get-python-versions + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-versions: ${{ fromJson(needs.get-python-versions.outputs.version-matrix) }} + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Python ${{ matrix.python-versions }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ matrix.python-versions }} + + - name: Perform Pycodestyle Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/pycodestyle/master/pipeline.sh) + + pydocstyle: + name: Pydocstyle + needs: get-python-versions + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-versions: ${{ fromJson(needs.get-python-versions.outputs.version-matrix) }} + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Python ${{ matrix.python-versions }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ matrix.python-versions }} + + - name: Perform Pydocstyle Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/pydocstyle/master/pipeline.sh) + + pylama: + name: Pylama + needs: get-python-versions + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-versions: ${{ fromJson(needs.get-python-versions.outputs.version-matrix) }} + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Python ${{ matrix.python-versions }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ matrix.python-versions }} + + - name: Perform Pylama Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/pylama/master/pipeline.sh) + + pylint: + name: Pylint + needs: get-python-versions + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-versions: ${{ fromJson(needs.get-python-versions.outputs.version-matrix) }} + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Python ${{ matrix.python-versions }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ matrix.python-versions }} + + - name: Perform Pylint Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/pylint/master/pipeline.sh) + + cicd-pipeline: + if: always() + name: CI/CD Pipeline + needs: + - shellcheck + - bandit + - pycodestyle + - pydocstyle + - pylama + - pylint + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/citation-validation.yml b/.github/workflows/citation-validation.yml new file mode 100644 index 0000000..6ce137d --- /dev/null +++ b/.github/workflows/citation-validation.yml @@ -0,0 +1,84 @@ +name: Citation Validation + +on: + push: + branches-ignore: + - 'dependabot/**' + paths: + - 'CITATION.cff' + pull_request: + branches: + - '**' + paths: + - 'CITATION.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-ruby-version: + name: Get Latest Ruby Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "ruby" + highest-only: true + remove-patch-version: true + + awesomebot: + name: Awesomebot + needs: get-ruby-version + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Ruby ${{ needs.get-ruby-version.outputs.version }} + uses: ruby/setup-ruby@7dc18ff0ca6e3630d3f29d2a85ebf6cc27ae9d6c # v1.177.0 + with: + ruby-version: ${{ needs.get-ruby-version.outputs.version }} + + - name: Perform Awesomebot Analysis + env: + FLAGS: "default" + WHITELIST: "https://img.shields.io" + INCLUDE_FILES: "CITATION.cff" + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/awesomebot/master/pipeline.sh) + + validate-citation-file: + name: Validate CITATION.cff + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Validate CITATION.cff + uses: citation-file-format/cffconvert-github-action@4cf11baa70a673bfdf9dad0acc7ee33b3f4b6084 # v2.0.0 + with: + args: "--validate" + + citation-validation-pipeline: + if: always() + name: Citation Validation Pipeline + needs: + - awesomebot + - validate-citation-file + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e2492c0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,70 @@ +name: CodeQL + +on: + push: + branches-ignore: + - 'dependabot/**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + + pull_request: + branches: + - '**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + + schedule: + - cron: '26 4 * * 1' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + codeql-analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + languages: ['python'] + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@a57c67b89589d2d13d5ac85a9fc4679c7539f94c # v2.17.3 + with: + languages: ${{ matrix.languages }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@a57c67b89589d2d13d5ac85a9fc4679c7539f94c # v2.17.3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@a57c67b89589d2d13d5ac85a9fc4679c7539f94c # v2.17.3 + + slack-workflow-status: + if: always() + name: Slack Post Workflow Notification + needs: + - codeql-analyze + runs-on: ubuntu-latest + + steps: + - name: Slack Workflow Notifications + if: ${{ github.event_name == 'schedule' && needs.codeql-analyze.result != 'success' }} + uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a # v1.3.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + include_jobs: on-failure + include_commit_message: true diff --git a/.github/workflows/delete-old-workflow-runs.yml b/.github/workflows/delete-old-workflow-runs.yml new file mode 100644 index 0000000..f3cfbe5 --- /dev/null +++ b/.github/workflows/delete-old-workflow-runs.yml @@ -0,0 +1,75 @@ +name: Delete Old Workflow Runs + +on: + workflow_dispatch: + inputs: + days: + description: 'Number of days to retain workflow runs.' + required: true + default: '30' + minimum-runs: + description: 'The minimum number of runs to keep for each workflow.' + required: true + default: '6' + + schedule: + - cron: '19 4 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DAYS: 30 + MINIMUM_RUNS: 6 + +permissions: read-all + +jobs: + set-runtime-values: + name: Set Runtime Values + runs-on: ubuntu-latest + outputs: + days: ${{ steps.set-output-defaults.outputs.days }} + minimum-runs: ${{ steps.set-output-defaults.outputs.minimum-runs }} + + steps: + - name: Set Runtime Values + id: set-output-defaults + run: | + echo "days=${{ github.event.inputs.days || env.DAYS }}" >> "${GITHUB_OUTPUT}" + echo "minimum-runs=${{ github.event.inputs.minimum-runs || env.MINIMUM_RUNS }}" >> "${GITHUB_OUTPUT}" + + delete-old-workflows: + name: Delete Old Workflow Runs + runs-on: ubuntu-latest + permissions: + actions: write + needs: + - set-runtime-values + + steps: + - name: Delete Old Workflow Runs + uses: Mattraks/delete-workflow-runs@39f0bbed25d76b34de5594dceab824811479e5de # v2.0.6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + retain_days: ${{ needs.set-runtime-values.outputs.days }} + keep_minimum_runs: ${{ needs.set-runtime-values.outputs.minimum-runs }} + + slack-workflow-status: + if: always() + name: Slack Post Workflow Notification + needs: + - delete-old-workflows + runs-on: ubuntu-latest + + steps: + - name: Slack Workflow Notifications + if: ${{ github.event_name == 'schedule' && needs.delete-old-workflows.result != 'success'}} + uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a # v1.3.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + include_jobs: on-failure + include_commit_message: true diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 0000000..247ba9d --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,53 @@ +name: Dependabot Pull Request Approve & Merge + +on: pull_request + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + dependabot: + name: Dependabot + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Fetch Metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@5e5f99653a5b510e8555840e80cbf1514ad4af38 # v2.1.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Approve PR + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' || steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor' }} + run: | + gh pr review --approve "${PR_URL}" -b "I'm **approving** this pull request because it includes a patch or minor update" + gh pr edit "${PR_URL}" --add-label "dependabot: auto approve" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-Merge Non-Major Updates + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' || steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor' }} + run: | + gh pr comment "${PR_URL}" --body "I'm automatically merging this PR because it includes a patch or minor update" + gh pr merge --auto --squash --delete-branch "${PR_URL}" + gh pr edit "${PR_URL}" --add-label "dependabot: auto merge" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Comment & Label Major Updates + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-major' }} + run: | + gh pr comment "${PR_URL}" --body "I'm **NOT** automatically merging this PR because it includes a major update of a dependency" + gh pr edit "${PR_URL}" --add-label "dependabot: manual merge" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/document-validation.yml b/.github/workflows/document-validation.yml new file mode 100644 index 0000000..248aaa1 --- /dev/null +++ b/.github/workflows/document-validation.yml @@ -0,0 +1,102 @@ +name: Documentation Validation + +on: + push: + branches-ignore: + - 'dependabot/**' + paths: + - '**/*.md' + pull_request: + branches: + - '**' + paths: + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-node-version: + name: Get Latest Node Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "node" + highest-only: true + remove-patch-version: true + + get-ruby-version: + name: Get Latest Ruby Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "ruby" + highest-only: true + remove-patch-version: true + + awesomebot: + name: Awesomebot + needs: get-ruby-version + runs-on: ubuntu-latest + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Ruby ${{ needs.get-ruby-version.outputs.version }} + uses: ruby/setup-ruby@7dc18ff0ca6e3630d3f29d2a85ebf6cc27ae9d6c # v1.177.0 + with: + ruby-version: ${{ needs.get-ruby-version.outputs.version }} + + - name: Perform Awesomebot Analysis + env: + FLAGS: "default" + WHITELIST: "https://img.shields.io" + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/awesomebot/master/pipeline.sh) + + markdown-lint: + name: Markdown Lint + needs: get-node-version + runs-on: ubuntu-latest + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Node ${{ needs.get-node-version.outputs.version }} + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: ${{ needs.get-node-version.outputs.version }} + + - name: Perform Markdown Lint Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/markdown-lint/master/pipeline.sh) + env: + EXCLUDE_FILES: "README.md" + + repository-validation-pipeline: + if: always() + name: Documentation Validation Pipeline + needs: + - awesomebot + - markdown-lint + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/generate-release.yml b/.github/workflows/generate-release.yml new file mode 100644 index 0000000..d9d3692 --- /dev/null +++ b/.github/workflows/generate-release.yml @@ -0,0 +1,109 @@ +name: Generate a Release + +on: + push: + tags: + - 'v[0-9].[0-9]+.[0-9]+' + - '!v[0-9].[0-9]+.[0-9]+rc[0-9]+' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-python-version: + name: Get Latest Python Version + runs-on: ubuntu-latest + outputs: + highest-version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "python" + highest-only: true + + build-and-publish: + name: Build & publish to PyPI + runs-on: ubuntu-latest + needs: + - get-python-version + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Python ${{ needs.get-python-version.outputs.highest-version }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ needs.get-python-version.outputs.highest-version }} + + - name: Update Pip + run: python -m pip install --upgrade pip + + - name: Install Required Tooling + run: pip install setuptools wheel twine + + - name: Build Binary Wheel & Source Tarball + run: python setup.py sdist bdist_wheel + + - name: Verify Binary Wheel & Source Tarball + run: twine check dist/* + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* + + set-release-version: + name: Set Release Version + runs-on: ubuntu-latest + outputs: + release-version: ${{ steps.set-release-version.outputs.release-version }} + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Set the Release Version + id: set-release-version + run: | + echo "release-version=${GITHUB_REF#refs/*/}" >> "${GITHUB_OUTPUT}" + + create-release: + name: Create a Release + permissions: + contents: write + runs-on: ubuntu-latest + needs: + - set-release-version + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Generate Changelog + uses: Bullrich/generate-release-changelog@6b60f004b4bf12ff271603dc32dbd261965ad2f2 # v2.0.2 + id: Changelog + env: + REPO: ${{ github.repository }} + + - name: Create a Release + id: create_release + uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref }} + name: ${{ needs.set-release-version.outputs.release-version }} + body: ${{ steps.Changelog.outputs.changelog }} + draft: false + prerelease: false diff --git a/.github/workflows/generate-test-release.yml b/.github/workflows/generate-test-release.yml new file mode 100644 index 0000000..a687a4d --- /dev/null +++ b/.github/workflows/generate-test-release.yml @@ -0,0 +1,108 @@ +name: Generate a TEST Release + +on: + push: + tags: + - 'v[0-9].[0-9]+.[0-9]+rc[0-9]+' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-python-version: + name: Get Latest Python Version + runs-on: ubuntu-latest + outputs: + highest-version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "python" + highest-only: true + + + build-and-publish: + name: Build & publish to TestPyPI + runs-on: ubuntu-latest + needs: + - get-python-version + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Set up Python ${{ needs.get-python-version.outputs.highest-version }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ needs.get-python-version.outputs.highest-version }} + + - name: Update Pip + run: python -m pip install --upgrade pip + + - name: Install Required Tooling + run: pip install setuptools wheel twine + + - name: Build Binary Wheel & Source Tarball + run: python setup.py sdist bdist_wheel + + - name: Verify Binary Wheel & Source Tarball + run: twine check dist/* + + - name: Publish to TestPyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} + run: twine upload -r testpypi dist/* + + set-release-version: + name: Set Release Version + runs-on: ubuntu-latest + outputs: + release-version: ${{ steps.set-release-version.outputs.release-version }} + + steps: + - name: Checkout the repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Set the release version + id: set-release-version + run: echo "release-version=${GITHUB_REF#refs/*/}" >> "${GITHUB_OUTPUT}" + + create-release: + name: Create Release + permissions: + contents: write + runs-on: ubuntu-latest + needs: + - set-release-version + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Generate Changelog + uses: Bullrich/generate-release-changelog@6b60f004b4bf12ff271603dc32dbd261965ad2f2 # v2.0.2 + id: Changelog + env: + REPO: ${{ github.repository }} + + - name: Create a Release + id: create_release + uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref }} + name: ${{ needs.set-release-version.outputs.release-version }} + body: ${{ steps.Changelog.outputs.changelog }} + draft: false + prerelease: true diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 0000000..ddd7cd8 --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,27 @@ +name: Greetings + +on: + pull_request: + issues: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + greetings: + name: Handle Greetings + permissions: + issues: write + pull-requests: write + runs-on: ubuntu-latest + + steps: + - name: Handle Greetings + uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 # v1.3.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: "Thank you for raising your first issue - all contributions to this project are welcome!" + pr-message: "Thank you for raising your first pull request - all contributions to this project are welcome!" diff --git a/.github/workflows/purge-deprecated-workflow-runs.yml b/.github/workflows/purge-deprecated-workflow-runs.yml new file mode 100644 index 0000000..f339cbe --- /dev/null +++ b/.github/workflows/purge-deprecated-workflow-runs.yml @@ -0,0 +1,47 @@ +name: Purge Deprecated Workflow Runs + +on: + workflow_dispatch: + + schedule: + - cron: '17 2 * * 1' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + purge-obsolete-workflows: + name: Purge Deprecated Workflow Runs + permissions: + actions: write + runs-on: ubuntu-latest + + steps: + - name: Purge Deprecated Workflow Runs + uses: otto-de/purge-deprecated-workflow-runs@31a4e821d43e9a354cbd65845922c76e4b4b3633 # v 2.0.4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + remove-obsolete: true + remove-cancelled: true + remove-failed: true + remove-skipped: true + + slack-workflow-status: + if: always() + name: Slack Post Workflow Notification + needs: + - purge-obsolete-workflows + runs-on: ubuntu-latest + + steps: + - name: Slack Workflow Notifications + if: ${{ github.event_name == 'schedule' && needs.purge-obsolete-workflows.result != 'success'}} + uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a # v1.3.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + include_jobs: on-failure + include_commit_message: true diff --git a/.github/workflows/repository-validation.yml b/.github/workflows/repository-validation.yml new file mode 100644 index 0000000..8f0c10e --- /dev/null +++ b/.github/workflows/repository-validation.yml @@ -0,0 +1,100 @@ +name: Repository Validation + +on: + push: + branches-ignore: + - 'dependabot/**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + pull_request: + branches: + - '**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-go-version: + name: Get Latest Go Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Versions + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # V0.1.3 + id: get-language-versions + with: + language: "go" + highest-only: true + remove-patch-version: true + + get-python-version: + name: Get Latest Python Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Versions + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # V0.1.3 + id: get-language-versions + with: + language: "python" + highest-only: true + remove-patch-version: true + + action-lint: + name: Action Lint + needs: get-go-version + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # V4.1.6 + + - name: Setup Go ${{ needs.get-go-version.outputs.version }} + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # V5.0.1 + with: + go-version: ${{ needs.get-go-version.outputs.version }} + + - name: Perform Action Lint Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/action-lint/master/pipeline.sh) + + yaml-lint: + name: YAML Lint + needs: get-python-version + runs-on: ubuntu-latest + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # V4.1.6 + + - name: Set up Python ${{ needs.get-python-version.outputs.version }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # V5.1.0 + with: + python-version: ${{ needs.get-python-version.outputs.version }} + + - name: Perform YAML Lint Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/yaml-lint/master/pipeline.sh) + + repository-validation-pipeline: + if: always() + name: Repository Validation Pipeline + needs: + - action-lint + - yaml-lint + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # V4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/security-hardening.yml b/.github/workflows/security-hardening.yml new file mode 100644 index 0000000..4064ce5 --- /dev/null +++ b/.github/workflows/security-hardening.yml @@ -0,0 +1,33 @@ +name: Security Hardening + +on: + push: + branches-ignore: + - 'dependabot/**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + pull_request: + branches: + - '**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + security-hardening: + name: Harden Security + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Ensure SHA Pinned Actions + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@40e45e738b3cad2729f599d8afc6ed02184e1dbd # v3.0.5 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..90047d2 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,57 @@ +name: Stale Issue & PR Handler + +on: + schedule: + - cron: '58 2 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + stale: + name: Handle Stale Issues & PRs + permissions: + contents: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + + steps: + - name: Handle Stale Issues & PRs + uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + id: stale-issues + with: + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' + close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' + days-before-issue-stale: 30 + days-before-issue-close: 5 + stale-issue-label: 'state: stale' + close-issue-label: 'resolution: closed' + exempt-issue-labels: 'state: blocked,state: keep' + stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' + close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' + days-before-pr-stale: 45 + days-before-pr-close: 10 + stale-pr-label: 'state: stale' + close-pr-label: 'resolution: closed' + exempt-pr-labels: 'state: blocked,state: keep' + + slack-workflow-status: + if: always() + name: Slack Post Workflow Notification + needs: + - stale + runs-on: ubuntu-latest + + steps: + - name: Slack Workflow Notifications + if: ${{ github.event_name == 'schedule' && needs.stale.result != 'success'}} + uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a # v1.3.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + include_jobs: on-failure + include_commit_message: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc8f995 --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +### Hard Coded Minimums ### +# +# Because I use a Mac +# +**/.DS-Store + +# +# Ignore Awesomebot output +# +**/ab-results* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..804e6c1 --- /dev/null +++ b/.yamllint @@ -0,0 +1,26 @@ +--- + +extends: default + +rules: + braces: + level: warning + max-spaces-inside: 1 + brackets: + level: warning + max-spaces-inside: 1 + colons: + level: warning + commas: + level: warning + comments-indentation: disable + document-start: disable + empty-lines: + level: warning + hyphens: + level: warning + indentation: + level: warning + indent-sequences: consistent + line-length: disable + truthy: disable diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..4e46df1 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,15 @@ +cff-version: 1.2.0 +message: If you use this software, please cite it using these metadata. +title: Time to First Byte +abstract: Display the time-to-first-byte for any given url. +type: software +version: 0.1.0 +date-released: 2024-05-22 +repository-code: https://github.com/PlatformEngineersToolbox/time-to-first-byte-python +keywords: + - "Wolf Software" + - "Software" +license: MIT +authors: + - family-names: "Wolf" + orcid: "https://orcid.org/0009-0007-0983-2072" diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c14811f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,25 @@ +The MIT License (MIT) +===================== + +Copyright © `2009-2024` `Wolf Software` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8078417 --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ + +

+ + PlatformEngineersToolbox logo + +
+ + Github Build Status + + + License + + + Created + +
+ + Release + + + Released + + + Commits since release + +
+ + + + + + + + + + + + +

+ +## Overview + +This tool is designed to allow you to measure and display the 'Time To First Byte' (ttfb) for a given url. It can also help identify bottlenecks or latency issues that might be causing slow responses. + +We also provide a simple [bash version](https://github.com/PlatformEngineersToolbox/time-to-first-byte) if you prefer that to a python package. + +## Installation + +```shell +pip install wolfsoftware.ttfb +``` + +## Usage + +``` +usage: ttfb [-h] [-d] [-v] [-V] [-m | -f] [-c COUNT] -u URL + +Display the time-to-first-byte for any given url. + +flags: + -h, --help show this help message and exit + -d, --debug Very noisy (default: False) + -v, --verbose Verbose output - show scan results as they come in (default: False) + -V, --version Show program's version number and exit. + +exclusive flags: + -m, --minimal Show minimal set of timing values. (default: False) + -f, --full Show full set of timing values. (default: False) + +optional: + -c COUNT, --count COUNT + How many times to test [1-25] (default: 1) + +required: + -u URL, --url URL The URL to test (default: None) +``` + +## Results Output + +### Single Connection Test + +##### Standard Output (Default) +``` +----------------------------------------------------------------------------------------------------------- + Time to First Byte Test +----------------------------------------------------------------------------------------------------------- + Lookup Time: 0.005087 Connect Time: 0.025123 StartXfer Time (TTFB): 0.111106 Total Time: 0.111173 +----------------------------------------------------------------------------------------------------------- +``` + +##### Minimal Output (-m) + +``` +---------------------------------------------------------- + Time to First Byte Test +---------------------------------------------------------- + StartXfer Time (TTFB): 0.106124 Total Time: 0.106208 +---------------------------------------------------------- +``` + +##### Full Output (-f) + +``` +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Time to First Byte Test +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Lookup Time: 0.004987 Connect Time: 0.022486 AppCon Time: 0.089366 PreXfer Time: 0.089427 Redirect Time: 0.000000 StartXfer Time (TTFB): 0.108404 Total Time: 0.108475 +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +``` + +### Repeated Connection Test + +It is also possible to specify how many connections to make when testing by adding the -c flag. This can be combined with the existing output flags (-m and -f) + +``` +----------------------------------------------------------------------------------------------------------- + Time to First Byte Test +----------------------------------------------------------------------------------------------------------- + Lookup Time: 0.005112 Connect Time: 0.034810 StartXfer Time (TTFB): 0.119705 Total Time: 0.119777 + Lookup Time: 0.005087 Connect Time: 0.023240 StartXfer Time (TTFB): 0.108551 Total Time: 0.108623 + Lookup Time: 0.004158 Connect Time: 0.021478 StartXfer Time (TTFB): 0.110971 Total Time: 0.111036 + Lookup Time: 0.005108 Connect Time: 0.022807 StartXfer Time (TTFB): 0.111455 Total Time: 0.111526 + Lookup Time: 0.004764 Connect Time: 0.022720 StartXfer Time (TTFB): 0.111475 Total Time: 0.111547 +----------------------------------------------------------------------------------------------------------- +``` + +## Timing Key + +| Time | Description | +| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Lookup time (time_namelookup) | The time, in seconds, it took from the start until the name resolving was completed. | +| Connect time (time_connect) | The time, in seconds, it took from the start until the TCP connect to the remote host was completed. | +| AppCon time (time_appconnect) | The time, in seconds, it took from the start until the SSL/SSH/etc connect/handshake to the remote host was completed. | +| PreXfer time (time_pretransfer) | The time, in seconds, it took from the start until the file transfer was just about to begin. This includes all 'pre-transfer' commands and negotiations that are specific to the particular protocol(s) involved. | +| Redirect time (time_redirect) | The time, in seconds, it took for all redirection steps include name lookup, connect, pretransfer and transfer before the final transaction was started. 'time_redirect' shows the complete execution time for multiple redirections. | +| StartXfer time (time_starttransfer) | The time, in seconds, it took from the start until the first byte was just about to be transferred. This includes 'time_pretransfer' and also the time the server needed to calculate the result. | +| Total time | The sum of all the other times. | + + +## Response Times: The 3 Important Limits + +Short note for your information. + +* **0.1 second** – is about the limit for having the user feel that the system is reacting instantaneously, meaning that no special feedback is necessary except to display the result; +* **1.0 second** – is about the limit for the user’s flow of thought to stay uninterrupted, even though the user will notice the delay. Normally, no special feedback is necessary during delays of more than 0.1 but less than 1.0 second, but the user does lose the feeling of operating directly on the data; +* **10 seconds** – is about the limit for keeping the user’s attention focused on the dialogue. For longer delays, users will want to perform other tasks while waiting for the computer to finish, so they should be given feedback indicating when the computer expects to be done. Feedback during the delay is especially important if the response time is + +
+

diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..57413e5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +colorama==0.4.6 +setuptools==70.0.0 +wolfsoftware.drawlines==0.1.1 +wolfsoftware.prereqs==0.1.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b6e67e6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,33 @@ +[pep8] +max_line_length = 160 + +[pycodestyle] +count = True +statistics = True +max-line-length = 160 + +[pylama:pep8] +max_line_length = 160 + +[pylama:pycodestyle] +max_line_length = 160 + +[pylama:pylint] +max_line_length = 160 + +[pylint.FORMAT] +max-line-length = 160 + +[pylint] +disable=unknown-option-value, + global-statement, + invalid-name, + line-too-long, + unnecessary-pass, + broad-exception-caught, + broad-exception-raised, +# Version 3.9.* + broad-except, + unsupported-binary-operation, +# Version 3.8.* + unsubscriptable-object diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c7530aa --- /dev/null +++ b/setup.py @@ -0,0 +1,61 @@ +# setup.py + +"""Setup script.""" + +from setuptools import setup + +with open('requirements.txt', 'r', encoding='UTF-8') as f: + required: list[str] = f.read().splitlines() + +with open("README.md", 'r', encoding='UTF-8') as f: + long_description: str = f.read() + +setup( + name='wolfsoftware.ttfb', + version='0.1.0', + author='Wolf Software', + author_email='pypi@wolfsoftware.com', + description='Display the time-to-first-byte for any given url.', + long_description=long_description, + long_description_content_type='text/markdown', + license='MIT', + packages=['wolfsoftware.ttfb'], + install_requires=required, + keywords=['python', 'time-to-first-byte'], + + entry_points={ + 'console_scripts': [ + 'ttfb=wolfsoftware.ttfb.main:main', + ], + }, + + url='https://github.com/PlatformEngineersToolbox/time-to-first-byte-package', + project_urls={ + ' Source': 'https://github.com/PlatformEngineersToolbox/time-to-first-byte-package', + ' Tracker': 'https://github.com/PlatformEngineersToolbox/time-to-first-byte-package/issues/', + ' Documentation': 'https://github.com/PlatformEngineersToolbox/time-to-first-byte-package', + ' Sponsor': 'https://github.com/sponsors/WolfSoftware', + }, + + classifiers=[ + # 'Development Status :: 1 - Planning', + # 'Development Status :: 2 - Pre-Alpha', + # 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', + # 'Development Status :: 5 - Production/Stable', + # 'Development Status :: 6 - Mature', + # 'Development Status :: 7 - Inactive', + 'Environment :: Console', + 'Intended Audience :: Developers', + # 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Operating System :: OS Independent', + 'Topic :: Software Development', + ], + python_requires='>=3.9', +) diff --git a/wolfsoftware/ttfb/__init.py__ b/wolfsoftware/ttfb/__init.py__ new file mode 100644 index 0000000..1abcfc8 --- /dev/null +++ b/wolfsoftware/ttfb/__init.py__ @@ -0,0 +1,11 @@ +""" +wolfsoftware.ttfb. + +A simple Python utility to display the time to getting the first byte from a given website url. +""" +import importlib.metadata + +try: + __version__: str = importlib.metadata.version('wolfsoftware.drawlines') +except importlib.metadata.PackageNotFoundError: + __version__ = 'unknown' diff --git a/wolfsoftware/ttfb/cli.py b/wolfsoftware/ttfb/cli.py new file mode 100644 index 0000000..c91fd5d --- /dev/null +++ b/wolfsoftware/ttfb/cli.py @@ -0,0 +1,138 @@ +""" +This module contains the main execution flow for the URL timing analysis program. + +This program checks for the presence of required command-line tools, parses command-line +arguments, validates a given URL, and measures the timing metrics for the URL using curl. +The timing results are displayed based on the specified configuration options. + +Modules and Functions: +- check_int_range: Validates that an integer value is within a specified range. +- setup_arg_parser: Sets up the command-line argument parser with necessary arguments and options. +- process_arguments: Processes and validates the command-line arguments. +- run: Main function to execute the program, coordinating all necessary steps. +- create_configuration_from_arguments: Creates a configuration object from the parsed arguments. +- process_url: Validates the URL and performs the timing analysis. +- display_timing: Displays detailed timing results for the URL. +- display_results: Displays the results header and configuration information. +- check_prerequisite: Checks for the presence of required command-line tools. +- validate_url: Validates that the URL is properly formed and reachable. +""" +# pylint: disable=relative-beyond-top-level + +import argparse +import sys + +from types import SimpleNamespace + +from .config import create_configuration_from_arguments +from .globals import ARG_PARSER_DESCRIPTION, ARG_PARSER_EPILOG, ARG_PARSER_PROG_NAME, VERSION_STRING +from .process import process_url +from .utils import check_prereqs + + +def check_int_range(value) -> int: + """ + Validate that an integer value is within the specified range (1 to 25). + + This function attempts to convert the input value to an integer and checks if it + falls within the range of 1 to 25, inclusive. If the value is not an integer or + is outside the specified range, it raises an `argparse.ArgumentTypeError`. + + Arguments: + value (str): The input value to be validated. + + Returns: + int: The validated integer value. + + Raises: + argparse.ArgumentTypeError: If the input value is not a valid integer. + argparse.ArgumentTypeError: If the integer value is not within the range 1 to 25. + """ + try: + ivalue = int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"Invalid integer value: {value}") from exc + if ivalue < 1 or ivalue > 25: + raise argparse.ArgumentTypeError(f"Integer value must be between 1 and 25: {value}") + return ivalue + + +def setup_arg_parser() -> argparse.ArgumentParser: + """ + Set up and returns the argument parser for the program. + + This function defines and configures the argument parser with the necessary arguments and options + for the program, including flags, exclusive flags, optional arguments, and required arguments. + + Returns: + argparse.ArgumentParser: The configured argument parser. + """ + parser = argparse.ArgumentParser(prog=ARG_PARSER_PROG_NAME, + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description=ARG_PARSER_DESCRIPTION, + epilog=ARG_PARSER_EPILOG) + + flags: argparse._ArgumentGroup = parser.add_argument_group(title='flags') + exclusive_flags: argparse._ArgumentGroup = parser.add_argument_group(title='exclusive flags') + optional: argparse._ArgumentGroup = parser.add_argument_group(title='optional') + required: argparse._ArgumentGroup = parser.add_argument_group(title='required') + + flags.add_argument("-h", "--help", action="help", default=argparse.SUPPRESS, help="show this help message and exit") + flags.add_argument("-d", "--debug", action="store_true", default=False, help="Very noisy") + flags.add_argument("-v", "--verbose", action="store_true", default=False, help="Verbose output - show scan results as they come in") + flags.add_argument('-V', '--version', action="version", version=VERSION_STRING, help="Show program's version number and exit.") + + exclusive_flags_group: argparse._MutuallyExclusiveGroup = exclusive_flags.add_mutually_exclusive_group(required=False) + exclusive_flags_group.add_argument('-m', '--minimal', action="store_true", default=False, help="Show minimal set of timing values.") + exclusive_flags_group.add_argument('-f', '--full', action="store_true", default=False, help="Show full set of timing values.") + + optional.add_argument("-c", "--count", type=check_int_range, default=1, help="How many times to test [1-25]") + + required.add_argument("-u", "--url", type=str, required=True, help="The URL to test") + + return parser + + +def process_arguments(parser: argparse.ArgumentParser) -> argparse.Namespace: + """ + Process the command line arguments. + + This function uses the provided argument parser to parse the command line arguments, + validate the input, and then return the parsed arguments as a Namespace object. + + Arguments: + parser (argparse.ArgumentParser): The argument parser configured with the necessary arguments and options. + + Returns: + argparse.Namespace: The parsed command line arguments. + """ + args: argparse.Namespace = parser.parse_args() + + return args + + +def run() -> None: + """ + Master controller function. + + This function performs the following steps: + 1. Checks prerequisites and obtains command paths. + 2. Sets up the argument parser. + 3. Processes command-line arguments. + 4. Creates a configuration from the processed arguments. + 5. Processes a URL based on the created configuration. + + If there is an argument type error during argument processing, it prints the usage information, + prints the error message, and exits the program with a status code of 1. + """ + command_paths: dict = check_prereqs() + parser: argparse.ArgumentParser = setup_arg_parser() + try: + args: argparse.Namespace = process_arguments(parser) + config: SimpleNamespace = create_configuration_from_arguments(args, command_paths) + process_url(config) + except argparse.ArgumentTypeError as err: + parser.print_usage() + print(err) + sys.exit(1) diff --git a/wolfsoftware/ttfb/config.py b/wolfsoftware/ttfb/config.py new file mode 100644 index 0000000..e1ad784 --- /dev/null +++ b/wolfsoftware/ttfb/config.py @@ -0,0 +1,57 @@ +""" +This module defines the configuration setup for the URL timing analysis program. + +The main purpose of this module is to create a configuration object from the +command-line arguments and command paths. The configuration object is used to +store various settings such as verbosity, debug mode, URL, and screen width, +which are necessary for performing the timing analysis. + +Functions: +- create_configuration_from_arguments: Creates and returns a configuration object + based on parsed command-line arguments and command paths. + +Modules: +- argparse.Namespace: Used for type annotation of command-line arguments. +- types.SimpleNamespace: Used to create a simple object for storing configuration settings. +""" + +from argparse import Namespace + +from types import SimpleNamespace + + +def create_configuration_from_arguments(args: Namespace, command_paths: dict) -> SimpleNamespace: + """ + Create a configuration object from command-line arguments and command paths. + + This function takes the parsed command-line arguments and command paths to populate + a SimpleNamespace object with the appropriate configuration settings. + + Arguments: + args (Namespace): The parsed command-line arguments. + command_paths (dict): A dictionary containing command paths. + + Returns: + SimpleNamespace: A configuration object populated with the necessary settings. + This includes verbosity, debug mode, minimal/full configuration, + count, URL, screen width, and command paths. + """ + config: SimpleNamespace = SimpleNamespace() + + config.verbose = args.verbose + config.debug = args.debug + config.minimal = args.minimal + config.full = args.full + config.count = args.count + config.url = args.url + + if config.full: + config.screen_width = 182 + elif config.minimal: + config.screen_width = 58 + else: + config.screen_width = 107 + + config.command_paths = command_paths + + return config diff --git a/wolfsoftware/ttfb/constants.py b/wolfsoftware/ttfb/constants.py new file mode 100644 index 0000000..42aae46 --- /dev/null +++ b/wolfsoftware/ttfb/constants.py @@ -0,0 +1,32 @@ +""" +This module initializes color settings for terminal output using the colorama library. + +The main purpose of this module is to set up color constants for use in formatting terminal +output with various colors and styles. It initializes the colorama library to enable cross-platform +color support in the terminal. + +Modules and Constants: +- colorama: A library for cross-platform colored terminal text. +- BLACK, BLUE, CYAN, GREEN, GREY, MAGENTA, RED, WHITE, YELLOW: Foreground color constants. +- BOLD: Constant for bright style text. +- RESET: Constant to reset all styles to default. + +The module initializes colorama to ensure that color support is properly set up on all platforms. +""" + +import colorama + +colorama.init() + +BLACK: str = colorama.Fore.BLACK +BLUE: str = colorama.Fore.BLUE +CYAN: str = colorama.Fore.CYAN +GREEN: str = colorama.Fore.GREEN +GREY: str = colorama.Fore.LIGHTBLACK_EX +MAGENTA: str = colorama.Fore.MAGENTA +RED: str = colorama.Fore.RED +WHITE: str = colorama.Fore.WHITE +YELLOW: str = colorama.Fore.YELLOW + +BOLD: str = colorama.Style.BRIGHT +RESET: str = colorama.Style.RESET_ALL diff --git a/wolfsoftware/ttfb/exceptions.py b/wolfsoftware/ttfb/exceptions.py new file mode 100644 index 0000000..7801743 --- /dev/null +++ b/wolfsoftware/ttfb/exceptions.py @@ -0,0 +1,21 @@ +""" +This module defines a custom exception for specific error handling within the program. + +The main purpose of this module is to create a custom exception class that allows for more +granular and descriptive error handling tailored to the application's domain logic. + +Classes: +- CustomException: A custom exception class used for specific error scenarios in the application. +""" + + +class CustomException(Exception): + """ + A custom exception class for specific error handling within the program. + + This class is used to define exceptions that are specific to the application's + domain logic, allowing for more granular and descriptive error handling. + + Inherits from: + Exception: The base class for all built-in exceptions. + """ diff --git a/wolfsoftware/ttfb/globals.py b/wolfsoftware/ttfb/globals.py new file mode 100644 index 0000000..53bc2ef --- /dev/null +++ b/wolfsoftware/ttfb/globals.py @@ -0,0 +1,36 @@ +""" +This module sets up global constants and version information for the URL timing analysis program. + +The main purpose of this module is to define constants that are used throughout the application, +such as the program name, description, and version information. It also checks for prerequisite +commands needed for the program to run. + +Modules and Constants: +- importlib.metadata: Used to retrieve package version information. +- version: The current version of the 'wolfsoftware.ttfb' package, or 'unknown' if the package is not found. +- ARG_PARSER_PROG_NAME: The program name for the argument parser. +- ARG_PARSER_DESCRIPTION: The description of the program for the argument parser. +- ARG_PARSER_EPILOG: The epilog for the argument parser. +- VERSION_STRING: A formatted string displaying the current version of the program. +- SCRIPT_TITLE: The title of the script, used in the output display. +- prerequisite_commands: A list of commands that are required for the program to run. + +Exceptions: +- importlib.metadata.PackageNotFoundError: If the 'wolfsoftware.ttfb' package is not found. +""" + +import importlib.metadata + +try: + version: str = importlib.metadata.version('wolfsoftware.ttfb') +except importlib.metadata.PackageNotFoundError: + version = 'unknown' + +ARG_PARSER_PROG_NAME: str = "ttfb" +ARG_PARSER_DESCRIPTION: str = "Display the time-to-first-byte for any given url." +ARG_PARSER_EPILOG: str = "The Epilog goes here" + +VERSION_STRING: str = "Current version of " + ARG_PARSER_PROG_NAME + " is v" + version + +SCRIPT_TITLE = "Time to First Byte Tester" +prerequisite_commands: list[str] = ["curl"] diff --git a/wolfsoftware/ttfb/main.py b/wolfsoftware/ttfb/main.py new file mode 100644 index 0000000..b893f1d --- /dev/null +++ b/wolfsoftware/ttfb/main.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +""" +This module serves as the entry point for the URL timing analysis program. + +The main purpose of this module is to initiate the program execution by calling the `run` function +and handling any KeyboardInterrupt exceptions to allow for graceful termination of the program. + +Functions: +- main: The main entry point of the program that calls the `run` function and handles exceptions. + +Modules: +- sys: Provides access to system-specific parameters and functions. +- run: The main function that executes the core logic of the program, imported from the cli module. +- system: A function to display system notifications, imported from the notify module. +""" +# pylint: disable=relative-beyond-top-level + +import sys + +from .cli import run +from .notify import system + + +def main() -> None: + """ + Execute the main entry point for the program. + + This function attempts to execute the `run` function and handles a KeyboardInterrupt + exception to gracefully exit the program. + + If a KeyboardInterrupt is detected (typically from pressing Ctrl+C), it prints an + exit message and terminates the program with a status code of 1. + """ + try: + run() + except KeyboardInterrupt: + system("\n[*] Exiting Program\n") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/wolfsoftware/ttfb/notify.py b/wolfsoftware/ttfb/notify.py new file mode 100644 index 0000000..a4643db --- /dev/null +++ b/wolfsoftware/ttfb/notify.py @@ -0,0 +1,81 @@ +""" +This module defines functions for printing formatted messages to the terminal. + +The main purpose of this module is to provide utility functions for displaying +success, warning, error, information, and system messages with specific color +and style formatting using predefined constants. + +Functions: +- success: Prints a success message formatted with bold and green text. +- warn: Prints a warning message formatted with bold and yellow text. +- error: Prints an error message formatted with bold and red text. +- info: Prints an information message formatted with bold and cyan text. +- system: Prints a system message formatted with bold and grey text. + +Modules: +- constants: Imports color and style constants (CYAN, GREEN, GREY, RED, YELLOW, BOLD, RESET) + from the constants module. +""" +# pylint: disable=relative-beyond-top-level + +from .constants import CYAN, GREEN, GREY, RED, YELLOW, BOLD, RESET + + +def success(message: str) -> None: + """ + Print a success message with a specific format. + + This function outputs a message indicating success, formatted with bold and green text. + + Arguments: + message (str): The success message to be printed. + """ + print(f'[ {BOLD}{GREEN}Success{RESET} ] {message}') + + +def warn(message: str) -> None: + """ + Print a warning message with a specific format. + + This function outputs a message indicating a warning, formatted with bold and yellow text. + + Arguments: + message (str): The warning message to be printed. + """ + print(f'[ {BOLD}{YELLOW}Warning{RESET} ] {message}') + + +def error(message: str) -> None: + """ + Print an error message with a specific format. + + This function outputs a message indicating success, formatted with bold and red text. + + Arguments: + message (str): The error message to be printed. + """ + print(f'[ {BOLD}{RED}Error{RESET} ] {message}') + + +def info(message: str) -> None: + """ + Print an information message with a specific format. + + This function outputs a message indicating success, formatted with bold and cyan text. + + Arguments: + message (str): The information message to be printed. + """ + print(f'[ {BOLD}{CYAN}Info{RESET} ] {message}') + + +def system(message: str) -> None: + """ + Print a system message with a specific format. + + This function outputs a message indicating success, formatted with bold and grey text. + + Arguments: + message (str): The system message to be printed. + """ + print(f'{BOLD}{GREY}{message}{RESET}') diff --git a/wolfsoftware/ttfb/process.py b/wolfsoftware/ttfb/process.py new file mode 100644 index 0000000..1cb9f8d --- /dev/null +++ b/wolfsoftware/ttfb/process.py @@ -0,0 +1,91 @@ +""" +This module handles URL processing and timing information display for the URL timing analysis program. + +The main purpose of this module is to validate a given URL, execute curl commands to measure various +timing metrics, and display the results in a formatted manner. + +Functions: +- process_url: Validates the URL and initiates the timing display process. +- display_timing: Executes curl commands to measure and display timing metrics for the URL. + +Modules: +- subprocess: Used to run curl commands to measure timing metrics. +- types.SimpleNamespace: Used to handle configuration settings. +- wolfsoftware.drawlines: Provides functions to draw formatted lines in the terminal. +- globals: Imports global constants like SCRIPT_TITLE. +- utils: Imports utility functions like validate_url. +""" +# pylint: disable=relative-beyond-top-level + +import subprocess # nosec B404 + +from types import SimpleNamespace + +from wolfsoftware.drawlines import draw_line + +from .globals import SCRIPT_TITLE +from .utils import validate_url + + +def process_url(config: SimpleNamespace) -> None: + """ + Process a URL based on the provided configuration. + + This function validates the URL specified in the configuration and displays timing information. + + Arguments: + config (SimpleNamespace): The configuration object containing the URL and other settings. + """ + validate_url(config.url) + display_timing(config) + + +def display_timing(config: SimpleNamespace) -> None: + """ + Display timing information for the specified URL. + + This function prints formatted lines and executes curl commands to display various timing metrics + (such as lookup time, connect time, TTFB, and total time) for the URL specified in the configuration. + + The command executed depends on the configuration: + - Minimal: Only TTFB and total time. + - Full: Detailed timing metrics including lookup, connect, app connect, pre-transfer, redirect, TTFB, and total time. + - Default: A subset of the full metrics. + + Arguments: + config (SimpleNamespace): The configuration object containing settings such as screen width, URL, command paths, + verbosity, and the number of times to repeat the command. + """ + print(draw_line(width=config.screen_width)) + print(draw_line(width=config.screen_width, text=SCRIPT_TITLE, fill_char=' ')) + print(draw_line(width=config.screen_width, text=f"Results for {config.url}", fill_char=' ')) + print(draw_line(width=config.screen_width)) + + minimal_command: list[str] = [ + config.command_paths['curl'], '-L', '-o', '/dev/null', '-H', 'Cache-Control: no-cache', '-s', '-w', + ' StartXfer Time (TTFB): %{time_starttransfer} Total Time: %{time_total}\n', + config.url + ] + full_command: list[str] = [ + config.command_paths['curl'], '-L', '-o', '/dev/null', '-H', 'Cache-Control: no-cache', '-s', '-w', + ( + ' Lookup Time: %{time_namelookup} Connect Time: %{time_connect} AppCon Time: %{time_appconnect} PreXfer Time: %{time_pretransfer} ' + 'Redirect Time: %{time_redirect} StartXfer Time (TTFB): %{time_starttransfer} Total Time: %{time_total}\n' + ), + config.url + ] + default_command: list[str] = [ + config.command_paths['curl'], '-L', '-o', '/dev/null', '-H', 'Cache-Control: no-cache', '-s', '-w', + ' Lookup Time: %{time_namelookup} Connect Time: %{time_connect} StartXfer Time (TTFB): %{time_starttransfer} Total Time: %{time_total}\n', + config.url + ] + + for _i in range(config.count): + if config.minimal: + subprocess.run(minimal_command, text=True, check=True) # nosec B603 + elif config.full: + subprocess.run(full_command, text=True, check=True) # nosec B603 + else: + subprocess.run(default_command, text=True, check=True) # nosec B603 + + print(draw_line(width=config.screen_width)) diff --git a/wolfsoftware/ttfb/results.py b/wolfsoftware/ttfb/results.py new file mode 100644 index 0000000..6c86a98 --- /dev/null +++ b/wolfsoftware/ttfb/results.py @@ -0,0 +1,37 @@ +""" +This module handles the display of results for the URL timing analysis program. + +The main purpose of this module is to format and display the results header and configuration +information for the specified URL. + +Functions: +- display_results: Displays custom formatted lines and configuration details for the URL. + +Modules: +- types.SimpleNamespace: Used to handle configuration settings. +- utils: Imports utility functions like draw_custom_line for custom line drawing. +""" +# pylint: disable=relative-beyond-top-level + +from types import SimpleNamespace + +from .utils import draw_custom_line + + +def display_results(config: SimpleNamespace) -> None: + """ + Display the results header and configuration information for the specified URL. + + This function prints custom formatted lines and the configuration details. + It uses `draw_custom_line` to display the header and the URL being tested, and + prints the configuration object. + + Arguments: + config (SimpleNamespace): The configuration object containing settings such as the URL and other details. + """ + draw_custom_line() + draw_custom_line("Time to First Byte Tester", "center", " ", color="cyan") + draw_custom_line(f"Results for: {config.url}", "center", " ") + draw_custom_line() + print(f"Config: {config}") + draw_custom_line() diff --git a/wolfsoftware/ttfb/utils.py b/wolfsoftware/ttfb/utils.py new file mode 100644 index 0000000..f0f8277 --- /dev/null +++ b/wolfsoftware/ttfb/utils.py @@ -0,0 +1,87 @@ +""" +This module handles prerequisite checks and URL validation for the URL timing analysis program. + +The main purpose of this module is to ensure that all necessary commands are available +and to validate the format and reachability of the specified URL. + +Functions: +- check_prerequisite: Verifies the presence of prerequisite commands and returns their paths. +- validate_url: Validates that a URL is well-formed and reachable. + +Modules: +- shutil: Used to check for the presence of commands. +- sys: Provides access to system-specific parameters and functions. +- subprocess: Used to run curl commands to validate URL reachability (with security warning disabled). +- globals: Imports global constants like prerequisite_commands. +- notify: Imports utility functions like error for displaying error messages. +""" +# pylint: disable=relative-beyond-top-level + +import sys + +import subprocess # nosec B404 + +from wolfsoftware.prereqs import check_prerequisite, PrerequisiteCheckError + +from .globals import prerequisite_commands +from .notify import error + + +def check_prereqs() -> dict: + """ + Check for the presence of prerequisite commands and returns their paths. + + This function iterates through a list of prerequisite commands to verify their installation + using `shutil.which`. It collects the full paths of the installed commands into a dictionary. + If any commands are not found, it prints the errors and exits the program with a status code of 1. + + Returns: + dict: A dictionary mapping each prerequisite command to its full path. + + Exits: + If any prerequisite commands are not installed, prints the errors and exits the program. + """ + try: + command_paths: dict = check_prerequisite(prerequisite_commands) + return command_paths + except PrerequisiteCheckError as errors: + error("Prerequisite check failed:") + for err in errors.errors: + print(err) + sys.exit(1) + + +def validate_url(url) -> None: + """ + Validate the given URL to ensure it is well-formed and reachable. + + This function checks that the URL is a string and starts with 'http://' or 'https://'. + It then uses a curl command to verify that the URL exists and is reachable. If the URL is invalid + or unreachable, it prints an error message and exits the program with a status code of 1. + + Arguments: + url (str): The URL to be validated. + + Exits: + If the URL does not exist or is unreachable, prints an error message and exits the program. + """ + if not isinstance(url, str) or not url.startswith(('http://', 'https://')): + error("Invalid URL - must start with http:// or https://") + sys.exit(1) + + command: list[str] = ['curl', '-o', '/dev/null', '--silent', '--head', '--fail', '--connect-timeout', '1', url] + try: + result: subprocess.CompletedProcess[str] = subprocess.run( # nosec B603 + command, text=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + if result.returncode != 0: + error(f"{url} does not exist - aborting") + sys.exit(1) + + except subprocess.CalledProcessError: + error(f"{url} does not exist - aborting") + sys.exit(1) + + except Exception: + error(f"An unexpected error occurred while checking {url}") + sys.exit(1)