From 99bf556445822df8e4887c66113b3fe8eed1facb Mon Sep 17 00:00:00 2001 From: "Mathias L. Baumann" Date: Thu, 22 Feb 2024 15:10:16 +0100 Subject: [PATCH] Initial commit Signed-off-by: Mathias L. Baumann --- .cookiecutter-replay.json | 45 ++ .editorconfig | 26 ++ .github/ISSUE_TEMPLATE/bug.yml | 66 +++ .github/ISSUE_TEMPLATE/config.yml | 10 + .github/ISSUE_TEMPLATE/feature.yml | 61 +++ .github/RELEASE_NOTES.template.md | 17 + .../arm64-ubuntu-20.04-python-3.11.Dockerfile | 33 ++ .../containers/nox-cross-arch/entrypoint.bash | 9 + .../containers/test-installation/Dockerfile | 17 + .github/dependabot.yml | 41 ++ .github/keylabeler.yml | 21 + .github/labeler.yml | 63 +++ .github/workflows/ci.yaml | 441 ++++++++++++++++++ .github/workflows/dco-merge-queue.yml | 11 + .github/workflows/labeler.yml | 24 + .github/workflows/release-notes-check.yml | 30 ++ .gitignore | 150 ++++++ CODEOWNERS | 8 + CONTRIBUTING.md | 183 ++++++++ LICENSE | 21 + MANIFEST.in | 13 + README.md | 24 + RELEASE_NOTES.md | 17 + docs/CONTRIBUTING.md | 1 + docs/SUMMARY.md | 3 + docs/_css/mkdocstrings.css | 44 ++ docs/_css/style.css | 70 +++ docs/_img/logo.png | Bin 0 -> 57278 bytes docs/_overrides/main.html | 8 + docs/_scripts/macros.py | 83 ++++ docs/_scripts/mkdocstrings_autoapi.py | 8 + docs/index.md | 1 + mkdocs.yml | 138 ++++++ noxfile.py | 8 + pyproject.toml | 170 +++++++ src/frequenz/actor/dispatch/__init__.py | 25 + src/frequenz/actor/dispatch/conftest.py | 13 + src/frequenz/actor/dispatch/py.typed | 0 tests/test_frequenz_dispatch.py | 18 + 39 files changed, 1921 insertions(+) create mode 100644 .cookiecutter-replay.json create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/RELEASE_NOTES.template.md create mode 100644 .github/containers/nox-cross-arch/arm64-ubuntu-20.04-python-3.11.Dockerfile create mode 100755 .github/containers/nox-cross-arch/entrypoint.bash create mode 100644 .github/containers/test-installation/Dockerfile create mode 100644 .github/dependabot.yml create mode 100644 .github/keylabeler.yml create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/dco-merge-queue.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/release-notes-check.yml create mode 100644 .gitignore create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 RELEASE_NOTES.md create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/SUMMARY.md create mode 100644 docs/_css/mkdocstrings.css create mode 100644 docs/_css/style.css create mode 100644 docs/_img/logo.png create mode 100644 docs/_overrides/main.html create mode 100644 docs/_scripts/macros.py create mode 100644 docs/_scripts/mkdocstrings_autoapi.py create mode 100644 docs/index.md create mode 100644 mkdocs.yml create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 src/frequenz/actor/dispatch/__init__.py create mode 100644 src/frequenz/actor/dispatch/conftest.py create mode 100644 src/frequenz/actor/dispatch/py.typed create mode 100644 tests/test_frequenz_dispatch.py diff --git a/.cookiecutter-replay.json b/.cookiecutter-replay.json new file mode 100644 index 0000000..8b3de27 --- /dev/null +++ b/.cookiecutter-replay.json @@ -0,0 +1,45 @@ +{ + "cookiecutter": { + "Introduction": "", + "type": "actor", + "name": "frequenz-dispatch", + "description": "A highlevel interface for the dispatch API", + "title": "Dispatch Highlevel Interface", + "keywords": "dispatch,highlevel,api", + "github_org": "frequenz-floss", + "license": "MIT", + "author_name": "Frequenz Energy-as-a-Service GmbH", + "author_email": "floss@frequenz.com", + "python_package": "frequenz.actor.dispatch", + "pypi_package_name": "frequenz-actor-dispatch", + "github_repo_name": "frequenz-dispatch-python", + "default_codeowners": "@frequenz-floss/api-dispatch-team", + "_template": "gh:frequenz-floss/frequenz-repo-config-python", + "_repo_dir": "/home/marenz/.cookiecutters/frequenz-repo-config-python/cookiecutter" + }, + "_cookiecutter": { + "Introduction": "{{cookiecutter | introduction}}", + "type": [ + "actor", + "api", + "app", + "lib", + "model" + ], + "name": null, + "description": null, + "title": "{{cookiecutter | proj_title}}", + "keywords": "(comma separated: 'frequenz', and are included automatically)", + "github_org": "frequenz-floss", + "license": [ + "MIT", + "Proprietary" + ], + "author_name": "Frequenz Energy-as-a-Service GmbH", + "author_email": "floss@frequenz.com", + "python_package": "{{cookiecutter | python_package}}", + "pypi_package_name": "{{cookiecutter | pypi_package_name}}", + "github_repo_name": "{{cookiecutter | github_repo_name}}", + "default_codeowners": "(like @some-org/some-team; defaults to a team based on the repo type)" + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c7f9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Set default charset, indent style and trimming of whitespace +[{.editorconfig,CODEOWNERS,LICENSE,*.{in,json,md,proto,py,pyi,toml,yaml,yml}}] +charset = utf-8 +indent_style = space +trim_trailing_whitespace = true + +# 4 space indentation +[*.{py,pyi}] +indent_size = 4 + +# 2 space indentation +[{.editorconfig,CODEOWNERS,LICENSE,*.{in,json,proto,toml,yaml,yml}}] +indent_size = 2 + +# No indentation size specified for *.md because different blocks have +# different indentation rules diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..ff62366 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,66 @@ +# GitHub issue form. For more information see: +# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms + +name: Report something is not working properly 🐛 +description: + Use this if there is something that is not working properly. If you are not + sure or you need help making something work, please ask a question instead. +labels: + - "priority:❓" + - "type:bug" +body: + - type: markdown + attributes: + value: + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Please tell us what happened that shouldn't have. + placeholder: What happened that shouldn't have. + validations: + required: true + - type: textarea + id: what-expected + attributes: + label: What did you expect instead? + description: Please tell us what did you expect to happen. + placeholder: What did you expect to happen. + validations: + required: true + - type: input + id: version + attributes: + label: Affected version(s) + description: + Please add a comma-separated list of the versions affected by this + issue. + placeholder: 'Example: v0.11.0, v0.12.0' + - type: dropdown + id: part + attributes: + label: Affected part(s) + description: + Which parts of the repo are affected by this issue? Select all that + apply. + multiple: true + options: + - I don't know (part:❓) + - Documentation (part:docs) + - Unit, integration and performance tests (part:tests) + - Build script, CI, dependencies, etc. (part:tooling) + # TODO(cookiecutter): Add other parts + # Please have in mind that that the part:xxx labels need to + # be created in the GitHub repository. + validations: + required: true + - type: textarea + id: extra + attributes: + label: Extra information + description: + Please write here any extra information you think it might be relevant, + e.g., if this didn't happen before, or if you suspect where the problem + might be. + placeholder: Any extra information you think it might be relevant. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..825cf27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +# GitHub issue template chooser. For more information see: +# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser + +blank_issues_enabled: true +contact_links: + - name: Ask a question ❓ + url: https://github.com/frequenz-floss/frequenz-dispatch-python/discussions/new?category=support + # TODO(cookiecutter): Make sure the GitHub repository has a discussion category "Support" + # Rename the "Q&A" category to "Support" and change the emoji to 🆘 (SOS) + about: Use this if you are not sure how to do something, have installation problems, etc. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..459dd4b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,61 @@ +# GitHub issue form. For more information see: +# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms + +name: Request a feature or enhancement ✨ +description: Use this if something is missing or could be done better or more easily. +labels: + - "part:❓" + - "priority:❓" + - "type:enhancement" +body: + - type: markdown + attributes: + value: + Thanks for taking the time to fill out this feature or enhancement + request! + - type: textarea + id: whats-needed + attributes: + label: What's needed? + description: + Please tell us what's missing or what could be done better or more easily. + placeholder: What's missing or what could be done better or more easily. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: + Please tell us how you think the needs above can be fulfilled. Only + fill this field if it wasn't described above. + placeholder: + How do you think the needs above can be fulfilled. Only fill this field + if it wasn't described above. + - type: textarea + id: use-cases + attributes: + label: Use cases + description: + Please tell us about the main use cases you see for this new feature or + enhancement to help us understand more. + placeholder: + The main use cases you see for this new feature or enhancement to help + us understand more. + - type: textarea + id: alternatives + attributes: + label: Alternatives and workarounds + description: + Please tell us if you tried any alternatives or workarounds for these + use cases and how (un)useful they were. + placeholder: + Any alternatives or workarounds for these use cases and how (un)useful + they were. + - type: textarea + id: additional-context + attributes: + label: Additional context + description: + Please add any additional information here - screenshots, diagrams, etc. + placeholder: Any additional information here - screenshots, diagrams, etc. diff --git a/.github/RELEASE_NOTES.template.md b/.github/RELEASE_NOTES.template.md new file mode 100644 index 0000000..d4546f4 --- /dev/null +++ b/.github/RELEASE_NOTES.template.md @@ -0,0 +1,17 @@ +# Dispatch Highlevel Interface Release Notes + +## Summary + + + +## Upgrading + + + +## New Features + + + +## Bug Fixes + + diff --git a/.github/containers/nox-cross-arch/arm64-ubuntu-20.04-python-3.11.Dockerfile b/.github/containers/nox-cross-arch/arm64-ubuntu-20.04-python-3.11.Dockerfile new file mode 100644 index 0000000..4a29eec --- /dev/null +++ b/.github/containers/nox-cross-arch/arm64-ubuntu-20.04-python-3.11.Dockerfile @@ -0,0 +1,33 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH +# This Dockerfile is used to run the tests in arm64, which is not supported by +# GitHub Actions at the moment. + +FROM docker.io/library/ubuntu:20.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Install Python 3.11 and curl to install pip later +RUN apt-get update -y && \ + apt-get install --no-install-recommends -y \ + software-properties-common && \ + add-apt-repository ppa:deadsnakes/ppa && \ + apt-get install --no-install-recommends -y \ + ca-certificates \ + curl \ + git \ + python3.11 \ + python3.11-distutils && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Install pip +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 + +RUN update-alternatives --install \ + /usr/local/bin/python python /usr/bin/python3.11 1 && \ + python -m pip install --upgrade --no-cache-dir pip + +COPY entrypoint.bash /usr/bin/entrypoint.bash + +ENTRYPOINT ["/usr/bin/entrypoint.bash"] diff --git a/.github/containers/nox-cross-arch/entrypoint.bash b/.github/containers/nox-cross-arch/entrypoint.bash new file mode 100755 index 0000000..f344deb --- /dev/null +++ b/.github/containers/nox-cross-arch/entrypoint.bash @@ -0,0 +1,9 @@ +#!/bin/bash +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH +set -e + +echo "System details:" $(uname -a) +echo "Machine:" $(uname -m) + +exec "$@" diff --git a/.github/containers/test-installation/Dockerfile b/.github/containers/test-installation/Dockerfile new file mode 100644 index 0000000..4be1515 --- /dev/null +++ b/.github/containers/test-installation/Dockerfile @@ -0,0 +1,17 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH +# This Dockerfile is used to test the installation of the python package in +# multiple platforms in the CI. It is not used to build the package itself. + +FROM --platform=${TARGETPLATFORM} python:3.11-slim + +RUN apt-get update -y && \ + apt-get install --no-install-recommends -y \ + git && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + python -m pip install --upgrade --no-cache-dir pip + +COPY dist dist +RUN pip install dist/*.whl && \ + rm -rf dist diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3077268 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,41 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + day: "thursday" + labels: + - "part:tooling" + - "type:tech-debt" + # Default versioning-strategy. For other versioning-strategy see: + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#versioning-strategy + versioning-strategy: auto + # Allow up to 10 open pull requests for updates to dependency versions + open-pull-requests-limit: 10 + # We group production and development ("optional" in the context of + # pyproject.toml) dependency updates when they are patch and minor updates, + # so we end up with less PRs being generated. + # Major updates are still managed, but they'll create one PR per + # dependency, as major updates are expected to be breaking, it is better to + # manage them individually. + groups: + required: + dependency-type: "production" + update-types: + - "minor" + - "patch" + optional: + dependency-type: "development" + update-types: + - "minor" + - "patch" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + day: "thursday" + labels: + - "part:tooling" + - "type:tech-debt" diff --git a/.github/keylabeler.yml b/.github/keylabeler.yml new file mode 100644 index 0000000..e5abffe --- /dev/null +++ b/.github/keylabeler.yml @@ -0,0 +1,21 @@ +# KeywordLabeler app configuration. For more information check: +# https://github.com/ZeWaka/KeywordLabeler#readme + +# Determines if we search the title (optional). Defaults to true. +matchTitle: true + +# Determines if we search the body (optional). Defaults to true. +matchBody: true + +# Determines if label matching is case sensitive (optional). Defaults to true. +caseSensitive: true + +# Explicit keyword mappings to labels. Form of match:label. Required. +labelMappings: + "part:docs": "part:docs" + "part:tests": "part:tests" + "part:tooling": "part:tooling" + "part:❓": "part:❓" + # TODO(cookiecutter): Add other parts + # Please have in mind that that the part:xxx labels need to + # be created in the GitHub repository. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..9003502 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,63 @@ +# Configuration for the Labeler GitHub action, executed by +# .github/workflows/labeler.yml. +# +# The basic syntax is [label]: [path patterns]. +# +# For more details on the configuration please see: +# https://github.com/marketplace/actions/labeler + +# TODO(cookiecutter): Add different parts of the source +# For example: +# +# "part:module": +# - changed-files: +# - any-glob-to-any-file: +# - "src/frequenz/actor/frequenz_dispatch/module/**" +# +# "part:other": +# - changed-files: +# - any-glob-to-any-file: +# - "src/frequenz/actor/frequenz_dispatch/other/**" +# +# # For excluding some files (in this example, label "part:complicated" +# # everything inside src/ with a .py suffix, except for src/__init__.py) +# "part:complicated": +# - all: +# - changed-files: +# - any-glob-to-any-file: +# - "src/**/*.py" +# - all-glob-to-all-file: +# - "!src/__init__.py" +# +# Please have in mind that that the part:xxx labels need to +# be created in the GitHub repository. + +"part:docs": + - changed-files: + - any-glob-to-any-file: + - "**/*.md" + - "docs/**" + - "examples/**" + - LICENSE + +"part:tests": + - changed-files: + - any-glob-to-any-file: + - "**/conftest.py" + - "tests/**" + +"part:tooling": + - changed-files: + - any-glob-to-any-file: + - "**/*.ini" + - "**/*.toml" + - "**/*.yaml" + - "**/*.yml" + - "**/conftest.py" + - ".editorconfig" + - ".git*" + - ".git*/**" + - "docs/*.py" + - CODEOWNERS + - MANIFEST.in + - noxfile.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..172374b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,441 @@ +name: CI + +on: + merge_group: + pull_request: + push: + # We need to explicitly include tags because otherwise when adding + # `branches-ignore` it will only trigger on branches. + tags: + - '*' + branches-ignore: + # Ignore pushes to merge queues. + # We only want to test the merge commit (`merge_group` event), the hashes + # in the push were already tested by the PR checks + - 'gh-readonly-queue/**' + - 'dependabot/**' + workflow_dispatch: + +env: + # Please make sure this version is included in the `matrix`, as the + # `matrix` section can't use `env`, so it must be entered manually + DEFAULT_PYTHON_VERSION: '3.11' + # It would be nice to be able to also define a DEFAULT_UBUNTU_VERSION + # but sadly `env` can't be used either in `runs-on`. + +jobs: + nox: + name: Test with nox + strategy: + fail-fast: false + matrix: + os: + - ubuntu-20.04 + python: + - "3.11" + nox-session: + # To speed things up a bit we use the special ci_checks_max session + # that uses the same venv to run multiple linting sessions + - "ci_checks_max" + - "pytest_min" + runs-on: ${{ matrix.os }} + + steps: + - name: Print environment (debug) + run: env + + - name: Fetch sources + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + cache: 'pip' + + - name: Install required Python packages + run: | + python -m pip install --upgrade pip + python -m pip install -e .[dev-noxfile] + pip freeze + + - name: Create nox venv + env: + NOX_SESSION: ${{ matrix.nox-session }} + run: nox --install-only -e "$NOX_SESSION" + + - name: Print pip freeze for nox venv (debug) + env: + NOX_SESSION: ${{ matrix.nox-session }} + run: | + . ".nox/$NOX_SESSION/bin/activate" + pip freeze + deactivate + + - name: Run nox + env: + NOX_SESSION: ${{ matrix.nox-session }} + run: nox -R -e "$NOX_SESSION" + timeout-minutes: 10 + + # This job runs if all the `nox` matrix jobs ran and succeeded. + # It is only used to have a single job that we can require in branch + # protection rules, so we don't have to update the protection rules each time + # we add or remove a job from the matrix. + nox-all: + # The job name should match the name of the `nox` job. + name: Test with nox + needs: ["nox"] + runs-on: ubuntu-20.04 + steps: + - name: Return true + run: "true" + + nox-cross-arch: + name: Cross-arch tests with nox + if: github.event_name != 'pull_request' + strategy: + fail-fast: false + # Before adding new items to this matrix, make sure that a dockerfile + # exists for the combination of items in the matrix. + # Refer to .github/containers/nox-cross-arch/README.md to learn how to + # add and name new dockerfiles. + matrix: + arch: + - arm64 + os: + - ubuntu-20.04 + python: + - "3.11" + nox-session: + - "pytest_min" + - "pytest_max" + runs-on: ${{ matrix.os }} + + steps: + - name: Fetch sources + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/${{ matrix.arch }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # This is a workaround to prevent the cache from growing indefinitely. + # https://docs.docker.com/build/ci/github-actions/cache/#local-cache + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Cache container layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-nox-${{ matrix.arch }}-${{ matrix.os }}-${{ matrix.python }} + + - name: Build image + uses: docker/build-push-action@v5 + with: + context: .github/containers/nox-cross-arch + file: .github/containers/nox-cross-arch/${{ matrix.arch }}-${{ matrix.os }}-python-${{ matrix.python }}.Dockerfile + platforms: linux/${{ matrix.arch }} + tags: localhost/nox-cross-arch:latest + push: false + load: true + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + # Refer to the workaround mentioned above + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + # Cache pip downloads + - name: Cache pip downloads + uses: actions/cache@v3 + with: + path: /tmp/pip-cache + key: nox-${{ matrix.nox-session }}-${{ matrix.arch }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('**/pyproject.toml') }} + + # This ensures that the docker container has access to the pip cache. + # Changing the user in the docker-run step causes it to fail due to + # incorrect permissions. Setting the ownership of the pip cache to root + # before running is a workaround to this issue. + - name: Set pip cache owners to root for docker + run: if [[ -e /tmp/pip-cache ]]; then sudo chown -R root:root /tmp/pip-cache; fi + + - name: Run nox + run: | + docker run \ + --rm \ + -v $(pwd):/${{ github.workspace }} \ + -v /tmp/pip-cache:/root/.cache/pip \ + -w ${{ github.workspace }} \ + --net=host \ + --platform linux/${{ matrix.arch }} \ + localhost/nox-cross-arch:latest \ + bash -c "pip install -e .[dev-noxfile]; nox --install-only -e ${{ matrix.nox-session }}; pip freeze; nox -e ${{ matrix.nox-session }}" + timeout-minutes: 30 + + # This ensures that the runner has access to the pip cache. + - name: Reset pip cache ownership + if: always() + run: sudo chown -R $USER:$USER /tmp/pip-cache + + # This job runs if all the `nox-cross-arch` matrix jobs ran and succeeded. + # As the `nox-all` job, its main purpose is to provide a single point of + # reference in branch protection rules, similar to how `nox-all` operates. + # However, there's a crucial difference: the `nox-cross-arch` job is omitted + # in PRs. Without the `nox-cross-arch-all` job, the inner matrix wouldn't be + # expanded in such scenarios. This would lead to the CI indefinitely waiting + # for these jobs to complete due to the branch protection rules, essentially + # causing it to hang. This behavior is tied to a recognized GitHub matrices + # issue when certain jobs are skipped. For a deeper understanding, refer to: + # https://github.com/orgs/community/discussions/9141 + nox-cross-arch-all: + # The job name should match the name of the `nox-cross-arch` job. + name: Cross-arch tests with nox + needs: ["nox-cross-arch"] + runs-on: ubuntu-20.04 + steps: + - name: Return true + run: "true" + + build: + name: Build distribution packages + runs-on: ubuntu-20.04 + steps: + - name: Fetch sources + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + cache: 'pip' + + - name: Install required Python packages + run: | + python -m pip install -U pip + python -m pip install -U build + pip freeze + + - name: Build the source and binary distribution + run: python -m build + + - name: Upload distribution files + uses: actions/upload-artifact@v3 + with: + name: dist-packages + path: dist/ + if-no-files-found: error + + test-installation: + name: Test package installation in different architectures + needs: ["build"] + runs-on: ubuntu-20.04 + steps: + - name: Fetch sources + uses: actions/checkout@v4 + - name: Download package + uses: actions/download-artifact@v3 + with: + name: dist-packages + path: dist + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up docker-buildx + uses: docker/setup-buildx-action@v3 + - name: Test Installation + uses: docker/build-push-action@v5 + with: + context: . + file: .github/containers/test-installation/Dockerfile + platforms: linux/amd64,linux/arm64 + tags: localhost/test-installation + push: false + + test-docs: + name: Test documentation website generation + if: github.event_name != 'push' + runs-on: ubuntu-20.04 + steps: + - name: Fetch sources + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup Git user and e-mail + uses: frequenz-floss/setup-git-user@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + cache: 'pip' + + - name: Install build dependencies + run: | + python -m pip install -U pip + python -m pip install .[dev-mkdocs] + pip freeze + + - name: Generate the documentation + env: + MIKE_VERSION: gh-${{ github.job }} + run: | + mike deploy $MIKE_VERSION + mike set-default $MIKE_VERSION + + - name: Upload site + uses: actions/upload-artifact@v3 + with: + name: docs-site + path: site/ + if-no-files-found: error + + publish-docs: + name: Publish documentation website to GitHub pages + needs: ["nox-all", "nox-cross-arch-all", "test-installation"] + if: github.event_name == 'push' + runs-on: ubuntu-20.04 + permissions: + contents: write + steps: + - name: Fetch sources + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup Git user and e-mail + uses: frequenz-floss/setup-git-user@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + cache: 'pip' + + - name: Install build dependencies + run: | + python -m pip install -U pip + python -m pip install .[dev-mkdocs] + pip freeze + + - name: Calculate and check version + id: mike-version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPO: ${{ github.repository }} + GIT_REF: ${{ github.ref }} + GIT_SHA: ${{ github.sha }} + run: | + python -m frequenz.repo.config.cli.version.mike.info + + - name: Fetch the gh-pages branch + if: steps.mike-version.outputs.version + run: git fetch origin gh-pages --depth=1 + + - name: Build site + if: steps.mike-version.outputs.version + env: + VERSION: ${{ steps.mike-version.outputs.version }} + TITLE: ${{ steps.mike-version.outputs.title }} + ALIASES: ${{ steps.mike-version.outputs.aliases }} + # This is not ideal, we need to define all these variables here + # because we need to calculate all the repository version information + # to be able to show the correct versions in the documentation when + # building it. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPO: ${{ github.repository }} + GIT_REF: ${{ github.ref }} + GIT_SHA: ${{ github.sha }} + run: | + mike deploy --update-aliases --title "$TITLE" "$VERSION" $ALIASES + + - name: Sort site versions + if: steps.mike-version.outputs.version + run: | + git checkout gh-pages + python -m frequenz.repo.config.cli.version.mike.sort versions.json + git commit -a -m "Sort versions.json" + + - name: Publish site + if: steps.mike-version.outputs.version + run: | + git push origin gh-pages + + create-github-release: + name: Create GitHub release + needs: ["publish-docs"] + # Create a release only on tags creation + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + permissions: + # We need write permissions on contents to create GitHub releases and on + # discussions to create the release announcement in the discussion forums + contents: write + discussions: write + runs-on: ubuntu-20.04 + steps: + - name: Download distribution files + uses: actions/download-artifact@v3 + with: + name: dist-packages + path: dist + + - name: Download RELEASE_NOTES.md + run: | + set -ux + gh api \ + -X GET \ + -f ref=$REF \ + -H "Accept: application/vnd.github.raw" \ + "/repos/$REPOSITORY/contents/RELEASE_NOTES.md" \ + > RELEASE_NOTES.md + env: + REF: ${{ github.ref }} + REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create GitHub release + run: | + set -ux + extra_opts= + if echo "$REF_NAME" | grep -- -; then extra_opts=" --prerelease"; fi + gh release create \ + -R "$REPOSITORY" \ + --notes-file RELEASE_NOTES.md \ + --generate-notes \ + $extra_opts \ + $REF_NAME \ + dist/* + env: + REF_NAME: ${{ github.ref_name }} + REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-to-pypi: + name: Publish packages to PyPI + needs: ["create-github-release"] + runs-on: ubuntu-20.04 + permissions: + # For trusted publishing. See: + # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ + id-token: write + steps: + - name: Download distribution files + uses: actions/download-artifact@v3 + with: + name: dist-packages + path: dist + + - name: Publish the Python distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/dco-merge-queue.yml b/.github/workflows/dco-merge-queue.yml new file mode 100644 index 0000000..fb1cd90 --- /dev/null +++ b/.github/workflows/dco-merge-queue.yml @@ -0,0 +1,11 @@ +# Based on https://github.com/hyperledger/besu/pull/5207/files +name: DCO +on: + merge_group: + +jobs: + DCO: + runs-on: ubuntu-latest + if: ${{ github.actor != 'dependabot[bot]' }} + steps: + - run: echo "This DCO job runs on merge_queue event and doesn't check PR contents" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..c844b8d --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,24 @@ +name: Pull Request Labeler + +on: [pull_request_target] + +jobs: + Label: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Labeler + # XXX: !!! SECURITY WARNING !!! + # pull_request_target has write access to the repo, and can read secrets. We + # need to audit any external actions executed in this workflow and make sure no + # checked out code is run (not even installing dependencies, as installing + # dependencies usually can execute pre/post-install scripts). We should also + # only use hashes to pick the action to execute (instead of tags or branches). + # For more details read: + # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # 5.0.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + dot: true diff --git a/.github/workflows/release-notes-check.yml b/.github/workflows/release-notes-check.yml new file mode 100644 index 0000000..cb39183 --- /dev/null +++ b/.github/workflows/release-notes-check.yml @@ -0,0 +1,30 @@ +name: Release Notes Check + +on: + merge_group: + pull_request: + types: + # On by default if you specify no types. + - "opened" + - "reopened" + - "synchronize" + # For `skip-label` only. + - "labeled" + - "unlabeled" + + +jobs: + check-release-notes: + name: Check release notes are updated + runs-on: ubuntu-latest + steps: + - name: Check for a release notes update + if: github.event_name == 'pull_request' + uses: brettcannon/check-for-changed-files@4170644959a21843b31f1181f2a1761d65ef4791 # v1.2.0 + with: + # TODO(cookiecutter): Uncomment the following line for private repositories, otherwise remove it and remove it + # token: ${{ secrets.github_token }} + file-pattern: "RELEASE_NOTES.md" + prereq-pattern: "src/**" + skip-label: "cmd:skip-release-notes" + failure-message: "Missing a release notes update. Please add one or apply the ${skip-label} label to the pull request" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6997f69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,150 @@ +# 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 +.vscode + +# 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/ +.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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +# direnv https://github.com/direnv/direnv +.envrc +.direnv/ + +# 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 +.idea + +# Automatically generated documentation +docs/reference/ +site/ diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..db11f95 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,8 @@ +# Each line is a file pattern followed by one or more owners. +# Owners will be requested for review when someone opens a pull request. + +# Fallback owner. +# These are the default owners for everything in the repo, unless a later match +# takes precedence. +# TODO(cookiecutter): Add more specific code-owners, check if the default is correct +* @frequenz-floss/api-dispatch-team diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a30eafc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,183 @@ +# Contributing to Dispatch Highlevel Interface + +## Build + +You can use `build` to simply build the source and binary distribution: + +```sh +python -m pip install build +python -m build +``` + +## Local development + +You can use editable installs to develop the project locally (it will install +all the dependencies too): + +```sh +python -m pip install -e . +``` + +Or you can install all development dependencies (`mypy`, `pylint`, `pytest`, +etc.) in one go too: +```sh +python -m pip install -e .[dev] +``` + +If you don't want to install all the dependencies, you can also use `nox` to +run the tests and other checks creating its own virtual environments: + +```sh +python -m pip install .[dev-noxfile] +nox +``` + +You can also use `nox -R` to reuse the current testing environment to speed up +test at the expense of a higher chance to end up with a dirty test environment. + +### Running tests / checks individually + +For a better development test cycle you can install the runtime and test +dependencies and run `pytest` manually. + +```sh +python -m pip install .[dev-pytest] # included in .[dev] too + +# And for example +pytest tests/test_*.py +``` + +Or you can use `nox`: + +```sh +nox -R -s pytest -- test/test_*.py +``` + +The same appliest to `pylint` or `mypy` for example: + +```sh +nox -R -s pylint -- test/test_*.py +nox -R -s mypy -- test/test_*.py +``` + +### Building the documentation + +To build the documentation, first install the dependencies (if you didn't +install all `dev` dependencies): + +```sh +python -m pip install -e .[dev-mkdocs] +``` + +Then you can build the documentation (it will be written in the `site/` +directory): + +```sh +mkdocs build +``` + +Or you can just serve the documentation without building it using: + +```sh +mkdocs serve +``` + +Your site will be updated **live** when you change your files (provided that +you used `pip install -e .`, beware of a common pitfall of using `pip install` +without `-e`, in that case the API reference won't change unless you do a new +`pip install`). + +To build multi-version documentation, we use +[mike](https://github.com/jimporter/mike). If you want to see how the +multi-version sites looks like locally, you can use: + +```sh +mike deploy my-version +mike set-default my-version +mike serve +``` + +`mike` works in mysterious ways. Some basic information: + +* `mike deploy` will do a `mike build` and write the results to your **local** + `gh-pages` branch. `my-version` is an arbitrary name for the local version + you want to preview. +* `mike set-default` is needed so when you serve the documentation, it goes to + your newly produced documentation by default. +* `mike serve` will serve the contents of your **local** `gh-pages` branch. Be + aware that, unlike `mkdocs serve`, changes to the sources won't be shown + live, as the `mike deploy` step is needed to refresh them. + +Be careful not to use `--push` with `mike deploy`, otherwise it will push your +local `gh-pages` branch to the `origin` remote. + +That said, if you want to test the actual website in **your fork**, you can +always use `mike deploy --push --remote your-fork-remote`, and then access the +GitHub pages produced for your fork. + +## Releasing + +These are the steps to create a new release: + +1. Get the latest head you want to create a release from. + +2. Update the `RELEASE_NOTES.md` file if it is not complete, up to date, and + remove template comments (` + +## Upgrading + + + +## New Features + + + +## Bug Fixes + + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..ea38c9b --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1 @@ +--8<-- "CONTRIBUTING.md" diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000..3755def --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,3 @@ +* [Home](index.md) +* [API Reference](reference/) +* [Contributing](CONTRIBUTING.md) diff --git a/docs/_css/mkdocstrings.css b/docs/_css/mkdocstrings.css new file mode 100644 index 0000000..572abff --- /dev/null +++ b/docs/_css/mkdocstrings.css @@ -0,0 +1,44 @@ +/* Recommended style from: + * https://mkdocstrings.github.io/python/customization/#recommended-style-material + * With some additions from: + * https://github.com/mkdocstrings/mkdocstrings/blob/master/docs/css/mkdocstrings.css + */ + +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid var(--md-typeset-table-color); +} + +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: 4px solid rgba(230, 230, 230); + margin-bottom: 80px; +} + +/* Avoid breaking parameters name, etc. in table cells. */ +td code { + word-break: normal !important; +} + +/* Mark external links as such. */ +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + background-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + position: relative; + top: 0.1em; + margin-left: 0.2em; + margin-right: 0.1em; + + height: 1em; + width: 1em; + border-radius: 100%; + background-color: var(--md-typeset-a-color); +} +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} diff --git a/docs/_css/style.css b/docs/_css/style.css new file mode 100644 index 0000000..c6b2eac --- /dev/null +++ b/docs/_css/style.css @@ -0,0 +1,70 @@ +/* Based on: + * https://github.com/mkdocstrings/mkdocstrings/blob/master/docs/css/style.css + */ + +/* Increase logo size */ +.md-header__button.md-logo { + padding-bottom: 0.2rem; + padding-right: 0; +} +.md-header__button.md-logo img { + height: 1.5rem; +} + +/* Mark external links as such (also in nav) */ +a.external:hover::after, a.md-nav__link[href^="https:"]:hover::after { + /* https://primer.style/octicons/link-external-16 */ + background-image: url('data:image/svg+xml,'); + height: 0.8em; + width: 0.8em; + margin-left: 0.2em; + content: ' '; + display: inline-block; +} + +/* More space at the bottom of the page */ +.md-main__inner { + margin-bottom: 1.5rem; +} + +/* Code annotations with numbers. + * + * Normally annotations are shown with a (+) button that expands the + * annotation. To be able to explain code step by step, it is good to have + * annotations with numbers, to be able to follow the notes in a particular + * order. + * + * To do this, we need some custom CSS rules. Before this customization was + * officially supported and documented, but now they are not officially + * supported anymore, so it could eventually break (it already did once). + * + * If that happens we either need to look into how to fix the CSS ourselves or + * remove the feature. To do the customization, this is what we should be able + * to count on: + * + * "you can be sure that the data-md-annotation-id attribute will always be + * present in the source, which means you can always number them in any way you + * like." + * + * Code annotation are described here: + * https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#code-annotations + * + * Here are the original docs on how to enable numbered annotations: + * https://web.archive.org/web/20230724161216/https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#annotations-with-numbers + * + * This is the PR fixing the numbered annotations when they broke: + * https://github.com/frequenz-floss/frequenz-sdk-python/pull/684 + * + * And this is the reported regression when it was decided to drop support for + * numbered annotations officially: + * https://github.com/squidfunk/mkdocs-material/issues/6042 + */ +.md-typeset .md-annotation__index > ::before { + content: attr(data-md-annotation-id); +} +.md-typeset :focus-within > .md-annotation__index > ::before { + transform: none; +} +.md-typeset .md-annotation__index { + width: 4ch; +} diff --git a/docs/_img/logo.png b/docs/_img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7a9db3640ab3646f609b5607cbbaa1f795930e40 GIT binary patch literal 57278 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/_overrides/main.html b/docs/_overrides/main.html new file mode 100644 index 0000000..980f3ab --- /dev/null +++ b/docs/_overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block outdated %} + You're not viewing the latest (stable) version. + + Click here to go to latest (stable) version + +{% endblock %} diff --git a/docs/_scripts/macros.py b/docs/_scripts/macros.py new file mode 100644 index 0000000..ea0387c --- /dev/null +++ b/docs/_scripts/macros.py @@ -0,0 +1,83 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""This module defines macros for use in Markdown files.""" + +from typing import Any + +import markdown as md +from markdown.extensions import toc +from mkdocs_macros import plugin as macros + +_CODE_ANNOTATION_MARKER: str = ( + r'' + r'' + r'' + r"" + r"" +) + + +def _slugify(text: str) -> str: + """Slugify a text. + + Args: + text: The text to slugify. + + Returns: + The slugified text. + """ + # The type of the return value is not defined for the markdown library. + # Also for some reason `mypy` thinks the `toc` module doesn't have a + # `slugify_unicode` function, but it definitely does. + return toc.slugify_unicode(text, "-") # type: ignore[attr-defined,no-any-return] + + +def _hook_macros_plugin(env: macros.MacrosPlugin) -> None: + """Integrate the `mkdocs-macros` plugin into `mkdocstrings`. + + This is a temporary workaround to make `mkdocs-macros` work with + `mkdocstrings` until a proper `mkdocs-macros` *pluglet* is available. See + https://github.com/mkdocstrings/mkdocstrings/issues/615 for details. + + Args: + env: The environment to hook the plugin into. + """ + # get mkdocstrings' Python handler + python_handler = env.conf["plugins"]["mkdocstrings"].get_handler("python") + + # get the `update_env` method of the Python handler + update_env = python_handler.update_env + + # override the `update_env` method of the Python handler + def patched_update_env(markdown: md.Markdown, config: dict[str, Any]) -> None: + update_env(markdown, config) + + # get the `convert_markdown` filter of the env + convert_markdown = python_handler.env.filters["convert_markdown"] + + # build a chimera made of macros+mkdocstrings + def render_convert(markdown: str, *args: Any, **kwargs: Any) -> Any: + return convert_markdown(env.render(markdown), *args, **kwargs) + + # patch the filter + python_handler.env.filters["convert_markdown"] = render_convert + + # patch the method + python_handler.update_env = patched_update_env + + +def define_env(env: macros.MacrosPlugin) -> None: + """Define the hook to create macro functions for use in Markdown. + + Args: + env: The environment to define the macro functions in. + """ + # A variable to easily show an example code annotation from mkdocs-material. + # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations + env.variables["code_annotation_marker"] = _CODE_ANNOTATION_MARKER + + # TODO(cookiecutter): Add any other macros, variables and filters here. + + # This hook needs to be done at the end of the `define_env` function. + _hook_macros_plugin(env) diff --git a/docs/_scripts/mkdocstrings_autoapi.py b/docs/_scripts/mkdocstrings_autoapi.py new file mode 100644 index 0000000..1f7beb9 --- /dev/null +++ b/docs/_scripts/mkdocstrings_autoapi.py @@ -0,0 +1,8 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Generate the code reference pages.""" + +from frequenz.repo.config.mkdocs import api_pages + +api_pages.generate_python_api_pages("src", "reference") diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..612c7a5 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +--8<-- "README.md" diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..1416315 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,138 @@ +# MkDocs configuration +# For details see: https://www.mkdocs.org/user-guide/configuration/ + +# Project information +site_name: "Dispatch Highlevel Interface" +site_description: "A highlevel interface for the dispatch API" +site_author: "Frequenz Energy-as-a-Service GmbH" +copyright: "Copyright © 2024 Frequenz Energy-as-a-Service GmbH" +repo_name: "frequenz-dispatch-python" +repo_url: "https://github.com/frequenz-floss/frequenz-dispatch-python" +# TODO(cookiecutter): "main" is the GitHub repo default branch, you might want to update it +# if the project uses a different default branch. +edit_uri: "edit/main/docs/" +strict: true # Treat warnings as errors + +# Build directories +theme: + name: "material" + # TODO(cookiecutter): You might want to change the logo, the file is located in "docs/" + logo: _img/logo.png + favicon: _img/logo.png + language: en + icon: + edit: material/file-edit-outline + repo: fontawesome/brands/github + custom_dir: docs/_overrides + features: + - content.code.annotate + - content.code.copy + - navigation.indexes + - navigation.instant + - navigation.footer + - navigation.tabs + - navigation.top + - navigation.tracking + - toc.follow + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: deep purple + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: teal + toggle: + icon: material/weather-night + name: Switch to light mode + +extra: + # TODO(cookiecutter): You probably want to update the social links + social: + - icon: fontawesome/brands/github + link: https://github.com/frequenz-floss + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/company/frequenz-com + version: + provider: mike + default: latest + +extra_css: + - _css/style.css + - _css/mkdocstrings.css + +# Formatting options +markdown_extensions: + - admonition + - attr_list + - def_list + - footnotes + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.keys + - pymdownx.snippets: + check_paths: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed + - pymdownx.tasklist: + custom_checkbox: true + - toc: + permalink: "¤" + +plugins: + - gen-files: + scripts: + - docs/_scripts/mkdocstrings_autoapi.py + - literate-nav: + nav_file: SUMMARY.md + - mike: + alias_type: redirect + canonical_version: latest + - mkdocstrings: + default_handler: python + handlers: + python: + options: + paths: ["src"] + docstring_section_style: spacy + inherited_members: true + merge_init_into_class: false + separate_signature: true + show_category_heading: true + show_root_heading: true + show_root_members_full_path: true + show_signature_annotations: true + show_source: true + signature_crossrefs: true + import: + # TODO(cookiecutter): You might want to add other external references here + # See https://mkdocstrings.github.io/python/usage/#import for details + - https://docs.python.org/3/objects.inv + - https://frequenz-floss.github.io/frequenz-channels-python/v0.16/objects.inv + - https://frequenz-floss.github.io/frequenz-sdk-python/v0.25/objects.inv + - https://typing-extensions.readthedocs.io/en/stable/objects.inv + # Note this plugin must be loaded after mkdocstrings to be able to use macros + # inside docstrings. See the comment in `docs/_scripts/macros.py` for more + # details + - macros: + module_name: docs/_scripts/macros + on_undefined: strict + on_error_fail: true + - search + +# Preview controls +watch: + - "src" + - README.md + - CONTRIBUTING.md diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..0720f38 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,8 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Configuration file for nox.""" + +from frequenz.repo.config import RepositoryType, nox + +nox.configure(RepositoryType.ACTOR) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9469c7d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,170 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +[build-system] +requires = [ + "setuptools == 68.1.0", + "setuptools_scm[toml] == 7.1.0", + "frequenz-repo-config[actor] == 0.8.0", +] +build-backend = "setuptools.build_meta" + +[project] +name = "frequenz-actor-dispatch" +description = "A highlevel interface for the dispatch API" +readme = "README.md" +license = { text = "MIT" } +keywords = ["frequenz", "python", "actor", "frequenz-dispatch", "dispatch", "highlevel", "api"] +# TODO(cookiecutter): Remove and add more classifiers if appropriate +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries", + "Typing :: Typed", +] +requires-python = ">= 3.11, < 4" +# TODO(cookiecutter): Remove and add more dependencies if appropriate +dependencies = [ + "typing-extensions == 4.5.0", + # Make sure to update the version for cross-referencing also in the + # mkdocs.yml file when changing the version here (look for the config key + # plugins.mkdocstrings.handlers.python.import) + "frequenz-sdk == 0.25.0", +] +dynamic = ["version"] + +[[project.authors]] +name = "Frequenz Energy-as-a-Service GmbH" +email = "floss@frequenz.com" + +# TODO(cookiecutter): Remove and add more optional dependencies if appropriate +[project.optional-dependencies] +dev-flake8 = [ + "flake8 == 6.1.0", + "flake8-docstrings == 1.7.0", + "flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml + "pydoclint == 0.3.2", + "pydocstyle == 6.3.0", +] +dev-formatting = ["black == 23.9.1", "isort == 5.12.0"] +dev-mkdocs = [ + "black == 23.9.1", + "Markdown==3.4.4", + "mike == 2.0.0", + "mkdocs-gen-files == 0.5.0", + "mkdocs-literate-nav == 0.6.1", + "mkdocs-macros-plugin == 1.0.4", + "mkdocs-material == 9.3.1", + "mkdocstrings[python] == 0.23.0", + "frequenz-repo-config[actor] == 0.8.0", +] +dev-mypy = [ + "mypy == 1.5.1", + "types-Markdown == 3.4.2.10", + # For checking the noxfile, docs/ script, and tests + "frequenz-actor-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]", +] +dev-noxfile = [ + "nox == 2023.4.22", + "frequenz-repo-config[actor] == 0.8.0", +] +dev-pylint = [ + "pylint == 3.0.2", + # For checking the noxfile, docs/ script, and tests + "frequenz-actor-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]", +] +dev-pytest = [ + "pytest == 8.0.0", + "frequenz-repo-config[extra-lint-examples] == 0.8.0", + "pytest-mock == 3.11.1", + "pytest-asyncio == 0.21.1", + "async-solipsism == 0.5", +] +dev = [ + "frequenz-actor-dispatch[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]", +] + +[project.urls] +Documentation = "https://frequenz-floss.github.io/frequenz-dispatch-python/" +Changelog = "https://github.com/frequenz-floss/frequenz-dispatch-python/releases" +Issues = "https://github.com/frequenz-floss/frequenz-dispatch-python/issues" +Repository = "https://github.com/frequenz-floss/frequenz-dispatch-python" +Support = "https://github.com/frequenz-floss/frequenz-dispatch-python/discussions/categories/support" + +[tool.black] +line-length = 88 +target-version = ['py311'] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +line_length = 88 +src_paths = ["benchmarks", "examples", "src", "tests"] + +[tool.flake8] +# We give some flexibility to go over 88, there are cases like long URLs or +# code in documenation that have extra indentation. Black will still take care +# of making everything that can be 88 wide, 88 wide. +max-line-length = 100 +extend-ignore = [ + "E203", # Whitespace before ':' (conflicts with black) + "W503", # Line break before binary operator (conflicts with black) +] +# pydoclint options +style = "google" +check-return-types = false +check-yield-types = false +arg-type-hints-in-docstring = false +arg-type-hints-in-signature = true +allow-init-docstring = true + +[tool.pylint.similarities] +ignore-comments = ['yes'] +ignore-docstrings = ['yes'] +ignore-imports = ['no'] +min-similarity-lines = 40 + +[tool.pylint.messages_control] +disable = [ + "too-few-public-methods", + "too-many-return-statements", + # disabled because it conflicts with isort + "wrong-import-order", + "ungrouped-imports", + # pylint's unsubscriptable check is buggy and is not needed because + # it is a type-check, for which we already have mypy. + "unsubscriptable-object", + # Checked by flake8 + "line-too-long", + "redefined-outer-name", + "unnecessary-lambda-assignment", + "unused-import", + "unused-variable", +] + +[tool.pytest.ini_options] +testpaths = ["tests", "src"] +asyncio_mode = "auto" +required_plugins = ["pytest-asyncio", "pytest-mock"] + +[tool.mypy] +explicit_package_bases = true +namespace_packages = true +# This option disables mypy cache, and it is sometimes useful to enable it if +# you are getting weird intermittent error, or error in the CI but not locally +# (or vice versa). In particular errors saying that type: ignore is not +# used but getting the original ignored error when removing the type: ignore. +# See for example: https://github.com/python/mypy/issues/2960 +#no_incremental = true +packages = ["frequenz.actor.dispatch"] +strict = true + +[[tool.mypy.overrides]] +module = ["mkdocs_macros.*", "sybil", "sybil.*"] +ignore_missing_imports = true + +[tool.setuptools_scm] +version_scheme = "post-release" diff --git a/src/frequenz/actor/dispatch/__init__.py b/src/frequenz/actor/dispatch/__init__.py new file mode 100644 index 0000000..d1d8611 --- /dev/null +++ b/src/frequenz/actor/dispatch/__init__.py @@ -0,0 +1,25 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""A highlevel interface for the dispatch API. + +TODO(cookiecutter): Add a more descriptive module description. +""" + + +# TODO(cookiecutter): Remove this function +def delete_me(*, blow_up: bool = False) -> bool: + """Do stuff for demonstration purposes. + + Args: + blow_up: If True, raise an exception. + + Returns: + True if no exception was raised. + + Raises: + RuntimeError: if blow_up is True. + """ + if blow_up: + raise RuntimeError("This function should be removed!") + return True diff --git a/src/frequenz/actor/dispatch/conftest.py b/src/frequenz/actor/dispatch/conftest.py new file mode 100644 index 0000000..c84af78 --- /dev/null +++ b/src/frequenz/actor/dispatch/conftest.py @@ -0,0 +1,13 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Validate docstring code examples. + +Code examples are often wrapped in triple backticks (```) within docstrings. +This plugin extracts these code examples and validates them using pylint. +""" + +from frequenz.repo.config.pytest import examples +from sybil import Sybil + +pytest_collect_file = Sybil(**examples.get_sybil_arguments()).pytest() diff --git a/src/frequenz/actor/dispatch/py.typed b/src/frequenz/actor/dispatch/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_frequenz_dispatch.py b/tests/test_frequenz_dispatch.py new file mode 100644 index 0000000..126a38a --- /dev/null +++ b/tests/test_frequenz_dispatch.py @@ -0,0 +1,18 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Tests for the frequenz.actor.dispatch package.""" +import pytest + +from frequenz.actor.dispatch import delete_me + + +def test_frequenz_dispatch_succeeds() -> None: # TODO(cookiecutter): Remove + """Test that the delete_me function succeeds.""" + assert delete_me() is True + + +def test_frequenz_dispatch_fails() -> None: # TODO(cookiecutter): Remove + """Test that the delete_me function fails.""" + with pytest.raises(RuntimeError, match="This function should be removed!"): + delete_me(blow_up=True)