diff --git a/.flake8 b/.flake8 index c430446..e21fafd 100644 --- a/.flake8 +++ b/.flake8 @@ -4,7 +4,3 @@ # E501 line too long (83 > 79 characters) exclude = .git,.pycache,build,.eggs - -per-file-ignores = - ./src/actinia_example_plugin/wsgi.py: F401 - ./tests/test_resource_base.py: F401 diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index fd65518..0000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Python code style check with black - -on: [push] - -# only one run per PR/branch happens at a time, cancelling the old run when a new one starts -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - black: - - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@v4 - - name: Install apt dependencies - run: | - sudo apt-get update && sudo apt-get install python3 python3-pip -y - - name: Install pip dependencies - run: | - pip3 install black==23.1.0 - - name: Check code style with Black - run: | - black --check --diff --line-length 79 . diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml deleted file mode 100644 index 03141ae..0000000 --- a/.github/workflows/flake8.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Python Flake8 Code Quality - -on: [push] - -# only one run per PR/branch happens at a time, cancelling the old run when a new one starts -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - flake8-actinia: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.8 - - name: Install - run: | - python -m pip install --upgrade pip - pip install flake8==3.8.0 - - name: Run Flake8 - run: | - flake8 --config=.flake8 --count --statistics --show-source --jobs=$(nproc) . diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 5e37657..d51831c 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,17 +1,13 @@ --- name: Linting and code quality check -on: [push, pull_request] +on: + push: + branches: + - main + - develop + pull_request: jobs: lint: uses: mundialis/github-workflows/.github/workflows/linting.yml@main - # with: - # # set pylint-version to empty string to skip the pylint workflow - # pylint-version: '' - # BASH_SEVERITY: 'warning' - # VALIDATE_DOCKERFILE_HADOLINT: false - # VALIDATE_JSON: false - # VALIDATE_HTML: false - # VALIDATE_CSS: false - # VALIDATE_BASH_EXEC: false diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 74f7ddf..3dce8ca 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,42 +1,13 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package +name: Upload Python Package to test PyPI on: release: types: [published] jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python3 -m build --outdir build . - - name: Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: build/*.whl - - # - name: Publish package - # uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - # with: - # user: __token__ - # password: ${{ secrets.PYPI_API_TOKEN }} + publish-python: + uses: mundialis/github-workflows/.github/workflows/python-publish.yml@main + with: + test_pypi: true + secrets: + PYPI_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml deleted file mode 100644 index c85e40c..0000000 --- a/.github/workflows/super-linter.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: General linting - -on: [push] - -# only one run per PR/branch happens at a time, cancelling the old run when a new one starts -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - super-linter: - name: GitHub Super Linter - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Lint code base - uses: github/super-linter@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Listed but commented out linters would be nice to have. - # (see https://github.com/github/super-linter#environment-variables) - # - # Python (supported using Pylint) and C/C++ (not supported) are - # handled separately due to the complexity of the settings. - # VALIDATE_BASH: true - # VALIDATE_CSS: true - # VALIDATE_DOCKER: true - VALIDATE_JAVASCRIPT_ES: true - # VALIDATE_JAVASCRIPT_STANDARD: true - VALIDATE_JSON: true - VALIDATE_MARKDOWN: true - VALIDATE_POWERSHELL: true - # VALIDATE_XML: true - VALIDATE_YAML: true - FILTER_REGEX_EXCLUDE: ./config/templates/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 32e7c9e..cbe6bd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,3 +1,5 @@ +--- + name: actinia tests on: @@ -14,8 +16,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - # with: - # path: "." - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Replace run only unittest command diff --git a/.gitignore b/.gitignore index e1798f6..7bfa463 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,9 @@ sdist/* # docker docker/redis_data/dump.rdb !docker/actinia-example-plugin-test/actinia-example-plugin-test.cfg + +# linting with shared config file +.pylintrc +.pylintrc_allowed_to_fail +ruff-github-workflows.toml +ruff-merged.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7d6a505 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + exclude: | + (?x)^( + .*\.ref$| + .*\.svg$| + build/| + dist/| + src/actinia_core.egg-info/ + ) + - id: end-of-file-fixer + exclude: | + (?x)^( + .*\.ref$| + .*\.svg$| + build/| + dist/| + src/actinia_core.egg-info/ + ) + - repo: https://github.com/mundialis/github-workflows + rev: 1.4.0 + hooks: + - id: linting diff --git a/docker/Dockerfile b/docker/Dockerfile index 5b868f7..f678e63 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ -FROM mundialis/actinia-core:latest +FROM mundialis/actinia-core:5.0.0 # pwgen is needed for the tests -RUN pip3 install pwgen +RUN pip3 install --no-cache-dir pwgen==0.8.2.post0 COPY docker/actinia.cfg /etc/default/actinia COPY src /src/actinia-example-plugin/src/ @@ -9,9 +9,13 @@ COPY setup.cfg /src/actinia-example-plugin/ COPY setup.py /src/actinia-example-plugin/ COPY requirements.txt /src/actinia-example-plugin/ -RUN pip3 install -r /src/actinia-example-plugin/requirements.txt -RUN pip3 uninstall actinia-example-plugin.wsgi -y +RUN pip3 install --no-cache-dir -r /src/actinia-example-plugin/requirements.txt && \ + pip3 uninstall actinia-example-plugin.wsgi -y # SETUPTOOLS_SCM_PRETEND_VERSION is only needed if in the plugin folder is no # .git folder ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0 -RUN (cd /src/actinia-example-plugin && python3 setup.py install) + +WORKDIR /src/actinia-example-plugin +RUN python3 setup.py install + +WORKDIR /src/actinia_core \ No newline at end of file diff --git a/docker/actinia-example-plugin-test/Dockerfile b/docker/actinia-example-plugin-test/Dockerfile index c24d4ac..5d5ea4d 100644 --- a/docker/actinia-example-plugin-test/Dockerfile +++ b/docker/actinia-example-plugin-test/Dockerfile @@ -1,16 +1,16 @@ -FROM mundialis/actinia-core:latest as actinia_test +FROM mundialis/actinia-core:5.0.0 as actinia_test LABEL authors="Carmen Tawalika,Anika Weinmann" LABEL maintainer="tawalika@mundialis.de,weinmann@mundialis.de" -ENV ACTINIA_CUSTOM_TEST_CFG /etc/default/actinia-example-plugin-test +ENV ACTINIA_CUSTOM_TEST_CFG=/etc/default/actinia-example-plugin-test # TODO do not set DEFAULT_CONFIG_PATH if this is fixed -ENV DEFAULT_CONFIG_PATH /etc/default/actinia-example-plugin-test +ENV DEFAULT_CONFIG_PATH=/etc/default/actinia-example-plugin-test # install things only for tests -RUN apk add redis -RUN pip3 install iniconfig colorlog pwgen +RUN apk add --no-cache redis==7.0.15-r1 && \ + pip3 install --no-cache-dir iniconfig==2.0.0 colorlog==6.8.2 pwgen==0.8.2.post0 # COPY docker/actinia-example-plugin-test/start.sh /src/start.sh @@ -31,7 +31,6 @@ COPY . /src/actinia-example-plugin/ WORKDIR /src/actinia-example-plugin/ -RUN chmod a+x tests_with_redis.sh -RUN make install +RUN chmod a+x tests_with_redis.sh && make install # RUN make test diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..610ffe2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "actinia-example-plugin" +version = "1.0.0" +description = "An actinia-core plugin which adds example endpoints to actinia-core" +readme = "README.md" +authors = [ + { name = "Carmen Tawalika"}, + { name = "Anika Weinmann"}, +] +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", +] +requires-python = ">=3.8" +keywords = [ + "processing", + "earth observation", + "cloud-based processing", + "rest api", + "gis", + "grass gis", + "osgeo", + "example", +] +dependencies = [ + "colorlog>=4.2.1", + "xmltodict", +] + +[project.optional-dependencies] +test = [ + "pytest", + "pytest-cov", +] + +[project.urls] +Homepage = "https://github.com/mundialis/actinia-example-plugin" +Tutorial = "https://mundialis.github.io/actinia_core" +API_Docs = "https://redocly.github.io/redoc/?url=https://actinia.mundialis.de/latest/swagger.json" + +[tool.flake8] +max-line-length = 79 + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "--cov actinia_module_plugin --cov-report term-missing --verbose --tb=line -x -s" +testpaths = [ + "tests", +] +markers = [ + "dev: test current in development", + "unittest: completely independent test", + "integrationtest: integration test", +] diff --git a/renovate.json b/renovate.json index 39a2b6e..dfe6ee4 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,9 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" - ] + "config:recommended" + ], + "pre-commit": { + "enabled": true + } } diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..8ed9d87 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,4 @@ +lint.ignore = ["PLR0913", "PLR0917", "PLW0603", "S107", "S606"] + +[lint.per-file-ignores] +"tests/testsuite.py" = [ "PLC0415",] \ No newline at end of file diff --git a/scripts/create_own_plugin.sh b/scripts/create_own_plugin.sh old mode 100644 new mode 100755 index 9a5b8a4..224f8ba --- a/scripts/create_own_plugin.sh +++ b/scripts/create_own_plugin.sh @@ -40,28 +40,28 @@ PLUGIN_NAME=$1 PLUGIN_NAME2=$(tr -s '-' '_' <<< "${PLUGIN_NAME}") -if [ ! -v $2 ] && [ $2 == "git" ] -then - GIT_NAME="" - ORG_MSG="" - # if [[ -v $3 ]] - # then - # GIT_ORGANIZATION=$3 - # ORG_MSG=" in the organization ${GIT_ORGANIZATION}" - # GIT_NAME="${GIT_ORGANIZATION}/${PLUGIN_NAME}" - # else - # GIT_NAME=${PLUGIN_NAME} - # fi - echo "GIT repository will be created${ORG_MSG}" -else - echo "No GIT repository will be created" -fi +# if [ ! -v "$2" ] && [ "$2" == "git" ] +# then +# GIT_NAME="" +# ORG_MSG="" +# # if [[ -v $3 ]] +# # then +# # GIT_ORGANIZATION=$3 +# # ORG_MSG=" in the organization ${GIT_ORGANIZATION}" +# # GIT_NAME="${GIT_ORGANIZATION}/${PLUGIN_NAME}" +# # else +# # GIT_NAME=${PLUGIN_NAME} +# # fi +# echo "GIT repository ${GIT_NAME} will be created${ORG_MSG}" +# else +# echo "No GIT repository will be created" +# fi # git clone git@github.com:mundialis/actinia-example-plugin.git -git clone https://github.com/mundialis/actinia-example-plugin.git ${PLUGIN_NAME} +git clone https://github.com/mundialis/actinia-example-plugin.git "${PLUGIN_NAME}" -cd ${PLUGIN_NAME} +cd "${PLUGIN_NAME}" || exit rm -rf .git rm -f scripts/create_own_plugin.sh @@ -75,21 +75,19 @@ for file in $(find . -name '*actinia-example-plugin*' | sort --reverse) do DIR=$(dirname "${file}") BASENAME=$(basename "${file}") - new_name=$(echo ${BASENAME} | sed "s+actinia-example-plugin+${PLUGIN_NAME}+g") - # echo "${file} ${DIR}/${new_name}" - mv ${file} ${DIR}/${new_name} + new_name=${BASENAME//actinia-example-plugin/$PLUGIN_NAME} + mv "${file}" "${DIR}"/"${new_name}" done for file in $(find . -name '*actinia_example_plugin*' | sort --reverse) do DIR=$(dirname "${file}") BASENAME=$(basename "${file}") - new_name=$(echo ${BASENAME} | sed "s+actinia_example_plugin+${PLUGIN_NAME2}+g") - # echo "${file} ${DIR}/${new_name}" - mv ${file} ${DIR}/${new_name} + new_name=${BASENAME//actinia_example_plugin/$PLUGIN_NAME2} + mv "${file}" "${DIR}"/"${new_name}" done # create git repo -if [ ! -v $2 ] && [ $2 == "git" ] +if [ ! -v "$2" ] && [ "$2" == "git" ] then git init git add . && git commit -m "actinia plugin created from https://github.com/mundialis/actinia-example-plugin" diff --git a/setup.cfg b/setup.cfg index e0a90a3..77ccd84 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,116 +1,11 @@ -# This file is used to configure your project. -# Read more about the various options under: -# http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files - -[metadata] -name = actinia_example_plugin.wsgi -description = actinia example plugin -author = Anika Weinmann -author-email = aweinmann@mundialis.de -license = mit -long-description = file: README.md -long-description-content-type = text/x-rst; charset=UTF-8 -url = https://github.com/pyscaffold/pyscaffold/ -project-urls = - Documentation = https://pyscaffold.org/ -# Change if running only on Windows, Mac or Linux (comma-separated) -platforms = any -# Add here all kinds of additional classifiers as defined under -# https://pypi.python.org/pypi?%3Aaction=list_classifiers -classifiers = - Development Status :: 4 - Beta - Programming Language :: Python - [options] zip_safe = False -packages = find_namespace: +packages = find: include_package_data = True package_dir = =src -# DON'T CHANGE THE FOLLOWING LINE! IT WILL BE UPDATED BY PYSCAFFOLD! -setup_requires = pyscaffold>=3.2a0,<3.3a0 -# Add here dependencies of your project (semicolon/line-separated), e.g. -# install_requires = numpy; scipy -# The usage of test_requires is discouraged, see `Dependency Management` docs -# tests_require = pytest; pytest-cov -# Require a specific Python version, e.g. Python 2.7 or >= 3.4 -# python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* [options.packages.find] where = src exclude = tests - -[options.extras_require] -# Add here additional requirements for extra features, to install with: -# `pip install actinia-example-plugin[PDF]` like: -# PDF = ReportLab; RXP -# Add here test requirements (semicolon/line-separated) -testing = - pytest - pytest-cov - -[options.entry_points] -# Add here console scripts like: -# console_scripts = -# script_name = actinia_example_plugin.module:function -# For example: -# console_scripts = -# fibonacci = actinia_example_plugin.skeleton:run -# And any other entry points, for example: -# pyscaffold.cli = -# awesome = pyscaffoldext.awesome.extension:AwesomeExtension - -[test] -# py.test options when running `python setup.py test` -# addopts = --verbose -extras = True - -[tool:pytest] -# Options for py.test: -# Specify command line options as you would do when invoking py.test directly. -# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml -# in order to write a coverage file that can be read by Jenkins. -addopts = - --cov actinia_example_plugin --cov-report term-missing - --verbose --tb=line -x -s -norecursedirs = - dist - build - .tox -markers = - dev: test current in development - unittest: completely independent test - integrationtest: integration test - -[aliases] -dists = bdist_wheel - -[bdist_wheel] -# Use this option if your package is pure-python -universal = 1 - -[build_sphinx] -source_dir = docs -build_dir = build/sphinx - -[devpi:upload] -# Options for the devpi: PyPI server and packaging tool -# VCS export must be deactivated since we are using setuptools-scm -no-vcs = 1 -formats = bdist_wheel - -[flake8] -# Some sane defaults for the code style checker flake8 -exclude = - .tox - build - dist - .eggs - docs/conf.py - -[pyscaffold] -# PyScaffold's parameters when the project was created. -# This will be used when updating. Do not change! -version = 3.2.3 -package = actinia_example_plugin diff --git a/setup.py b/setup.py index c94a9e4..ea13c3a 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,23 @@ -# -*- coding: utf-8 -*- -""" - Setup file for actinia_example_plugin. - Use setup.cfg to configure your project. +#!/usr/bin/env python +"""Copyright (c) 2024 mundialis GmbH & Co. KG. - This file was generated with PyScaffold 3.2.3. - PyScaffold helps you to put up the scaffold of your new Python project. - Learn more under: https://pyscaffold.org/ -""" -import sys +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -from pkg_resources import VersionConflict, require -from setuptools import setup +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. -try: - require("setuptools>=38.3") -except VersionConflict: - print("Error: version of setuptools is too old (<38.3)!") - sys.exit(1) +You should have received a copy of the GNU General Public License +along with this program. If not, see . +Plugin setup file +""" + +from setuptools import setup if __name__ == "__main__": - setup(use_pyscaffold=True) + setup() diff --git a/src/actinia_example_plugin/__init__.py b/src/actinia_example_plugin/__init__.py index bb92c58..16ff8b7 100644 --- a/src/actinia_example_plugin/__init__.py +++ b/src/actinia_example_plugin/__init__.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,10 +20,10 @@ __license__ = "GPLv3" __author__ = "Carmen Tawalika, Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" -from pkg_resources import get_distribution, DistributionNotFound +from pkg_resources import DistributionNotFound, get_distribution try: # Change here if project is renamed and does not equal the package name diff --git a/src/actinia_example_plugin/api/__init__.py b/src/actinia_example_plugin/api/__init__.py index e69de29..ea6c7b2 100644 --- a/src/actinia_example_plugin/api/__init__.py +++ b/src/actinia_example_plugin/api/__init__.py @@ -0,0 +1,4 @@ +"""actinia-example-plguin API part of package. + +This part provides the API part of the actinia-example-plugin. +""" diff --git a/src/actinia_example_plugin/api/helloworld.py b/src/actinia_example_plugin/api/helloworld.py index 2b5cebb..9fde509 100644 --- a/src/actinia_example_plugin/api/helloworld.py +++ b/src/actinia_example_plugin/api/helloworld.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,39 +20,38 @@ __license__ = "GPLv3" __author__ = "Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" -from flask import request, make_response -from flask_restful_swagger_2 import swagger -from flask_restful_swagger_2 import Resource +from flask import make_response, request +from flask_restful_swagger_2 import Resource, swagger from actinia_example_plugin.apidocs import helloworld +from actinia_example_plugin.core.example import transform_input from actinia_example_plugin.model.response_models import ( SimpleStatusCodeResponseModel, ) -from actinia_example_plugin.core.example import transform_input class HelloWorld(Resource): - """Returns 'Hello world!'""" + """Returns 'Hello world!'.""" - def __init__(self): + def __init__(self) -> None: + """Hello world class initialisation.""" self.msg = "Hello world!" - @swagger.doc(helloworld.describeHelloWorld_get_docs) - def get(self): + @swagger.doc(helloworld.describe_hello_world_get_docs) + def get(self) -> SimpleStatusCodeResponseModel: """Get 'Hello world!' as answer string.""" return SimpleStatusCodeResponseModel(status=200, message=self.msg) - @swagger.doc(helloworld.describeHelloWorld_post_docs) - def post(self): + @swagger.doc(helloworld.describe_hello_world_post_docs) + def post(self) -> SimpleStatusCodeResponseModel: """Hello World post method with name from postbody.""" - req_data = request.get_json(force=True) if isinstance(req_data, dict) is False or "name" not in req_data: return make_response("Missing name in JSON content", 400) name = req_data["name"] - msg = transform_input(name) + msg = f"{self.msg} {transform_input(name)}" return SimpleStatusCodeResponseModel(status=200, message=msg) diff --git a/src/actinia_example_plugin/api/project_helloworld.py b/src/actinia_example_plugin/api/project_helloworld.py new file mode 100644 index 0000000..247d05d --- /dev/null +++ b/src/actinia_example_plugin/api/project_helloworld.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Hello World class +""" + +__license__ = "GPLv3" +__author__ = "Anika Weinmann" +__copyright__ = "Copyright 2024 mundialis GmbH & Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" + + +from typing import ClassVar + +from actinia_core.models.response_models import SimpleResponseModel +from actinia_core.rest.base.deprecated_locations import ( + location_deprecated_decorator, +) +from flask import jsonify, make_response, request +from flask.wrappers import Response +from flask_restful_swagger_2 import Resource, swagger + +from actinia_example_plugin.apidocs import project_helloworld +from actinia_example_plugin.core.example import transform_input + + +class ProjectHelloWorld(Resource): + """Returns 'Hello world with project/location!'.""" + + decorators: ClassVar[list] = [] + + # Add decorators for deprecated GRASS GIS locations + decorators.append(location_deprecated_decorator) + + def __init__(self) -> None: + """Project hello world class initialisation.""" + self.msg = "Project: Hello world!" + + @swagger.doc(project_helloworld.describe_project_hello_world_get_docs) + def get(self, project_name: str) -> Response: + """Get 'Hello world!' as answer string.""" + msg = f"{self.msg} {project_name}" + return make_response( + jsonify( + SimpleResponseModel( + status="200", + message=msg, + ), + ), + 200, + ) + + @swagger.doc(project_helloworld.describe_project_hello_world_post_docs) + def post(self, project_name: str) -> Response: + """Hello World post method with name from postbody.""" + req_data = request.get_json(force=True) + if isinstance(req_data, dict) is False or "name" not in req_data: + return make_response("Missing name in JSON content", 400) + name = req_data["name"] + msg = f"{self.msg} {transform_input(name)} {project_name}" + + return make_response( + jsonify( + SimpleResponseModel( + status="200", + message=msg, + ), + ), + 200, + ) diff --git a/src/actinia_example_plugin/apidocs/__init__.py b/src/actinia_example_plugin/apidocs/__init__.py index e69de29..fec4e58 100644 --- a/src/actinia_example_plugin/apidocs/__init__.py +++ b/src/actinia_example_plugin/apidocs/__init__.py @@ -0,0 +1,4 @@ +"""actinia-example-plguin API DOCs part of package. + +This part provides the API DOCs part of the actinia-example-plugin. +""" diff --git a/src/actinia_example_plugin/apidocs/helloworld.py b/src/actinia_example_plugin/apidocs/helloworld.py index ece3424..b016bfb 100644 --- a/src/actinia_example_plugin/apidocs/helloworld.py +++ b/src/actinia_example_plugin/apidocs/helloworld.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,15 +20,14 @@ __license__ = "GPLv3" __author__ = "Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" from actinia_example_plugin.model.response_models import ( SimpleStatusCodeResponseModel, ) - -describeHelloWorld_get_docs = { +describe_hello_world_get_docs = { # "summary" is taken from the description of the get method "tags": ["example"], "description": "Hello World example", @@ -38,11 +35,11 @@ "200": { "description": "This response returns the string 'Hello World!'", "schema": SimpleStatusCodeResponseModel, - } + }, }, } -describeHelloWorld_post_docs = { +describe_hello_world_post_docs = { # "summary" is taken from the description of the get method "tags": ["example"], "description": "Hello World example with name", @@ -61,7 +58,7 @@ "type": "string", "description": "detailed message", "example": "Missing name in JSON content", - } + }, }, }, }, diff --git a/src/actinia_example_plugin/apidocs/project_helloworld.py b/src/actinia_example_plugin/apidocs/project_helloworld.py new file mode 100644 index 0000000..699d930 --- /dev/null +++ b/src/actinia_example_plugin/apidocs/project_helloworld.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Hello World class +""" + +__license__ = "GPLv3" +__author__ = "Anika Weinmann" +__copyright__ = "Copyright 2024 mundialis GmbH & Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" + + +from actinia_example_plugin.model.response_models import ( + SimpleStatusCodeResponseModel, +) + +describe_project_hello_world_get_docs = { + # "summary" is taken from the description of the get method + "tags": ["example"], + "description": "Project Hello World example", + "parameters": [ + { + "name": "project_name", + "description": "The project name that contains the data that " + "should be processed", + "required": True, + "in": "path", + "type": "string", + "default": "nc_spm_08", + }, + ], + "responses": { + "200": { + "description": "This response returns the string 'Hello World!'", + "schema": SimpleStatusCodeResponseModel, + }, + }, +} + +describe_project_hello_world_post_docs = { + # "summary" is taken from the description of the get method + "tags": ["example"], + "description": "Project Hello World example with name", + "parameters": [ + { + "name": "project_name", + "description": "The project name that contains the data that " + "should be processed", + "required": True, + "in": "path", + "type": "string", + "default": "nc_spm_08", + }, + ], + "responses": { + "200": { + "description": "This response returns the string 'Hello World " + "NAME!'", + "schema": SimpleStatusCodeResponseModel, + }, + "400": { + "description": "This response returns a detail error message", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "detailed message", + "example": "Missing name in JSON content", + }, + }, + }, + }, + }, +} diff --git a/src/actinia_example_plugin/core/__init__.py b/src/actinia_example_plugin/core/__init__.py index e69de29..d7f8813 100644 --- a/src/actinia_example_plugin/core/__init__.py +++ b/src/actinia_example_plugin/core/__init__.py @@ -0,0 +1,4 @@ +"""actinia-example-plguin core part of package. + +This part provides the core part of the actinia-example-plugin. +""" diff --git a/src/actinia_example_plugin/core/example.py b/src/actinia_example_plugin/core/example.py index 55d82e7..eb9f38c 100644 --- a/src/actinia_example_plugin/core/example.py +++ b/src/actinia_example_plugin/core/example.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,10 +20,17 @@ __license__ = "GPLv3" __author__ = "Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" + + +def transform_input(inp: str) -> str: + """Return a transformed string as example core function. + + Args: + inp (str): Input string to transform + Returns: + (str) transformed string -def transform_input(inp): - """Example core function""" - out = f"Hello world {inp.upper()}!" - return out + """ + return f"Hello world {inp.upper()}!" diff --git a/src/actinia_example_plugin/endpoints.py b/src/actinia_example_plugin/endpoints.py index 2bdbfdc..6fee3d9 100644 --- a/src/actinia_example_plugin/endpoints.py +++ b/src/actinia_example_plugin/endpoints.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,15 +19,45 @@ __license__ = "GPLv3" __author__ = "Carmen Tawalika, Anika Weinmann" -__copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__copyright__ = "Copyright 2022-2024 mundialis GmbH & Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" +from actinia_core.endpoints import get_endpoint_class_name +from flask_restful_swagger_2 import Api from actinia_example_plugin.api.helloworld import HelloWorld +from actinia_example_plugin.api.project_helloworld import ProjectHelloWorld + + +def create_project_endpoints( + apidoc: Api, + projects_url_part: str = "projects", +) -> None: + """Add resources with "project" inside the endpoint url to the api. + + Args: + apidoc (Api): Flask api + projects_url_part (str): The name of the projects inside the endpoint + URL; to add deprecated location endpoints set + it to "locations" + + """ + apidoc.add_resource( + ProjectHelloWorld, + f"/{projects_url_part}/", + endpoint=get_endpoint_class_name(ProjectHelloWorld, projects_url_part), + ) # endpoints loaded if run as actinia-core plugin as well as standalone app -def create_endpoints(flask_api): +def create_endpoints(flask_api: Api) -> None: + """Create plugin endpoints.""" apidoc = flask_api apidoc.add_resource(HelloWorld, "/helloworld") + + # add deprecated location endpoints + create_project_endpoints(apidoc, projects_url_part="locations") + + # add project endpoints + create_project_endpoints(apidoc, projects_url_part="projects") diff --git a/src/actinia_example_plugin/model/__init__.py b/src/actinia_example_plugin/model/__init__.py index e69de29..41b92c6 100644 --- a/src/actinia_example_plugin/model/__init__.py +++ b/src/actinia_example_plugin/model/__init__.py @@ -0,0 +1,4 @@ +"""actinia-example-plguin model part of package. + +This part provides the model part of the actinia-example-plugin. +""" diff --git a/src/actinia_example_plugin/model/response_models.py b/src/actinia_example_plugin/model/response_models.py index 92c9621..dd2ab76 100644 --- a/src/actinia_example_plugin/model/response_models.py +++ b/src/actinia_example_plugin/model/response_models.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,8 +20,10 @@ __license__ = "GPLv3" __author__ = "Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" + +from typing import ClassVar from flask_restful_swagger_2 import Schema @@ -31,8 +31,8 @@ class SimpleStatusCodeResponseModel(Schema): """Simple response schema to inform about status.""" - type = "object" - properties = { + type: str = "object" + properties: ClassVar[dict] = { "status": { "type": "number", "description": "The status code of the request.", @@ -42,10 +42,11 @@ class SimpleStatusCodeResponseModel(Schema): "description": "A short message to describes the status", }, } - required = ["status", "message"] + required: ClassVar[list[str]] = ["status", "message"] -simpleResponseExample = SimpleStatusCodeResponseModel( - status=200, message="success" +simple_response_example = SimpleStatusCodeResponseModel( + status=200, + message="success", ) -SimpleStatusCodeResponseModel.example = simpleResponseExample +SimpleStatusCodeResponseModel.example = simple_response_example diff --git a/src/actinia_example_plugin/wsgi.py b/src/actinia_example_plugin/wsgi.py index 64ae9ed..173f4ed 100644 --- a/src/actinia_example_plugin/wsgi.py +++ b/src/actinia_example_plugin/wsgi.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,6 +18,4 @@ __license__ = "GPLv3" __author__ = "Carmen Tawalika, Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" - -from actinia_example_plugin.main import app as application +__maintainer__ = "mundialis GmbH & Co. KG" diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..95e6a85 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +"""Tests of the actinia-example-plguin. + +This package part provides the tests of the actinia-example-plugin. +""" diff --git a/tests/conftest.py b/tests/conftest.py index 94c17d1..dc384d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +20,6 @@ __license__ = "GPLv3" __author__ = "Carmen Tawalika" __copyright__ = "Copyright 2018-2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" # import pytest diff --git a/tests/integrationtests/__init__.py b/tests/integrationtests/__init__.py index e69de29..1f026a7 100644 --- a/tests/integrationtests/__init__.py +++ b/tests/integrationtests/__init__.py @@ -0,0 +1,4 @@ +"""Integration tests of the actinia-example-plguin. + +This package part provides the integration tests of the actinia-example-plugin. +""" diff --git a/tests/integrationtests/test_helloworld.py b/tests/integrationtests/test_helloworld.py index 25eee51..9369c94 100644 --- a/tests/integrationtests/test_helloworld.py +++ b/tests/integrationtests/test_helloworld.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,70 +20,84 @@ __license__ = "GPLv3" __author__ = "Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" import json + import pytest +from actinia_api import URL_PREFIX from flask import Response -from actinia_api import URL_PREFIX +from tests.testsuite import ActiniaTestCase -from ..testsuite import ActiniaTestCase +STATUS_CODE_200 = 200 +STATUS_CODE_400 = 400 class ActiniaHelloWorldTest(ActiniaTestCase): + """Actinia hello world test class for hello world endpoint.""" + @pytest.mark.integrationtest - def test_get_helloworld(self): - """Test the get method of the /helloworld endpoint""" - resp = self.app.get(URL_PREFIX + "/helloworld") + def test_get_helloworld(self) -> None: + """Test the get method of the /helloworld endpoint.""" + resp = self.app.get(f"{URL_PREFIX}/helloworld") assert isinstance( - resp, Response + resp, + Response, ), "The response is not of type Response" - assert resp.status_code == 200, "The status code is not 200" + assert ( + resp.status_code == STATUS_CODE_200 + ), f"The status code is not {STATUS_CODE_200}" assert hasattr(resp, "json"), "The response has no attribute 'json'" - assert "message" in resp.json, ( - "There is no 'message' inside the " "response" - ) - assert resp.json["message"] == "Hello world!", ( - "The response message" " is wrong" - ) + assert ( + "message" in resp.json + ), "There is no 'message' inside the response" + assert ( + resp.json["message"] == "Hello world!" + ), "The response message is wrong" @pytest.mark.integrationtest - def test_post_helloworld(self): - """Test the post method of the /helloworld endpoint""" + def test_post_helloworld(self) -> None: + """Test the post method of the /helloworld endpoint.""" postbody = {"name": "test"} resp = self.app.post( - URL_PREFIX + "/helloworld", + f"{URL_PREFIX}/helloworld", headers=self.user_auth_header, data=json.dumps(postbody), content_type="application/json", ) assert isinstance( - resp, Response + resp, + Response, ), "The response is not of type Response" - assert resp.status_code == 200, "The status code is not 200" + assert ( + resp.status_code == STATUS_CODE_200 + ), f"The status code is not {STATUS_CODE_200}" assert hasattr(resp, "json"), "The response has no attribute 'json'" - assert "message" in resp.json, ( - "There is no 'message' inside the " "response" - ) - assert resp.json["message"] == "Hello world TEST!", ( - "The response " "message is wrong" - ) + assert ( + "message" in resp.json + ), "There is no 'message' inside the response" + assert ( + resp.json["message"] == "Hello world TEST!" + ), "The response message is wrong" @pytest.mark.integrationtest - def test_post_helloworld_error(self): - """Test the post method of the /helloworld endpoint""" + def test_post_helloworld_error(self) -> None: + """Test the post method of the /helloworld endpoint.""" postbody = {"namee": "test"} resp = self.app.post( - URL_PREFIX + "/helloworld", + f"{URL_PREFIX}/helloworld", headers=self.user_auth_header, data=json.dumps(postbody), content_type="application/json", ) assert isinstance( - resp, Response + resp, + Response, ), "The response is not of type Response" - assert resp.status_code == 400, "The status code is not 400" + assert ( + resp.status_code == STATUS_CODE_400 + ), f"The status code is not {STATUS_CODE_400}" assert resp.data == b"Missing name in JSON content" diff --git a/tests/integrationtests/test_projecthelloworld.py b/tests/integrationtests/test_projecthelloworld.py new file mode 100644 index 0000000..7ed08a2 --- /dev/null +++ b/tests/integrationtests/test_projecthelloworld.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Hello World test +""" + +__license__ = "GPLv3" +__author__ = "Anika Weinmann" +__copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" + + +import json + +import pytest +from actinia_api import URL_PREFIX +from flask import Response + +from tests.testsuite import ActiniaTestCase + +STATUS_CODE_200 = 200 +STATUS_CODE_400 = 400 + + +class ActiniaHelloWorldTest(ActiniaTestCase): + """Actinia hello world test class for hello world endpoint.""" + + @pytest.mark.integrationtest + def test_get_helloworld(self) -> None: + """Test the get method of the /projects/ endpoint.""" + resp = self.app.get(f"{URL_PREFIX}/{self.project_url_part}/project1") + + assert isinstance( + resp, + Response, + ), "The response is not of type Response" + assert ( + resp.status_code == STATUS_CODE_200 + ), f"The status code is not {STATUS_CODE_200}" + assert hasattr(resp, "json"), "The response has no attribute 'json'" + assert ( + "message" in resp.json + ), "There is no 'message' inside the response" + assert ( + resp.json["message"] == "Project: Hello world! project1" + ), "The response message is wrong" + + @pytest.mark.integrationtest + def test_post_helloworld(self) -> None: + """Test the post method of the /projects/ endpoint.""" + postbody = {"name": "test"} + resp = self.app.post( + f"{URL_PREFIX}/{self.project_url_part}/project1", + headers=self.user_auth_header, + data=json.dumps(postbody), + content_type="application/json", + ) + assert isinstance( + resp, + Response, + ), "The response is not of type Response" + assert ( + resp.status_code == STATUS_CODE_200 + ), f"The status code is not {STATUS_CODE_200}" + assert hasattr(resp, "json"), "The response has no attribute 'json'" + assert ( + "message" in resp.json + ), "There is no 'message' inside the response" + assert ( + resp.json["message"] == "Hello world TEST! project1" + ), "The response message is wrong" + + @pytest.mark.integrationtest + def test_post_helloworld_error(self) -> None: + """Test the post method of the /projects/ endpoint.""" + postbody = {"namee": "test"} + resp = self.app.post( + f"{URL_PREFIX}/{self.project_url_part}/project1", + headers=self.user_auth_header, + data=json.dumps(postbody), + content_type="application/json", + ) + assert isinstance( + resp, + Response, + ), "The response is not of type Response" + assert ( + resp.status_code == STATUS_CODE_400 + ), f"The status code is not {STATUS_CODE_400}" + assert resp.data == b"Missing name in JSON content" diff --git a/tests/test_resource_base.py b/tests/test_resource_base.py index a73496a..f8f0e0b 100644 --- a/tests/test_resource_base.py +++ b/tests/test_resource_base.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2016-2022 mundialis GmbH & Co. KG +"""Copyright (c) 2016-2022 mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,19 +17,21 @@ Tests: Actinia resource test case base """ +from __future__ import annotations + import atexit import base64 import os import signal +import tempfile import time +from pathlib import Path -from werkzeug.datastructures import Headers - -from actinia_core.testsuite import ActiniaTestCaseBase, URL_PREFIX -from actinia_core.core.common.user import ActiniaUser from actinia_core.core.common.config import global_config +from actinia_core.core.common.user import ActiniaUser from actinia_core.endpoints import create_endpoints - +from actinia_core.testsuite import ActiniaTestCaseBase +from werkzeug.datastructures import Headers __license__ = "GPLv3" __author__ = "Sören Gebbert, Anika Weinmann" @@ -43,21 +43,22 @@ # Create endpoints create_endpoints() -redis_pid = None -server_test = False -custom_actinia_cfg = False +REDIS_PID = None +SERVER_TEST = False +CUSTOM_ACTINIA_CFG = False # If this environmental variable is set, then a real http request will be send # instead of using the flask test_client. if "ACTINIA_SERVER_TEST" in os.environ: - server_test = bool(os.environ["ACTINIA_SERVER_TEST"]) + SERVER_TEST = bool(os.environ["ACTINIA_SERVER_TEST"]) # Set this variable to use a actinia config file in a docker container if "ACTINIA_CUSTOM_TEST_CFG" in os.environ: - custom_actinia_cfg = str(os.environ["ACTINIA_CUSTOM_TEST_CFG"]) + CUSTOM_ACTINIA_CFG = str(os.environ["ACTINIA_CUSTOM_TEST_CFG"]) -def setup_environment(): - global redis_pid +def setup_environment() -> None: + """Setuo test environment.""" + global REDIS_PID # Set the port to the test redis server global_config.REDIS_SERVER_SERVER = "localhost" global_config.REDIS_SERVER_PORT = 7000 @@ -72,28 +73,28 @@ def setup_environment(): global_config.GRASS_GIS_START_SCRIPT = "/usr/local/bin/grass" # global_config.GRASS_DATABASE= "/usr/local/grass_test_db" # global_config.GRASS_DATABASE = "%s/actinia/grass_test_db" % home - global_config.GRASS_TMP_DATABASE = "/tmp" + global_config.GRASS_TMP_DATABASE = tempfile.TemporaryDirectory().name + Path(global_config.GRASS_TMP_DATABASE).mkdir(parents=True) - if server_test is False and custom_actinia_cfg is False: + if SERVER_TEST is False and CUSTOM_ACTINIA_CFG is False: # Start the redis server for user and logging management - redis_pid = os.spawnl( + REDIS_PID = os.spawnl( os.P_NOWAIT, "/usr/bin/redis-server", "common/redis.conf", - "--port %i" % global_config.REDIS_SERVER_PORT, + f"--port {global_config.REDIS_SERVER_PORT}", ) time.sleep(1) - if server_test is False and custom_actinia_cfg is not False: - global_config.read(custom_actinia_cfg) + if SERVER_TEST is False and CUSTOM_ACTINIA_CFG is not False: + global_config.read(CUSTOM_ACTINIA_CFG) -def stop_redis(): - if server_test is False: - global redis_pid - # Kill th redis server - if redis_pid is not None: - os.kill(redis_pid, signal.SIGTERM) +def stop_redis() -> None: + """Stop redis server.""" + # Kill th redis server + if SERVER_TEST is False and REDIS_PID is not None: + os.kill(REDIS_PID, signal.SIGTERM) # Register the redis stop function @@ -103,24 +104,28 @@ def stop_redis(): class ActiniaResourceTestCaseBase(ActiniaTestCaseBase): + """Actinia resource test case base class.""" + @classmethod def create_user( cls, - name="guest", - role="guest", - group="group", - password="abcdefgh", - accessible_datasets=None, - process_num_limit=1000, - process_time_limit=6000, - accessible_modules=None, - ): - auth = bytes("%s:%s" % (name, password), "utf-8") + name: str = "guest", + role: str = "guest", + group: str = "group", + password: str = "abcdefgh", + accessible_datasets: dict[str, list | None] | None = None, + process_num_limit: int = 1000, + process_time_limit: int = 6000, + accessible_modules: list[str] | None = None, + ) -> (str, str, Headers()): + """Create actinia user.""" + auth = bytes(f"{name}:{password}", "utf-8") # We need to create an HTML basic authorization header cls.auth_header[role] = Headers() cls.auth_header[role].add( - "Authorization", "Basic " + base64.b64encode(auth).decode() + "Authorization", + f"Basic {base64.b64encode(auth).decode()}", ) # Make sure the user database is empty diff --git a/tests/testsuite.py b/tests/testsuite.py index 3d4b576..d59fad7 100644 --- a/tests/testsuite.py +++ b/tests/testsuite.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,36 +17,50 @@ Base class for GRASS GIS REST API tests """ +from __future__ import annotations + __license__ = "GPLv3" __author__ = "Carmen Tawalika, Sören Gebbert" __copyright__ = "Copyright 2018-2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" - +__maintainer__ = "mundialis GmbH & Co. KG" import base64 import unittest +from typing import ClassVar import pwgen -from werkzeug.datastructures import Headers - from actinia_core.core.common import redis_interface from actinia_core.core.common.app import flask_app from actinia_core.core.common.config import global_config from actinia_core.core.common.user import ActiniaUser -from actinia_core.models.response_models import ProcessingResponseModel +from actinia_core.version import init_versions +from werkzeug.datastructures import Headers class ActiniaTestCase(unittest.TestCase): + """Actinia test case class.""" + # guest = None # admin = None # superadmin = None - user = None - auth_header = {} - users_list = [] - - def setUp(self): - """Overwrites method setUp from unittest.TestCase class""" - + user: str = None + auth_header: ClassVar[dict] = {} + users_list: ClassVar[list[str]] = [] + project_url_part: str = "projects" + + # set project_url_part to "locations" if GRASS GIS version < 8.4 + init_versions() + from actinia_core.version import G_VERSION + + grass_version_s: str = G_VERSION["version"] + grass_version: ClassVar[list[int]] = [ + int(item) for item in grass_version_s.split(".")[:2] + ] + if grass_version < [8, 4]: + project_url_part = "locations" + + def setUp(self) -> None: + """Overwrite method setUp from unittest.TestCase class.""" self.app_context = flask_app.app_context() self.app_context.push() # from http://flask.pocoo.org/docs/0.12/api/#flask.Flask.test_client: @@ -75,14 +87,14 @@ def setUp(self): # create test user for roles user (more to come) accessible_datasets = { - "nc_spm_08": ["PERMANENT", "user1", "modis_lst"] + "nc_spm_08": ["PERMANENT", "user1", "modis_lst"], } password = pwgen.pwgen() ( self.user_id, self.user_group, self.user_auth_header, - ) = self.createUser( + ) = self.create_user( name="user", role="user", password=password, @@ -94,7 +106,7 @@ def setUp(self): self.restricted_user_id, self.restricuted_user_group, self.restricted_user_auth_header, - ) = self.createUser( + ) = self.create_user( name="user2", role="user", password=password, @@ -107,7 +119,7 @@ def setUp(self): self.admin_id, self.admin_group, self.admin_auth_header, - ) = self.createUser( + ) = self.create_user( name="admin", role="admin", password=password, @@ -121,9 +133,8 @@ def setUp(self): # create_process_queue # create_process_queue(config=global_config) - def tearDown(self): - """Overwrites method tearDown from unittest.TestCase class""" - + def tearDown(self) -> None: + """Overwrite method tearDown from unittest.TestCase class.""" self.app_context.pop() # remove test user; disconnect redis @@ -131,22 +142,24 @@ def tearDown(self): user.delete() redis_interface.disconnect() - def createUser( + def create_user( self, - name="guest", - role="guest", - group="group", - password="abcdefgh", - accessible_datasets=None, - accessible_modules=global_config.MODULE_ALLOW_LIST, - process_num_limit=1000, - process_time_limit=6000, - ): - auth = bytes("%s:%s" % (name, password), "utf-8") + name: str = "guest", + role: str = "guest", + group: str = "group", + password: str = "abcdefgh", + accessible_datasets: dict[str, list | None] | None = None, + process_num_limit: int = 1000, + process_time_limit: int = 6000, + accessible_modules: list[str] = global_config.MODULE_ALLOW_LIST, + ) -> (str, str, Headers()): + """Create actinia user.""" + auth = bytes(f"{name}:{password}", "utf-8") # We need to create an HTML basic authorization header self.auth_header[role] = Headers() self.auth_header[role].add( - "Authorization", "Basic " + base64.b64encode(auth).decode() + "Authorization", + f"Basic {base64.b64encode(auth).decode()}", ) # Make sure the user database is empty @@ -170,17 +183,18 @@ def createUser( return name, group, self.auth_header[role] -def check_started_process(testCase, resp): - """Checks response of started process - TODO: can be enhanced""" - if type(resp.json["process_results"]) == dict: - resp.json["process_results"] = str(resp.json["process_results"]) - resp_class = ProcessingResponseModel(**resp.json) - assert resp_class["status"] == "accepted" - status_url = resp_class["urls"]["status"] - - # poll status_url - # TODO: status stays in accepted - status_resp = testCase.app.get( - status_url, headers=testCase.user_auth_header - ) - assert status_resp.json["urls"]["status"] == status_url +# def check_started_process(test_case: , resp: ) -> None: +# """Checks response of started process - TODO: can be enhanced.""" +# if isinstance(resp.json["process_results"], dict): +# resp.json["process_results"] = str(resp.json["process_results"]) +# resp_class = ProcessingResponseModel(**resp.json) +# assert resp_class["status"] == "accepted" +# status_url = resp_class["urls"]["status"] + +# # poll status_url +# # TODO: status stays in accepted +# status_resp = test_case.app.get( +# status_url, +# headers=test_case.user_auth_header, +# ) +# assert status_resp.json["urls"]["status"] == status_url diff --git a/tests/unittests/__init__.py b/tests/unittests/__init__.py index e69de29..07c2383 100644 --- a/tests/unittests/__init__.py +++ b/tests/unittests/__init__.py @@ -0,0 +1,4 @@ +"""Unittests of the actinia-example-plguin. + +This package part provides the unittests of the actinia-example-plugin. +""" diff --git a/tests/unittests/test_transformation.py b/tests/unittests/test_transformation.py index f7f703a..df89b40 100644 --- a/tests/unittests/test_transformation.py +++ b/tests/unittests/test_transformation.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2018-present mundialis GmbH & Co. KG +"""Copyright (c) 2018-present mundialis GmbH & Co. KG. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,18 +20,19 @@ __license__ = "GPLv3" __author__ = "Anika Weinmann" __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG" -__maintainer__ = "mundialis GmbH % Co. KG" +__maintainer__ = "mundialis GmbH & Co. KG" import pytest + from actinia_example_plugin.core.example import transform_input @pytest.mark.unittest @pytest.mark.parametrize( - "inp,ref_out", + ("inp", "ref_out"), [("test", "Hello world TEST!"), ("bla23", "Hello world BLA23!")], ) -def test_transform_input(inp, ref_out): +def test_transform_input(inp: str, ref_out: str) -> None: """Test for tranform_input function.""" out = transform_input(inp) assert out == ref_out, f"Wrong result from transform_input for {inp}" diff --git a/tests_with_redis.sh b/tests_with_redis.sh old mode 100644 new mode 100755 index 57405bf..26b66a6 --- a/tests_with_redis.sh +++ b/tests_with_redis.sh @@ -10,14 +10,14 @@ webhook-server --host "0.0.0.0" --port "5005" & sleep 10 # run tests -echo $ACTINIA_CUSTOM_TEST_CFG -echo $DEFAULT_CONFIG_PATH +echo "${ACTINIA_CUSTOM_TEST_CFG}" +echo "${DEFAULT_CONFIG_PATH}" -if [ "$1" == "dev" ] +if [ "$1" = "dev" ] then echo "Executing only 'dev' tests ..." python3 setup.py test --addopts "-m dev" -elif [ "$1" == "integrationtest" ] +elif [ "$1" = "integrationtest" ] then python3 setup.py test --addopts "-m 'integrationtest'" else