diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..39e7196 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,16 @@ +## Expected Behavior + + +## Actual Behavior + + +## Steps to Reproduce the Problem + +1. +2. +3. + +## Specifications + +- Platform: +- Python Version: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..75a0e8f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,10 @@ +Fixes # + +## Changes + + +## One line description for the changelog + + +- [ ] Tests pass +- [ ] Appropriate changes to README are included in PR diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c158df2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: CI + +on: [push, pull_request] + +jobs: + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: '3.10' + - name: Install tox + run: python -m pip install tox + - name: Run linting + run: python -m tox -e lint + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.6', '3.7', '3.8', '3.9', '3.10'] + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: python -m pip install tox + - name: Run tests + run: python -m tox -e py # Run tox using the version of Python in `PATH` diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml new file mode 100644 index 0000000..8ebf355 --- /dev/null +++ b/.github/workflows/pypi-release.yml @@ -0,0 +1,28 @@ +name: PyPI-Release + +on: + push: + branches: + - master + +jobs: + build-and-publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.x" + - name: Install build dependencies + run: pip install -U setuptools wheel build + - name: Build + run: python -m build . + - name: Publish + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.pypi_password }} + - name: Install GitPython and cdevents for pypi_packaging + run: pip install -U -r requirements/publish.txt + - name: Create Tag + run: python pypi_packaging.py diff --git a/.gitignore b/.gitignore index b6e4761..58710ec 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# VS Code +.vscode \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..933eba0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +--- +repos: + + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + args: + - --line-length=100 + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/pycqa/isort + rev: 5.9.3 + hooks: + - id: isort + files: ".*" + args: [--profile=black, --project=cdevent] + + - repo: https://github.com/pycqa/pydocstyle + rev: 5.1.1 + hooks: + - id: pydocstyle + args: [--convention=google] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.812 + hooks: + - id: mypy + files: "cdevents/.*" diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..2f23bcf --- /dev/null +++ b/.pylintrc @@ -0,0 +1,570 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.9 + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + fixme, + todo, + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=no + +# Signatures are removed from the similarity computation +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e65f1d6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.9.12-slim-buster + +# Setup environment +RUN apt-get update && apt-get install -y \ + python3-distutils \ + build-essential + +# Copy needed files (see .dockerignore for what will be included) +COPY . /cdevents-client + +WORKDIR /cdevents-client + +# Build +RUN /bin/bash -c "make package-install" + +WORKDIR /cdevents-client diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28156fb --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +include header.mk + +PACKAGES = core cli +export ABS_ROOT_PATH=$(shell pwd) + +.PHONY: packages $(PACKAGES) + +packages: $(PACKAGES) + +$(PACKAGES): + $(MAKE) -C $@ + +# the sets of directories to do various things in +INITPACKAGES = $(PACKAGES:%=init-%) +INSTALLPACKAGES = $(PACKAGES:%=package-install-%) +CLEANPACKAGES = $(PACKAGES:%=clean-%) +TESTPACKAGES = $(PACKAGES:%=test-%) +LINTPACKAGES = $(PACKAGES:%=lint-%) +FORMATPACKAGES = $(PACKAGES:%=format-%) +BUMPVERSIONPACKAGES = $(PACKAGES:%=bumpversion-%) + +help: ## Prints this help text + @python -c "$$PRINT_HELP_PYSCRIPT" < Makefile + +package-install: $(INSTALLPACKAGES) ## Installs all packages without dev dependencies +$(INSTALLPACKAGES): + $(MAKE) -C $(@:package-install-%=%) package-install + +test: $(TESTPACKAGES) ## Run tests on all packages +$(TESTPACKAGES): + $(MAKE) -C $(@:test-%=%) test + +clean: $(CLEANPACKAGES) ## Remove all build, test, coverage and Python artifacts +$(CLEANPACKAGES): + $(MAKE) -C $(@:clean-%=%) clean + +bumpversion: ${BUMPVERSIONPACKAGES} ## Bumps the (default: patch) version of all release packages. To bump minor or major, add bump=minor or bump=major to the make call. +$(BUMPVERSIONPACKAGES): + $(MAKE) -C $(@:bumpversion-%=%) bumpversion + +lint: $(LINTPACKAGES) +$(LINTPACKAGES): + $(MAKE) -C $(@:lint-%=%) lint + +format: $(FORMATPACKAGES) +$(FORMATPACKAGES): + $(MAKE) -C $(@:format-%=%) format + +check: ## Runs pre-commit hooks on all files + pre-commit run --all-files + +docker-build: ## Build and package Docker container + docker build -t cdevents -f Dockerfile . + +docker-shell: ## Opens a bash + docker run --rm --network host --volume $(pwd)/output/:/root/cdevents-client/ -it cdevents bash + +.PHONY: packages $(PACKAGES) +.PHONY: packages $(INITPACKAGES) +.PHONY: packages $(INSTALLPACKAGES) +.PHONY: packages $(TESTPACKAGES) +.PHONY: packages $(CLEANPACKAGES) +.PHONY: packages $(BUMPVERSIONPACKAGES) +.PHONY: init test clean bumpversion install internal-install check docker-build docker-shell diff --git a/README.md b/README.md index 65f4727..eb5e4b7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # sdk-python + Python SDK for CDEvents + +update ... diff --git a/cli/MANIFEST.in b/cli/MANIFEST.in new file mode 100644 index 0000000..d9f77c2 --- /dev/null +++ b/cli/MANIFEST.in @@ -0,0 +1,2 @@ +include cdevents/configuration/configuration.yaml +include cdevents/configuration/logging-configuration.yaml diff --git a/cli/Makefile b/cli/Makefile new file mode 100644 index 0000000..f473d81 --- /dev/null +++ b/cli/Makefile @@ -0,0 +1,7 @@ +include ../header.mk + +SRC := cdevents/cli + +MAKEFILE_LIST=../targets.mk + +include $(MAKEFILE_LIST) diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cli/cdevents/cli/__init__.py b/cli/cdevents/cli/__init__.py new file mode 100644 index 0000000..8bc3d2a --- /dev/null +++ b/cli/cdevents/cli/__init__.py @@ -0,0 +1,3 @@ +"""CDEvents CLI provides command-line interaction with Cd-Event client functionality.""" + +__version__ = "0.0.1" diff --git a/cli/cdevents/cli/__main__.py b/cli/cdevents/cli/__main__.py new file mode 100644 index 0000000..686348c --- /dev/null +++ b/cli/cdevents/cli/__main__.py @@ -0,0 +1,138 @@ +"""Module for main entry point for cdevents.cli.""" +import logging +import logging.config +from pathlib import Path + +import click +import yaml + +from cdevents.cli.constants import LOGGING_CONFIGURATION_FILE + +from cdevents.cli.artifact import packaged, published +from cdevents.cli.branch import created as branch_created +from cdevents.cli.branch import deleted as branch_deleted +from cdevents.cli.build import finished, queued, started +from cdevents.cli.env import created as env_created +from cdevents.cli.env import deleted as env_deleted +from cdevents.cli.env import modified as env_modified +from cdevents.cli.pipelinerun import finished as pipe_finished +from cdevents.cli.pipelinerun import queued as pipe_queued +from cdevents.cli.pipelinerun import started as pipe_started +from cdevents.cli.repository import created as repo_created +from cdevents.cli.repository import deleted as repo_deleted +from cdevents.cli.repository import modified as repo_modified +from cdevents.cli.service import deployed as service_deployed +from cdevents.cli.service import removed as service_removed +from cdevents.cli.service import rolledback as service_rolledback +from cdevents.cli.service import upgraded as service_upgraded +from cdevents.cli.taskrun import finished as taskrun_finished +from cdevents.cli.taskrun import started as taskrun_started +from cdevents.cli.utils import add_disclaimer_text + + +def configure_logging(): + """Configures logging from file.""" + config_file = Path(LOGGING_CONFIGURATION_FILE) + logging_config = yaml.safe_load(config_file.read_text(encoding="utf8")) + logging.config.dictConfig(logging_config) + + +@click.group(help=add_disclaimer_text("""Commands Build related CloudEvent.""")) +def build(): + """Click group for command 'build'.""" + + +build.add_command(finished) +build.add_command(queued) +build.add_command(started) + + +@click.group(help=add_disclaimer_text("""Commands Artifact related CloudEvent.""")) +def artifact(): + """Click group for command 'artifact'.""" + + +artifact.add_command(packaged) +artifact.add_command(published) + + +@click.group(help=add_disclaimer_text("""Commands Branch related CloudEvent.""")) +def branch(): + """Click group for command 'branch'.""" + + +branch.add_command(branch_created) +branch.add_command(branch_deleted) + + +@click.group(help=add_disclaimer_text("""Commands Environment related CloudEvent.""")) +def env(): + """Click group for command 'environment'.""" + + +env.add_command(env_created) +env.add_command(env_deleted) +env.add_command(env_modified) + + +@click.group(help=add_disclaimer_text("""Commands PipelineRun related CloudEvent.""")) +def pipelinerun(): + """Click group for command 'environment'.""" + + +pipelinerun.add_command(pipe_started) +pipelinerun.add_command(pipe_finished) +pipelinerun.add_command(pipe_queued) + +@click.group(help=add_disclaimer_text("""Commands Repository related CloudEvent.""")) +def repository(): + """Click group for command 'repository'.""" + +repository.add_command(repo_created) +repository.add_command(repo_modified) +repository.add_command(repo_deleted) + + +@click.group(help=add_disclaimer_text("""Commands Service related CloudEvent.""")) +def service(): + """Click group for command 'service'.""" + + +service.add_command(service_deployed) +service.add_command(service_upgraded) +service.add_command(service_removed) +service.add_command(service_rolledback) + + +@click.group(help=add_disclaimer_text("""Commands TaskRun related CloudEvent.""")) +def taskrun(): + """Click group for command 'taskrun'.""" + + +taskrun.add_command(taskrun_started) +taskrun.add_command(taskrun_finished) + + +@click.group( + help=add_disclaimer_text( + """Main entry point for cdevents client cli. + Select command group from Commands. + Add '--help' to see more information about each subcommand, option and argument.""" + ) +) +def cli(): + """Main method for cli.""" + configure_logging() + + +cli.add_command(build) +cli.add_command(artifact) +cli.add_command(branch) +cli.add_command(env) +cli.add_command(pipelinerun) +cli.add_command(repository) +cli.add_command(service) +cli.add_command(taskrun) + +if __name__ == "__main__": + cli() diff --git a/cli/cdevents/cli/artifact.py b/cli/cdevents/cli/artifact.py new file mode 100644 index 0000000..7febb6f --- /dev/null +++ b/cli/cdevents/cli/artifact.py @@ -0,0 +1,72 @@ +"""Module for cli artifact commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.artifact import ArtifactPackagedEvent, ArtifactPublishedEvent + +# pylint: disable=unused-argument +def common_artifact_options(function): + """Decorator for common cli options for artifact.""" + function = click.option( + "--id", + "-i", + required=False, + type=str, + help="Artifact Id.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Artifact Name.", + )(function) + function = click.option( + "--version", + "-v", + required=False, + type=str, + help="Artifact Version.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + type=(str, str), + multiple=True, + help="Artifact Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("Artifact Packaged CloudEvent.")) +@common_artifact_options +def packaged( + id: str, + name: str = None, + version: str = None, + data: List[str] = None, +): + print_function_args() + artifact_event = ArtifactPackagedEvent(id=id, name=name, version=version, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(artifact_event) + + +@click.command(help=add_disclaimer_text("Artifact Published CloudEvent.")) +@common_artifact_options +def published( + id: str, + name: str = None, + version: str = None, + data: List[str] = None, +): + print_function_args() + artifact_event = ArtifactPublishedEvent(id=id, name=name, version=version, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(artifact_event) diff --git a/cli/cdevents/cli/branch.py b/cli/cdevents/cli/branch.py new file mode 100644 index 0000000..22cc5db --- /dev/null +++ b/cli/cdevents/cli/branch.py @@ -0,0 +1,72 @@ +"""Module for cli branch commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.branch import BranchCreatedEvent, BranchDeletedEvent + +# pylint: disable=unused-argument +def common_branch_options(function): + """Decorator for common cli options for branch.""" + function = click.option( + "--id", + "-i", + required=False, + type=str, + help="Branch Id.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Branch Name.", + )(function) + function = click.option( + "--repoid", + "-v", + required=False, + type=str, + help="Branch Repository Id.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + type=(str, str), + multiple=True, + help="Branch Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("Branch Created CloudEvent.")) +@common_branch_options +def created( + id: str, + name: str = None, + repoid: str = None, + data: List[str] = None, +): + print_function_args() + branch_event = BranchCreatedEvent(id=id, name=name, repoid=repoid, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(branch_event) + + +@click.command(help=add_disclaimer_text("Branch Deleted CloudEvent.")) +@common_branch_options +def deleted( + id: str, + name: str = None, + repoid: str = None, + data: List[str] = None, +): + print_function_args() + branch_event = BranchDeletedEvent(id=id, name=name, repoid=repoid, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(branch_event) diff --git a/cli/cdevents/cli/build.py b/cli/cdevents/cli/build.py new file mode 100644 index 0000000..f850e4b --- /dev/null +++ b/cli/cdevents/cli/build.py @@ -0,0 +1,85 @@ +"""Module for cli build commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.build import BuildStartedEvent, BuildQueuedEvent, BuildFinishedEvent + +# pylint: disable=unused-argument +def common_build_options(function): + """Decorator for common cli options for build.""" + function = click.option( + "--id", + "-i", + required=False, + type=str, + help="Build Id.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Build Name.", + )(function) + function = click.option( + "--artifact", + "-a", + required=False, + type=str, + help="Build's Artifact Id.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + # type=click.Tuple([str, str]), + type=(str, str), + multiple=True, + help="Build Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("Build Started CloudEvent.")) +@common_build_options +def started( + id: str, + name: str = None, + artifact: str = None, + data: List[str] = None, +): + print_function_args() + build_event = BuildStartedEvent(id=id, name=name, artifact=artifact, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(build_event) + +@click.command(help=add_disclaimer_text("Build Finished CloudEvent.")) +@common_build_options +def finished( + id: str, + name: str = None, + artifact: str = None, + data: List[str] = None, +): + print_function_args() + build_event = BuildQueuedEvent(id=id, name=name, artifact=artifact, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(build_event) + +@click.command(help=add_disclaimer_text("PipelineRun Queued CloudEvent.")) +@common_build_options +def queued( + id: str, + name: str = None, + artifact: str = None, + data: List[str] = None, +): + print_function_args() + build_event = BuildFinishedEvent(id=id, name=name, artifact=artifact, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(build_event) diff --git a/cli/cdevents/cli/cdevents_command.py b/cli/cdevents/cli/cdevents_command.py new file mode 100644 index 0000000..633226d --- /dev/null +++ b/cli/cdevents/cli/cdevents_command.py @@ -0,0 +1,37 @@ +"""Module for cli common command code.""" +import logging +from abc import ABC + +from cloudevents.http import CloudEvent + +from cdevents.core.event_sender import EventSender + +from cdevents.cli.configuration_handler import ConfigurationHandler +from cdevents.cli.configuration_handler import new_default_configuration_handler + + +class CDeventsCommand(ABC): + """Abstract base class for all CDevents commands.""" + + def __init__(self, config_handler: ConfigurationHandler = None): + """Initializes base class. + + Args: + config_handler (ConfigurationHandler): the configuration handler. + """ + self._log = logging.getLogger(__name__) + self._config_handler = config_handler + if config_handler is None: + self._config_handler = new_default_configuration_handler() + + def run(self, event: CloudEvent): + """run command. + """ + e = EventSender(cde_link=self.config_handler.client.host) + e.send(event) + + + @property + def config_handler(self) -> ConfigurationHandler: + """Property for configuration handler.""" + return self._config_handler diff --git a/cli/cdevents/cli/configuration_handler.py b/cli/cdevents/cli/configuration_handler.py new file mode 100644 index 0000000..2ab089b --- /dev/null +++ b/cli/cdevents/cli/configuration_handler.py @@ -0,0 +1,122 @@ +"""Module for configuration provider.""" +from __future__ import annotations + +import copy +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Union + +from cdevents.cli.configuration_reader import ConfigurationReader +from cdevents.cli.constants import DEFAULT_CONFIGURATION_FILE +from cdevents.cli.utils import DictUtils + + +def get_default_configuration_file() -> str: + """Returns the default configuration file path.""" + return DEFAULT_CONFIGURATION_FILE + +def new_default_configuration_handler() -> ConfigurationHandler: + """Returnes a configuration handler with the default configuration file""" + config_handler: ConfigurationHandler = ConfigurationHandler.create_new( + get_default_configuration_file() + ) + + return config_handler + + +def new_configuration_handler_with_override( + client_host, source_name +) -> ConfigurationHandler: + """Returnes a configuration handler where args override configuration file.""" + args_as_config = ConfigurationHandler.create_override_config( + client_host=client_host, + source_name=source_name + ) + + config_handler: ConfigurationHandler = ConfigurationHandler.create_new( + get_default_configuration_file(), args_as_config + ) + + return config_handler + + +class ConfigurationHandler: + """Class for providing configuration.""" + + def __init__(self, configuration: dict): + """Initializes the configuration. + + Args: + configuration (dict): The configuration. + """ + self.client = _ClientConfig(**configuration["client"]) + self.source = _SourceConfig(**configuration["source"]) + + @staticmethod + def create_override_config( + client_host: str = None, + source_name: str = None, + ) -> dict: + """Create a dict that can be used for overriding the default configuration. + + Args: + host (str, optional): client host address. Defaults to None. + name (str, optional): source name. Defaults to None. + + Returns: + dict: the dict to ovverride configuration with. + """ + override_dict: dict = {} + if client_host: + DictUtils.merge_dicts({"client": {"host": client_host}}, override_dict) + if source_name: + DictUtils.merge_dicts({"source": {"name": source_name}}, override_dict) + + return override_dict + + @staticmethod + def create_new( + configuration_files: Union[list[str], str] = None, override_config: dict = None + ) -> ConfigurationHandler: + """Reads default configurationfile plus any additional configuration files provided. + + Additional configuration files will be merged with the default configfuration file. + Ovveride configuration will ovverid any configuration from files. + A configuration provider is returned. + + Args: + configuration_files (Union[list[str], str], optional): yaml or json configration file. \ + Defaults to None. + override_config (dict):override configuration from cli args. Defaults to None. + + Returns: + ConfigurationHandler: the handler. + """ + file_list = [] + if isinstance(configuration_files, str) or isinstance(configuration_files, Path): + file_list.append(configuration_files) + elif isinstance(configuration_files, list): + file_list.extend(configuration_files) + + reader = ConfigurationReader() + for _file in file_list: + reader.read_configuration(_file) + + # merge dicts + if override_config: + _config = copy.deepcopy(override_config) + DictUtils.merge_dicts(reader.configuration["configuration"], _config) + else: + _config = reader.configuration["configuration"] + + return ConfigurationHandler(_config) + + +@dataclass +class _ClientConfig: + host: str + +@dataclass +class _SourceConfig: + name: str diff --git a/cli/cdevents/cli/configuration_reader.py b/cli/cdevents/cli/configuration_reader.py new file mode 100644 index 0000000..e8f0c63 --- /dev/null +++ b/cli/cdevents/cli/configuration_reader.py @@ -0,0 +1,87 @@ +# Should support yaml, json file json config string +"""Module for configuration read and provisioning.""" +import copy +import json +import logging +from pathlib import Path +from typing import Union + +import yaml + +from cdevents.cli.utils import DictUtils + + +class ConfigurationReader: + """Handle reading of configuration. + + Configuration source can be provided as file paths or JSON string. + """ + + _YAML_FILE_SUFFIXES = [".yml", ".yaml"] + _JSON_FILE_SUFFIXES = [".json"] + + def __init__(self) -> None: # noqa: D107 + self._configuration: dict = {} + self._log = logging.getLogger(__name__) + + @property + def configuration(self) -> dict: + """Returns a deep copy of the configuration dict.""" + return copy.deepcopy(self._configuration) + + @configuration.setter + def configuration(self, configuration: dict): + """Merge existing configuration with new configuration. + + If the new configuration have the same corresponding keys, + the new configuration will overwrite the previous. + + Args: + configuration (dict): _description_ + """ + _config = copy.deepcopy(configuration) + if not self._configuration: + self._configuration = _config + else: + DictUtils.merge_dicts( + self._configuration, + _config, + ) + self._configuration = _config + + def read_configuration(self, configuration_file: Union[str, Path]): + """Read configuration from file. + + Args: + configuration (Union[str, Path]): Path to configuration file. + + Raises: + FileNotFoundError: Thrown if configuration file does not exist + ValueError: Thrown if file suffix is not supported. + """ + _config_file = Path(configuration_file) + self._log.debug("Reading configuration file '%s'", _config_file) + if not _config_file.exists(): + raise FileNotFoundError(f"Configuration file {_config_file} not found.") + elif self._is_supported_yaml_suffix(_config_file): + self.configuration = yaml.safe_load(_config_file.read_text(encoding="utf-8")) + + elif self._is_supported_json_suffix(_config_file): + self.configuration = json.loads(_config_file.read_text("utf-8")) + + else: + error_msg = ( + f"Unsupported file format {_config_file.suffix!r}," + + f" Supported formats: {self._YAML_FILE_SUFFIXES+self._JSON_FILE_SUFFIXES!r}" + ) + self._log.error(error_msg) + raise ValueError(error_msg) + self._log.debug("Configuration: '%s'", self.configuration) + + @staticmethod + def _is_supported_json_suffix(_config_file: Path) -> bool: + return _config_file.suffix.lower() in ConfigurationReader._JSON_FILE_SUFFIXES + + @staticmethod + def _is_supported_yaml_suffix(_config_file: Path) -> bool: + return _config_file.suffix.lower() in ConfigurationReader._YAML_FILE_SUFFIXES diff --git a/cli/cdevents/cli/constants.py b/cli/cdevents/cli/constants.py new file mode 100644 index 0000000..9996948 --- /dev/null +++ b/cli/cdevents/cli/constants.py @@ -0,0 +1,9 @@ +"""Constants for cdevents cli.""" +from pathlib import Path + +# configuration/configuration.yaml +_CLI_CDEVENTS_DIR = Path(__file__).parent.parent +DEFAULT_CONFIGURATION_FILE = str(Path(_CLI_CDEVENTS_DIR, "configuration", "configuration.yaml")) +LOGGING_CONFIGURATION_FILE = str( + Path(_CLI_CDEVENTS_DIR, "configuration", "logging-configuration.yaml") +) diff --git a/cli/cdevents/cli/env.py b/cli/cdevents/cli/env.py new file mode 100644 index 0000000..1d63481 --- /dev/null +++ b/cli/cdevents/cli/env.py @@ -0,0 +1,84 @@ +"""Module for cli environment commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.env import EnvEventCreatedEvent, EnvEventModifiedEvent, EnvEventDeletedEvent + +# pylint: disable=unused-argument +def common_env_options(function): + """Decorator for common cli options for environment.""" + function = click.option( + "--id", + "-i", + required=False, + type=str, + help="Environment's Id.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Environment's Name.", + )(function) + function = click.option( + "--repo", + "-r", + required=False, + type=str, + help="Environment's RepoUrl.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + type=(str, str), + multiple=True, + help="Environment's Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("Environment Created CloudEvent.")) +@common_env_options +def created( + id: str, + name: str = None, + repo: str = None, + data: List[str] = None, +): + print_function_args() + env_event = EnvEventCreatedEvent(id=id, name=name, repo=repo, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(env_event) + +@click.command(help=add_disclaimer_text("Environment Deleted CloudEvent.")) +@common_env_options +def deleted( + id: str, + name: str = None, + repo: str = None, + data: List[str] = None, +): + print_function_args() + env_event = EnvEventModifiedEvent(id=id, name=name, repo=repo, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(env_event) + +@click.command(help=add_disclaimer_text("Environment Modified CloudEvent.")) +@common_env_options +def modified( + id: str, + name: str = None, + repo: str = None, + data: List[str] = None, +): + print_function_args() + env_event = EnvEventDeletedEvent(id=id, name=name, repo=repo, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(env_event) diff --git a/cli/cdevents/cli/pipelinerun.py b/cli/cdevents/cli/pipelinerun.py new file mode 100644 index 0000000..dd732aa --- /dev/null +++ b/cli/cdevents/cli/pipelinerun.py @@ -0,0 +1,107 @@ +"""Module for cli pipelinerun commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.pipelinerun import PipelinerunStartedEvent, PipelinerunFinishedEvent, PipelinerunQueuedEvent + +# pylint: disable=unused-argument +def common_pipelinerun_options(function): + """Decorator for common cli options for pipelinerun.""" + function = click.option( + "--id", + "-i", + required=False, + type=str, + help="Pipeline Run Id.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Pipeline Run's Name.", + )(function) + function = click.option( + "--status", + "-s", + required=False, + type=str, + help="Pipeline Run's Status.", + )(function) + function = click.option( + "--url", + "-u", + required=False, + type=str, + help="Pipeline Run's URL.", + )(function) + function = click.option( + "--errors", + "-e", + required=False, + type=str, + help="Pipeline Run's Errors.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + type=(str, str), + multiple=True, + help="Pipeline Run's Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("PipelineRun Started CloudEvent.")) +@common_pipelinerun_options +def started( + id: str, + name: str = None, + status: str = None, + url: str = None, + errors: str = None, + data: List[str] = None, +): + print_function_args() + pipelinerun_event = PipelinerunStartedEvent(id=id, name=name, status=status, url=url, errors=errors, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(pipelinerun_event) + +@click.command(help=add_disclaimer_text("PipelineRun Finished CloudEvent.")) +@common_pipelinerun_options +def finished( + id: str, + name: str = None, + status: str = None, + url: str = None, + errors: str = None, + data: List[str] = None, +): + print_function_args() + pipelinerun_event = PipelinerunFinishedEvent(id=id, name=name, status=status, url=url, errors=errors, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(pipelinerun_event) + + +@click.command(help=add_disclaimer_text("PipelineRun Queued CloudEvent.")) +@common_pipelinerun_options +def queued( + id: str, + name: str = None, + status: str = None, + url: str = None, + errors: str = None, + data: List[str] = None, +): + print_function_args() + pipelinerun_event = PipelinerunQueuedEvent(id=id, name=name, status=status, url=url, errors=errors, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(pipelinerun_event) + + diff --git a/cli/cdevents/cli/repository.py b/cli/cdevents/cli/repository.py new file mode 100644 index 0000000..96404d4 --- /dev/null +++ b/cli/cdevents/cli/repository.py @@ -0,0 +1,87 @@ +"""Module for cli repository commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.repository import RepositoryCreatedEvent, RepositoryModifiedEvent, RepositoryDeletedEvent + +# pylint: disable=unused-argument +def common_repository_options(function): + """Decorator for common cli options for repository.""" + function = click.option( + "--id", + "-i", + required=False, + type=str, + help="Repository Id.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Repository's Name.", + )(function) + function = click.option( + "--url", + "-u", + required=False, + type=str, + help="Repository's URL.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + type=(str, str), + multiple=True, + help="Repository's Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("Repository Created CloudEvent.")) +@common_repository_options +def created( + id: str, + name: str = None, + url: str = None, + data: List[str] = None, +): + print_function_args() + repository_event = RepositoryCreatedEvent(id=id, name=name, url=url, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(repository_event) + + +@click.command(help=add_disclaimer_text("Repository Modified CloudEvent.")) +@common_repository_options +def modified( + id: str, + name: str = None, + url: str = None, + data: List[str] = None, +): + print_function_args() + repository_event = RepositoryModifiedEvent(id=id, name=name, url=url, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(repository_event) + + +@click.command(help=add_disclaimer_text("Repository Deleted CloudEvent.")) +@common_repository_options +def deleted( + id: str, + name: str = None, + url: str = None, + data: List[str] = None, +): + print_function_args() + repository_event = RepositoryDeletedEvent(id=id, name=name, url=url, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(repository_event) + diff --git a/cli/cdevents/cli/service.py b/cli/cdevents/cli/service.py new file mode 100644 index 0000000..6085200 --- /dev/null +++ b/cli/cdevents/cli/service.py @@ -0,0 +1,97 @@ +"""Module for cli service commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.service import ServiceDeployedEvent, ServiceUpgradedEvent, ServiceRolledbackEvent, ServiceRemovedEvent + +# pylint: disable=unused-argument +def common_service_options(function): + """Decorator for common cli options for service.""" + function = click.option( + "--envid", + "-e", + required=False, + type=str, + help="Environment Id where the Service is running.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Service's Name.", + )(function) + function = click.option( + "--version", + "-v", + required=False, + type=str, + help="Service's Version.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + type=(str, str), + multiple=True, + help="Service's Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("Service Deployed CloudEvent.")) +@common_service_options +def deployed( + envid: str, + name: str = None, + version: str = None, + data: List[str] = None, +): + print_function_args() + service_event = ServiceDeployedEvent(envid=envid, name=name, version=version, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(service_event) + +@click.command(help=add_disclaimer_text("Service Upgraded CloudEvent.")) +@common_service_options +def upgraded( + envid: str, + name: str = None, + version: str = None, + data: List[str] = None, +): + print_function_args() + service_event = ServiceUpgradedEvent(envid=envid, name=name, version=version, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(service_event) + +@click.command(help=add_disclaimer_text("Service Rolledback CloudEvent.")) +@common_service_options +def rolledback( + envid: str, + name: str = None, + version: str = None, + data: List[str] = None, +): + print_function_args() + service_event = ServiceRolledbackEvent(envid=envid, name=name, version=version, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(service_event) + +@click.command(help=add_disclaimer_text("Service Removed CloudEvent.")) +@common_service_options +def removed( + envid: str, + name: str = None, + version: str = None, + data: List[str] = None, +): + print_function_args() + service_event = ServiceRemovedEvent(envid=envid, name=name, version=version, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(service_event) diff --git a/cli/cdevents/cli/taskrun.py b/cli/cdevents/cli/taskrun.py new file mode 100644 index 0000000..50757c0 --- /dev/null +++ b/cli/cdevents/cli/taskrun.py @@ -0,0 +1,72 @@ +"""Module for cli taskrun commands.""" +from __future__ import annotations +from typing import List +import click + +from cdevents.cli.utils import add_disclaimer_text, print_function_args +from cdevents.cli.cdevents_command import CDeventsCommand + +from cdevents.core.taskrun import TaskRunStartedEvent, TaskRunFinishedEvent + +# pylint: disable=unused-argument +def common_taskrun_options(function): + """Decorator for common cli options for taskrun.""" + function = click.option( + "--id", + "-i", + required=False, + type=str, + help="Task Run Id.", + )(function) + function = click.option( + "--name", + "-n", + required=False, + type=str, + help="Task Run's Name.", + )(function) + function = click.option( + "--pipelineid", + "-p", + required=False, + type=str, + help="Task Run's Pipeline Id.", + )(function) + function = click.option( + "--data", + "-d", + required=False, + type=(str, str), + multiple=True, + help="Task Run's Data.", + )(function) + + return function + + +@click.command(help=add_disclaimer_text("TaskRun Started CloudEvent.")) +@common_taskrun_options +def started( + id: str, + name: str = None, + pipelineid: str = None, + data: List[str] = None, +): + print_function_args() + taskrun_event = TaskRunStartedEvent(id=id, name=name, pipelineid=pipelineid, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(taskrun_event) + +@click.command(help=add_disclaimer_text("TaskRun Finished CloudEvent.")) +@common_taskrun_options +def finished( + id: str, + name: str = None, + pipelineid: str = None, + data: List[str] = None, +): + print_function_args() + taskrun_event = TaskRunFinishedEvent(id=id, name=name, pipelineid=pipelineid, data=data) + cdevents_command = CDeventsCommand() + cdevents_command.run(taskrun_event) + diff --git a/cli/cdevents/cli/utils.py b/cli/cdevents/cli/utils.py new file mode 100644 index 0000000..441a6bd --- /dev/null +++ b/cli/cdevents/cli/utils.py @@ -0,0 +1,130 @@ +"""Only used for unit test.""" +from __future__ import annotations + +import datetime +import inspect +import os +from pathlib import Path + + +def print_function_args(): + """Prints name. parameter names and values of calling function. + + Mainly for test purpose. + """ + frame = inspect.stack()[1].frame + args, _, _, values = inspect.getargvalues(frame) + func_info_list = [f"func={inspect.stack()[1].function}"] + for arg in args: + func_info_list.append(f"{arg}={values.get(arg)}") + print(", ".join(func_info_list)) + + +def time_stamp(separator: str = "_", utc: bool = True) -> str: + """Returns a time stamp. + + format "YYYYmmddHHMMSSsss" + Args: + separator (str, optional):Separator char. Defaults to "_". + + Returns: + str: _description_ + """ + if utc: + return _utcnow().strftime(f"%Y%m%d{separator}%H%M%S%f")[:-3] + else: + return _now().strftime(f"%Y%m%d{separator}%H%M%S%f")[:-3] + + +def _utcnow() -> datetime.datetime: + return datetime.datetime.utcnow() + + +def _now() -> datetime.datetime: + return datetime.datetime.now() + + +class DictUtils: + """Utility class for dictionary related methods.""" + + @staticmethod + def merge_dicts(source: dict, destination: dict): + """Merges source with destination dict. + + If corresponding key exist in both source and destination, destination will not be + overwritten, unless the value is a list, then source list will be appended to exsting + distination list. + + Args: + source (dict): dict to be merge. + destination (dict): dict to be merged into. + """ + # pylint: disable=invalid-name + for k, v in source.items(): + if ( + k in destination + and isinstance(destination[k], dict) + and isinstance(source[k], dict) + ): + DictUtils.merge_dicts(source[k], destination[k]) + # Only update destination if key does not exist + elif k in destination and isinstance(destination[k], list): + destination[k].extend(v) + elif k not in destination: + destination[k] = v + + +def yaml_files_in_directory(config: Path) -> list[str]: + """List all yaml files in directory. + + Args: + config (Path): the directory. + + Raises: + FileNotFoundError: raised if source directory does not exist. + + Returns: + list[str]: list of yaml files. + """ + config_files = [] + if not os.path.exists(config): + raise FileNotFoundError(f"Path {config!r} does not exist.") + if os.path.isdir(config): + yaml_files = [ + str(Path(config, file)) + for file in os.listdir(config) + if file.endswith((".yaml", ".yml")) + ] + config_files.extend(yaml_files) + else: + config_files.append(str(config)) + return config_files + + +def is_windows() -> bool: + """True if the host system is Windows. + + Returns: + bool: the result. + """ + return _os_name() == "nt" + + +def _os_name() -> str: + return os.name + + +# TODO: Update disclaimer text +DISCLAIMER_TEXT = "" + + +def add_disclaimer_text(help_text: str) -> str: + """Return disclaimer text + help text. + + Returns: + str: Disclaimer + help_text. + """ + return f"""{DISCLAIMER_TEXT} + + {help_text} + """ diff --git a/cli/cdevents/configuration/__init__.py b/cli/cdevents/configuration/__init__.py new file mode 100644 index 0000000..8ca75fa --- /dev/null +++ b/cli/cdevents/configuration/__init__.py @@ -0,0 +1 @@ +"""Package only to enable access to config files after package install or build.""" diff --git a/cli/cdevents/configuration/configuration.yaml b/cli/cdevents/configuration/configuration.yaml new file mode 100644 index 0000000..e7fa2e8 --- /dev/null +++ b/cli/cdevents/configuration/configuration.yaml @@ -0,0 +1,6 @@ +--- +configuration: + source: + name: cde-cli + client: + host: http://localhost:8080 diff --git a/cli/cdevents/configuration/logging-configuration.yaml b/cli/cdevents/configuration/logging-configuration.yaml new file mode 100644 index 0000000..537698c --- /dev/null +++ b/cli/cdevents/configuration/logging-configuration.yaml @@ -0,0 +1,26 @@ +--- +version: 1 +disable_existing_loggers: false +formatters: + simple: + format: "%(asctime)s.%(msecs)03d [%(threadName)s] %(levelname)-s - %(message)-s - %(name)s:%(lineno)d" # yamllint disable-line + datefmt: "%Y-%m-%dT%H:%M:%S" +handlers: + console: + class: logging.StreamHandler + formatter: simple + stream: ext://sys.stdout + file: + class: logging.handlers.RotatingFileHandler + formatter: simple + filename: cdevents-client.log + maxBytes: 10485760 # 10MB + backupCount: 20 + encoding: utf8 +loggers: + logger-1: + handlers: [console, file] + propagate: true +root: + level: INFO + handlers: [console, file] diff --git a/cli/setup.cfg b/cli/setup.cfg new file mode 100644 index 0000000..7319fe5 --- /dev/null +++ b/cli/setup.cfg @@ -0,0 +1,64 @@ +[bumpversion] +current_version = 0.0.1 + +[bumpversion:file:./cdevents/cli/__init__.py] + +[metadata] +name = cdevents.cli +description = CDEvents CLI +long_description = file: README.md +version = attr: cdevents.cli.__version__ +author = doWhile +author_email = info@dowhile.com +keywords = cdevents, cli +url = todo +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: Other/Proprietary License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.9 + Topic :: Software Development :: Libraries :: Python Modules + +[options] +packages = find_namespace: +zip_safe = False +include_package_data = True +install_requires = + pytest + click>=8.0.4 + pyyaml>=6.0 + click-option-group + cloudevents + cdevents.core + +[options.extras_require] +dev = + black + bump2version + isort + mypy + pre-commit + pydocstyle + pytest + pytest_mock + yamllint + pylint +build = + wheel + +[options.packages.find] +include = + cdevents.* + +[bdist_wheel] +universal = 0 + +[options.entry_points] +console_scripts = + cdevents = cdevents.cli.__main__:cli diff --git a/cli/setup.py b/cli/setup.py new file mode 100644 index 0000000..f3d9343 --- /dev/null +++ b/cli/setup.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The setup script.""" + +__requires__ = "setuptools >= 44.0.0" + +from setuptools import setup + +setup() diff --git a/cli/tests/__init__.py b/cli/tests/__init__.py new file mode 100644 index 0000000..149f70c --- /dev/null +++ b/cli/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for core package.""" diff --git a/cli/tests/configuration_test.yaml b/cli/tests/configuration_test.yaml new file mode 100644 index 0000000..5018b4d --- /dev/null +++ b/cli/tests/configuration_test.yaml @@ -0,0 +1,8 @@ +# N.B. The file is used for testing the configuration handler. +# The content should reflect the content of the default configuration file cli/configuration/configuration.yaml +--- +configuration: + source: + name: cde-cli + client: + host: http://localhost:8080 diff --git a/cli/tests/test_artifact.py b/cli/tests/test_artifact.py new file mode 100644 index 0000000..4ed109e --- /dev/null +++ b/cli/tests/test_artifact.py @@ -0,0 +1,80 @@ +"""Unit tests for artifact.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.artifact import packaged, published +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ID_ARG = "id" +NAME_ARG = "name" +VERSION_ARG = "version" +DATA_ARG = "data" + +@pytest.mark.unit +def test_packaged(runner: CliRunner): + """Test packaging of an artifact.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_version = "MyArtifact" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + packaged, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{VERSION_ARG}", + expected_version, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{VERSION_ARG}={expected_version}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + + + +@pytest.mark.unit +def test_published(runner: CliRunner): + """Test published of an artifact.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_version = "MyArtifact" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + published, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{VERSION_ARG}", + expected_version, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{VERSION_ARG}={expected_version}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + diff --git a/cli/tests/test_branch.py b/cli/tests/test_branch.py new file mode 100644 index 0000000..c2f8cfd --- /dev/null +++ b/cli/tests/test_branch.py @@ -0,0 +1,77 @@ +"""Unit tests for branch.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.branch import created, deleted +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ID_ARG = "id" +NAME_ARG = "name" +REPOID_ARG = "repoid" +DATA_ARG = "data" + +@pytest.mark.unit +def test_created(runner: CliRunner): + """Test created of a branch.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_repoid = "repo1" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + created, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{REPOID_ARG}", + expected_repoid, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{REPOID_ARG}={expected_repoid}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_deleted(runner: CliRunner): + """Test deleted of a branch.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_repoid = "repo1" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + deleted, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{REPOID_ARG}", + expected_repoid, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{REPOID_ARG}={expected_repoid}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout diff --git a/cli/tests/test_build.py b/cli/tests/test_build.py new file mode 100644 index 0000000..9c6380d --- /dev/null +++ b/cli/tests/test_build.py @@ -0,0 +1,108 @@ +"""Unit tests for build.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.build import started, finished, queued +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ID_ARG = "id" +NAME_ARG = "name" +ARTIFACT_ARG = "artifact" +DATA_ARG = "data" + +@pytest.mark.unit +def test_started(runner: CliRunner): + """Test started of a build.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_artifact = "artifact1" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + started, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{ARTIFACT_ARG}", + expected_artifact, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{ARTIFACT_ARG}={expected_artifact}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + + +@pytest.mark.unit +def test_finished(runner: CliRunner): + """Test finished of a build.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_artifact = "artifact1" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + finished, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{ARTIFACT_ARG}", + expected_artifact, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{ARTIFACT_ARG}={expected_artifact}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + + +@pytest.mark.unit +def test_queued(runner: CliRunner): + """Test queued of a build.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_artifact = "artifact1" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + queued, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{ARTIFACT_ARG}", + expected_artifact, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{ARTIFACT_ARG}={expected_artifact}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + diff --git a/cli/tests/test_configuration_handler.py b/cli/tests/test_configuration_handler.py new file mode 100644 index 0000000..8d36bc5 --- /dev/null +++ b/cli/tests/test_configuration_handler.py @@ -0,0 +1,49 @@ +"""Module for tests related to configuration_handler.""" +import os +from pathlib import Path + +import pytest + +from cdevents.cli.configuration_handler import ConfigurationHandler + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +THIS_DIR: Path = Path(__file__).parent +TEST_CONFIG_FILE: str = Path(THIS_DIR, "configuration_test.yaml").as_posix() + + +@pytest.mark.unit +def test_create_config_handler(): + + handler = ConfigurationHandler.create_new(TEST_CONFIG_FILE) + assert isinstance(handler, ConfigurationHandler) + + assert handler.source.name == "cde-cli" + assert handler.client.host == "http://localhost:8080" + + +@pytest.mark.unit +def test_create_config_handler_with_override(): + + expected_source_name = "MySourceName" + expected_client_host = "http://myhost:5000" + override_config = { + "source": { + "name": expected_source_name, + }, + "client": { + "host": expected_client_host, + }, + } + handler = ConfigurationHandler.create_new(TEST_CONFIG_FILE, override_config=override_config) + assert isinstance(handler, ConfigurationHandler) + + assert handler.source.name == expected_source_name + assert handler.client.host == expected_client_host + + +@pytest.mark.unit +def test_create_empty_override_dict(): + + expected = {} + actual = ConfigurationHandler.create_override_config() + assert expected == actual diff --git a/cli/tests/test_configuration_reader.py b/cli/tests/test_configuration_reader.py new file mode 100644 index 0000000..c0fce6f --- /dev/null +++ b/cli/tests/test_configuration_reader.py @@ -0,0 +1,99 @@ +"""Module testing ConfigurationReader""" +import json +import unittest +from pathlib import Path +from unittest.mock import patch + +import pytest +import yaml + +from cdevents.cli.configuration_reader import ConfigurationReader + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +TEST_DIR = Path(__file__).parent +CONFIG_DIR: Path = Path(TEST_DIR, "configurations") + +YAML_CONFIG_1 = """--- +configuration: + source: + name: cde-cli + client: + host: http://localhost:8080 +""" +DICT_CONFIG_1 = yaml.safe_load(YAML_CONFIG_1) + +JSON_CONFIG_1 = json.dumps(DICT_CONFIG_1) +YAML_CONFIG_2 = """--- +configuration: + source: + name: mysource + client: + host: http://myhost:5000 +""" +DICT_CONFIG_2 = { + "configuration": { + "source": {"name": "mysource"}, + "client": {"host": "http://myhost:5000"}, + }, +} + +DUMMY_PATH = "my/dummy/path/dummy.yaml" + + +class TestConfigurationReader(unittest.TestCase): + # pylint: disable=unused-argument + def setUp(self) -> None: + self.reader = ConfigurationReader() + + @pytest.mark.unit + @patch.object(Path, "exists", return_value=False) + def test_read_configuration_with_invalid_file_path_then_exception(self, mocked_exists): + with self.assertRaises(FileNotFoundError): + self.reader.read_configuration(Path(DUMMY_PATH)) + + @pytest.mark.unit + @patch.object(Path, "read_text", return_value=YAML_CONFIG_1) + @patch.object(Path, "exists", return_value=True) + def test_read_configuration_with_valid_yaml_file(self, mocked_exists, mocked_read_text): + self.reader.read_configuration(Path(DUMMY_PATH)) + actual = self.reader.configuration + self.assertDictEqual(actual, DICT_CONFIG_1) + + @pytest.mark.unit + @patch.object(Path, "read_text", return_value=JSON_CONFIG_1) + @patch.object(Path, "exists", return_value=True) + def test_read_configuration_with_valid_json_file(self, mocked_exists, mocked_read_text): + self.reader.read_configuration(Path("path/to/my/config.json")) + actual = self.reader.configuration + self.assertDictEqual(actual, DICT_CONFIG_1) + + @pytest.mark.unit + @patch.object(Path, "exists", return_value=True) + def test_read_configuration_with_invalid_filetype_then_value_error( + self, + mocked_exists, + ): + with self.assertRaises(ValueError): + self.reader.read_configuration(Path("path/to/my/invalid_config.jsonX")) + + @pytest.mark.unit + @patch.object(Path, "read_text", side_effect=[YAML_CONFIG_1, YAML_CONFIG_2]) + @patch.object(Path, "exists", return_value=True) + def test_read_configuration_with_2_configs(self, mocked_exists, mocked_read_text): + """Second configuration overwrites previous.""" + self.reader.read_configuration(Path(DUMMY_PATH)) + actual = self.reader.configuration + self.assertDictEqual(actual, DICT_CONFIG_1) + self.reader.read_configuration(Path("dummy_config2.yml")) + actual_2 = self.reader.configuration + self.assertDictEqual(actual_2, DICT_CONFIG_2) + + @pytest.mark.unit + def test_is_supported_suffix_with_valid_suffix(self): + self.assertTrue(ConfigurationReader._is_supported_json_suffix(Path("valid_suffix.json"))) + self.assertTrue(ConfigurationReader._is_supported_yaml_suffix(Path("valid_suffix.yml"))) + self.assertTrue(ConfigurationReader._is_supported_yaml_suffix(Path("valid_suffix.yAMl"))) + + @pytest.mark.unit + def test_is_supported_suffix_with_invalid_suffix(self): + self.assertFalse(ConfigurationReader._is_supported_yaml_suffix(Path("valid_suffix.berra"))) diff --git a/cli/tests/test_constants.py b/cli/tests/test_constants.py new file mode 100644 index 0000000..085e21f --- /dev/null +++ b/cli/tests/test_constants.py @@ -0,0 +1,17 @@ +"""Tests for constants.""" +from pathlib import Path + +import pytest + +from cdevents.cli.constants import DEFAULT_CONFIGURATION_FILE, LOGGING_CONFIGURATION_FILE + +# pylint: disable=missing-function-docstring, missing-class-docstring + +@pytest.mark.unit +def test_default_config_exist(): + assert Path(DEFAULT_CONFIGURATION_FILE).exists() + + +@pytest.mark.unit +def test_logging_config_file_exist(): + assert Path(LOGGING_CONFIGURATION_FILE).exists() diff --git a/cli/tests/test_env.py b/cli/tests/test_env.py new file mode 100644 index 0000000..ca293f8 --- /dev/null +++ b/cli/tests/test_env.py @@ -0,0 +1,108 @@ +"""Unit tests for env.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.env import created, deleted, modified +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ID_ARG = "id" +NAME_ARG = "name" +REPO_ARG = "repo" +DATA_ARG = "data" + +@pytest.mark.unit +def test_started(runner: CliRunner): + """Test started of an env.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_repo = "my-repo" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + created, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{REPO_ARG}", + expected_repo, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{REPO_ARG}={expected_repo}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + + +@pytest.mark.unit +def test_started(runner: CliRunner): + """Test started of an env.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_repo = "my-repo" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + deleted, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{REPO_ARG}", + expected_repo, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{REPO_ARG}={expected_repo}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + + +@pytest.mark.unit +def test_started(runner: CliRunner): + """Test started of an env.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_repo = "my-repo" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + modified, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{REPO_ARG}", + expected_repo, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{REPO_ARG}={expected_repo}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + diff --git a/cli/tests/test_main.py b/cli/tests/test_main.py new file mode 100644 index 0000000..8fdb00e --- /dev/null +++ b/cli/tests/test_main.py @@ -0,0 +1,15 @@ +import pytest +from click.testing import CliRunner + +from cdevents.cli.__main__ import cli + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +@pytest.mark.unit +def test_cli_main(runner: CliRunner): + + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 diff --git a/cli/tests/test_pipelinerun.py b/cli/tests/test_pipelinerun.py new file mode 100644 index 0000000..b90ac8f --- /dev/null +++ b/cli/tests/test_pipelinerun.py @@ -0,0 +1,133 @@ +"""Unit tests for pipelinerun.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.pipelinerun import started, finished, queued +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ID_ARG = "id" +NAME_ARG = "name" +STATUS_ARG = "status" +URL_ARG = "url" +ERRORS_ARG = "errors" +DATA_ARG = "data" + +@pytest.mark.unit +def test_started(runner: CliRunner): + """Test started of an env.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_status = "success" + expected_url = "https://my-url.com" + expected_errors = "my-errors" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + started, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{STATUS_ARG}", + expected_status, + f"--{URL_ARG}", + expected_url, + f"--{ERRORS_ARG}", + expected_errors, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{STATUS_ARG}={expected_status}" in result.stdout + assert f"{URL_ARG}={expected_url}" in result.stdout + assert f"{ERRORS_ARG}={expected_errors}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + + +@pytest.mark.unit +def test_finished(runner: CliRunner): + """Test finished of an env.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_status = "success" + expected_url = "https://my-url.com" + expected_errors = "my-errors" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + finished, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{STATUS_ARG}", + expected_status, + f"--{URL_ARG}", + expected_url, + f"--{ERRORS_ARG}", + expected_errors, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{STATUS_ARG}={expected_status}" in result.stdout + assert f"{URL_ARG}={expected_url}" in result.stdout + assert f"{ERRORS_ARG}={expected_errors}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_queued(runner: CliRunner): + """Test queued of an env.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_status = "success" + expected_url = "https://my-url.com" + expected_errors = "my-errors" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + queued, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{STATUS_ARG}", + expected_status, + f"--{URL_ARG}", + expected_url, + f"--{ERRORS_ARG}", + expected_errors, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{STATUS_ARG}={expected_status}" in result.stdout + assert f"{URL_ARG}={expected_url}" in result.stdout + assert f"{ERRORS_ARG}={expected_errors}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + diff --git a/cli/tests/test_repository.py b/cli/tests/test_repository.py new file mode 100644 index 0000000..bd13089 --- /dev/null +++ b/cli/tests/test_repository.py @@ -0,0 +1,105 @@ +"""Unit tests for repository.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.repository import created, deleted, modified +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ID_ARG = "id" +NAME_ARG = "name" +URL_ARG = "url" +DATA_ARG = "data" + +@pytest.mark.unit +def test_created(runner: CliRunner): + """Test created of an repository.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_url = "https://my-url.com" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + created, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{URL_ARG}", + expected_url, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{URL_ARG}={expected_url}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_modified(runner: CliRunner): + """Test modified of an repository.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_url = "https://my-url.com" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + modified, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{URL_ARG}", + expected_url, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{URL_ARG}={expected_url}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_deleted(runner: CliRunner): + """Test deleted of an repository.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_url = "https://my-url.com" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + deleted, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{URL_ARG}", + expected_url, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{URL_ARG}={expected_url}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout diff --git a/cli/tests/test_service.py b/cli/tests/test_service.py new file mode 100644 index 0000000..cb9bca0 --- /dev/null +++ b/cli/tests/test_service.py @@ -0,0 +1,134 @@ +"""Unit tests for service.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.service import deployed, upgraded, removed, rolledback +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ENVID_ARG = "envid" +NAME_ARG = "name" +VERSION_ARG = "version" +DATA_ARG = "data" + +@pytest.mark.unit +def test_deployed(runner: CliRunner): + """Test deployed of an service.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_version = "1.0.0" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + deployed, + [ + f"--{ENVID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{VERSION_ARG}", + expected_version, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ENVID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{VERSION_ARG}={expected_version}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_upgraded(runner: CliRunner): + """Test upgraded of an service.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_version = "1.0.0" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + upgraded, + [ + f"--{ENVID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{VERSION_ARG}", + expected_version, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ENVID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{VERSION_ARG}={expected_version}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_removed(runner: CliRunner): + """Test removed of an service.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_version = "1.0.0" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + removed, + [ + f"--{ENVID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{VERSION_ARG}", + expected_version, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ENVID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{VERSION_ARG}={expected_version}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_rolledback(runner: CliRunner): + """Test rolledback of an service.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_version = "1.0.0" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + rolledback, + [ + f"--{ENVID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{VERSION_ARG}", + expected_version, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ENVID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{VERSION_ARG}={expected_version}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout diff --git a/cli/tests/test_taskrun.py b/cli/tests/test_taskrun.py new file mode 100644 index 0000000..ba134d2 --- /dev/null +++ b/cli/tests/test_taskrun.py @@ -0,0 +1,76 @@ +"""Unit tests for taskrun.""" +import pytest +from unittest.mock import patch +from click.testing import CliRunner + +from cdevents.cli.taskrun import started, finished +from cdevents.cli.cdevents_command import CDeventsCommand + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +ID_ARG = "id" +NAME_ARG = "name" +PIPLINEID_ARG = "pipelineid" +DATA_ARG = "data" + +@pytest.mark.unit +def test_started(runner: CliRunner): + """Test started of an taskrun.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_pipelineid = "pipeline1" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + started, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{PIPLINEID_ARG}", + expected_pipelineid, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{PIPLINEID_ARG}={expected_pipelineid}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout + +@pytest.mark.unit +def test_finished(runner: CliRunner): + """Test finished of an taskrun.""" + + expected_id = "task1" + expected_name = "My Task Run" + expected_pipelineid = "pipeline1" + expected_data = ["key1", "value1"] + + with patch.object(CDeventsCommand, "run", spec=CDeventsCommand): + result = runner.invoke( + finished, + [ + f"--{ID_ARG}", + expected_id, + f"--{NAME_ARG}", + expected_name, + f"--{PIPLINEID_ARG}", + expected_pipelineid, + f"--{DATA_ARG}", + *expected_data, + ], + ) + assert result.exit_code == 0 + assert f"{ID_ARG}={expected_id}" in result.stdout + assert f"{NAME_ARG}={expected_name}" in result.stdout + assert f"{PIPLINEID_ARG}={expected_pipelineid}" in result.stdout + assert f"{DATA_ARG}=({tuple(expected_data)},)" in result.stdout diff --git a/cli/tests/test_utils.py b/cli/tests/test_utils.py new file mode 100644 index 0000000..a65d0b0 --- /dev/null +++ b/cli/tests/test_utils.py @@ -0,0 +1,99 @@ +"""Testing for module utils.""" +from datetime import datetime +from unittest import mock + +import pytest + +from cdevents.cli.utils import DictUtils, time_stamp + +# pylint: disable=missing-function-docstring, protected-access, missing-class-docstring +@pytest.mark.unit +def test_merge_dicts_1(): + source = {"a": 66} + target = {"b": 77, "c": 88} + expected = {"a": 66, "b": 77, "c": 88} + merge_and_test(source, target, expected) + + +@pytest.mark.unit +def test_merge_dicts_2(): + source = {"a": 66} + target = {"a": 77} + expected = {"a": 77} + merge_and_test(source, target, expected) + + +@pytest.mark.unit +def test_merge_dicts_3(): + source = {} + target = {"a": 77} + expected = {"a": 77} + merge_and_test(source, target, expected) + + +@pytest.mark.unit +def test_merge_dicts_3_1(): + target = {} + source = {"a": 77} + expected = {"a": 77} + merge_and_test(source, target, expected) + + +@pytest.mark.unit +def test_merge_dicts_4(): + source = {"a": {"aa": 11, "aaa": {"aaaa": 111}}} + target = {"b": {"bb": 22, "bbb": {"bbbb": 222}}} + expected = { + "a": {"aa": 11, "aaa": {"aaaa": 111}}, + "b": {"bb": 22, "bbb": {"bbbb": 222}}, + } + merge_and_test(source, target, expected) + + +@pytest.mark.unit +def test_merge_dicts_5(): + source = { + "a": {"aa": 12, "aaa": {"aaaa": 222, "bbb": 333}}, + "b": 4, + "c": [8, 9], + "d": [1], + } + target = { + "a": {"aa": 11, "aaa": {"aaaa": 111}}, + "b": 5, + "c": [1, 2, 3], + "d": {"v": 1}, + } + expected = { + "a": {"aa": 11, "aaa": {"aaaa": 111, "bbb": 333}}, + "b": 5, + "c": [1, 2, 3, 8, 9], + "d": {"v": 1}, + } + merge_and_test(source, target, expected) + + +def merge_and_test(source, target, expected): + DictUtils.merge_dicts(source, target) + assert target == expected + + +# FIXME: add test for times_tamep +# VALID_TIME_STAMP_REGEX= +EXPECTED_DATETIME = datetime( + year=2022, + month=1, + day=12, + hour=10, + minute=44, + second=23, +) +EXPECTED_TIMESTAMP = "20220112T104423000" + + +@pytest.mark.unit +@mock.patch("cdevents.cli.utils._utcnow") +def test_time_stamp(mock_utcnow): + mock_utcnow.return_value = EXPECTED_DATETIME + actual = time_stamp("T") + assert actual == EXPECTED_TIMESTAMP diff --git a/cli/tests/test_version.py b/cli/tests/test_version.py new file mode 100644 index 0000000..5bd0a1e --- /dev/null +++ b/cli/tests/test_version.py @@ -0,0 +1,16 @@ +import re + +import pytest + +from cdevents.cli import __version__ + +# From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +VALID_SEMVER_REGEX = ( + r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" + r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" +) + + +@pytest.mark.unit +def test_version_is_semantic(): + assert re.fullmatch(VALID_SEMVER_REGEX, __version__) diff --git a/core/Makefile b/core/Makefile new file mode 100644 index 0000000..0bf7a38 --- /dev/null +++ b/core/Makefile @@ -0,0 +1,7 @@ +include ../header.mk + +SRC := cdevents/core + +MAKEFILE_LIST=../targets.mk + +include $(MAKEFILE_LIST) diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..c0aa5b4 --- /dev/null +++ b/core/README.md @@ -0,0 +1 @@ +# CDevents Core diff --git a/core/cdevents/core/__init__.py b/core/cdevents/core/__init__.py new file mode 100644 index 0000000..b6c803e --- /dev/null +++ b/core/cdevents/core/__init__.py @@ -0,0 +1,4 @@ +"""CDEvents CLI provides command-line interaction with Cd-Event client functionality.""" + +__version__ = "0.0.1" + diff --git a/core/cdevents/core/artifact.py b/core/cdevents/core/artifact.py new file mode 100644 index 0000000..554b1cb --- /dev/null +++ b/core/cdevents/core/artifact.py @@ -0,0 +1,45 @@ +"""artifact""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class ArtifactEvent(Event): + """Artifact Event.""" + + def __init__(self, artifact_type: EventType, id: str=None, name: str=None, version: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = artifact_type + self._id = id + self._name = name + self._version = version + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "artifactid": self._id, + "artifactname": self._name, + "artifactversion": self._version, + } + return extensions + +class ArtifactPackagedEvent(ArtifactEvent): + + def __init__(self, id: str=None, name: str=None, version: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.ArtifactPackagedEventV1 + + super().__init__(artifact_type=self._event_type, id=id, name=name, version=version, attrs=attrs, data=data) + + +class ArtifactPublishedEvent(ArtifactEvent): + + def __init__(self, id: str=None, name: str=None, version: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.ArtifactPublishedEventV1 + + super().__init__(artifact_type=self._event_type, id=id, name=name, version=version, attrs=attrs, data=data) diff --git a/core/cdevents/core/branch.py b/core/cdevents/core/branch.py new file mode 100644 index 0000000..f8d3b41 --- /dev/null +++ b/core/cdevents/core/branch.py @@ -0,0 +1,44 @@ +"""branch""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class BranchEvent(Event): + """Branch Event.""" + + def __init__(self, branch_type: EventType, id: str=None, name: str=None, repoid: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = branch_type + self._id = id + self._name = name + self._repoid = repoid + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "branchid": self._id, + "branchname": self._name, + "branchrepositoryid": self._repoid, + } + return extensions + +class BranchCreatedEvent(BranchEvent): + + def __init__(self, id: str=None, name: str=None, repoid: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.BranchCreatedEventV1 + + super().__init__(branch_type=self._event_type, id=id, name=name, repoid=repoid, attrs=attrs, data=data) + +class BranchDeletedEvent(BranchEvent): + + def __init__(self, id: str=None, name: str=None, repoid: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.BranchDeletedEventV1 + + super().__init__(branch_type=self._event_type, id=id, name=name, repoid=repoid, attrs=attrs, data=data) diff --git a/core/cdevents/core/build.py b/core/cdevents/core/build.py new file mode 100644 index 0000000..0746c33 --- /dev/null +++ b/core/cdevents/core/build.py @@ -0,0 +1,53 @@ +"""build""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class BuildEvent(Event): + """Build Event.""" + + def __init__(self, build_type: EventType, id: str=None, name: str=None, artifact: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = build_type + self._id = id + self._name = name + self._artifact = artifact + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "buildid": self._id, + "buildname": self._name, + "buildartifactid": self._artifact, + } + return extensions + +class BuildStartedEvent(BuildEvent): + """Build Started Event.""" + def __init__(self, id: str=None, name: str=None, artifact: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.BuildStartedEventV1 + + super().__init__(build_type=self._event_type, id=id, name=name, artifact=artifact, attrs=attrs, data=data) + +class BuildQueuedEvent(BuildEvent): + """Build Queued Event.""" + def __init__(self, id: str=None, name: str=None, artifact: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.BuildQueuedEventV1 + + super().__init__(build_type=self._event_type, id=id, name=name, artifact=artifact, attrs=attrs, data=data) + +class BuildFinishedEvent(BuildEvent): + """Build Finished Event.""" + def __init__(self, id: str=None, name: str=None, artifact: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.BuildFinishedEventV1 + + super().__init__(build_type=self._event_type, id=id, name=name, artifact=artifact, attrs=attrs, data=data) diff --git a/core/cdevents/core/env.py b/core/cdevents/core/env.py new file mode 100644 index 0000000..d013dd4 --- /dev/null +++ b/core/cdevents/core/env.py @@ -0,0 +1,54 @@ +"""env""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class EnvEvent(Event): + """Env Event.""" + + def __init__(self, env_type: EventType, id: str, name: str, repo: str, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = env_type + self._id = id + self._name = name + self._repo = repo + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "envId": self._id, + "envname": self._name, + "envrepourl": self._repo, + } + return extensions + +class EnvEventCreatedEvent(EnvEvent): + + def __init__(self, id: str=None, name: str=None, repo: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.EnvironmentCreatedEventV1 + + super().__init__(env_type=self._event_type, id=id, name=name, repo=repo, attrs=attrs, data=data) + +class EnvEventModifiedEvent(EnvEvent): + + def __init__(self, id: str=None, name: str=None, repo: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.EnvironmentModifiedEventV1 + + super().__init__(env_type=self._event_type, id=id, name=name, repo=repo, attrs=attrs, data=data) + +class EnvEventDeletedEvent(EnvEvent): + + def __init__(self, id: str=None, name: str=None, repo: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.EnvironmentDeletedEventV1 + + super().__init__(env_type=self._event_type, id=id, name=name, repo=repo, attrs=attrs, data=data) + diff --git a/core/cdevents/core/event.py b/core/cdevents/core/event.py new file mode 100644 index 0000000..4217e43 --- /dev/null +++ b/core/cdevents/core/event.py @@ -0,0 +1,31 @@ +"""Core events.""" + +from abc import abstractmethod +from cloudevents.http import CloudEvent + +class Event(CloudEvent): + """Event.""" + + def __init__(self, event_type: str, extensions: dict, attrs=None, data = {}): + """Initializes class. + """ + if attrs: + super().__init__(attributes=attrs, data=data) + else: + self._event_type = event_type + self._extensions = extensions + self._data = data + + self._attributes = { + "type": self._event_type, + "source": "cde-cli", + "extensions": self._extensions, + } + super().__init__(self._attributes, dict(self._data)) + + @abstractmethod + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = {} + return extensions diff --git a/core/cdevents/core/event_sender.py b/core/cdevents/core/event_sender.py new file mode 100644 index 0000000..6ac55f6 --- /dev/null +++ b/core/cdevents/core/event_sender.py @@ -0,0 +1,25 @@ +"""Core events sender.""" + +import requests + +from cloudevents.http import CloudEvent, to_structured + +class EventSender(): + """Events Sender.""" + + def __init__(self, cde_link: str = None): + """Initializes class. + """ + self._cde_link = cde_link + if cde_link is None: + self._cde_link = "http://localhost:8080" + + + def send(self, event: CloudEvent): + """send events. + """ + headers, body = to_structured(event) + + # send and print event + result = requests.post(self._cde_link, headers=headers, data=body) + print(f"Response with state code {result.status_code}") diff --git a/core/cdevents/core/event_type.py b/core/cdevents/core/event_type.py new file mode 100644 index 0000000..6b43dd1 --- /dev/null +++ b/core/cdevents/core/event_type.py @@ -0,0 +1,63 @@ +"""Constants Event types.""" + +from enum import Enum + +# pylint: TODO: + +# # Change Events +# ChangeCreatedEventV1 :str = "cd.repository.change.created.v1" +# ChangeUpdatedEventV1 :str = "cd.repository.change.updated.v1" +# ChangeReviewedEventV1 :str = "cd.repository.change.reviewed.v1" +# ChangeMergedEventV1 :str = "cd.repository.change.merged.v1" +# ChangeAbandonedEventV1 :str = "cd.repository.change.abandoned.v1" + +# # TestCase Events +# TestCaseStartedEventV1 :str = "cd.test.case.started.v1" +# TestCaseQueuedEventV1 :str = "cd.test.case.queued.v1" +# TestCaseFinishedEventV1 :str = "cd.test.case.finished.v1" + +# # TestSuite Events +# TestSuiteStartedEventV1 :str = "cd.test.suite.started.v1" +# TestSuiteQueuedEventV1 :str = "cd.test.suite.queued.v1" +# TestSuiteFinishedEventV1 :str = "cd.test.suite.finished.v1" + +class EventType(Enum): + """Constants Event types.""" + + # Artifact Events + ArtifactPackagedEventV1: str = "cd.artifact.packaged.v1" + ArtifactPublishedEventV1: str = "cd.artifact.published.v1" + + # Branch Events + BranchCreatedEventV1: str = "cd.repository.branch.created.v1" + BranchDeletedEventV1: str = "cd.repository.branch.deleted.v1" + + # Build Events + BuildStartedEventV1 :str = "cd.build.started.v1" + BuildQueuedEventV1 :str = "cd.build.queued.v1" + BuildFinishedEventV1 :str = "cd.build.finished.v1" + + # Environment Events + EnvironmentCreatedEventV1 :str = "cd.environment.created.v1" + EnvironmentModifiedEventV1 :str = "cd.environment.modified.v1" + EnvironmentDeletedEventV1 :str = "cd.environment.deleted.v1" + + # PipelineRun Events + PipelineRunStartedEventV1 :str = "cd.pipelinerun.started.v1" + PipelineRunFinishedEventV1 :str = "cd.pipelinerun.finished.v1" + PipelineRunQueuedEventV1 :str = "cd.pipelinerun.queued.v1" + + # Repository Events + RepositoryCreatedEventV1 :str = "cd.repository.created.v1" + RepositoryModifiedEventV1 :str = "cd.repository.modified.v1" + RepositoryDeletedEventV1 :str = "cd.repository.deleted.v1" + + # Service Events + ServiceDeployedEventV1 :str = "cd.service.deployed.v1" + ServiceUpgradedEventV1 :str = "cd.service.upgraded.v1" + ServiceRolledbackEventV1 :str = "cd.service.rolledback.v1" + ServiceRemovedEventV1 :str = "cd.service.removed.v1" + + # TaskRun Events + TaskRunStartedEventV1 :str = "cd.taskrun.started.v1" + TaskRunFinishedEventV1 :str = "cd.taskrun.finished.v1" diff --git a/core/cdevents/core/extensions/artifact_extension.py b/core/cdevents/core/extensions/artifact_extension.py new file mode 100644 index 0000000..80a0426 --- /dev/null +++ b/core/cdevents/core/extensions/artifact_extension.py @@ -0,0 +1,28 @@ +"""ArtifactExtension represents the extension for extension context.""" + +from cloudevents.http import CloudEvent + +ArtifactIdExtension = "artifactid" +ArtifactNameExtension = "artifactname" +ArtifactVersionExtension = "artifactversion" + + +class ArtifactExtension(): + """Artifact Extension.""" + def __init__(self) -> None: + pass + + def read_transformer(): + """Read transformer.""" + pass + + def write_transformer(event: CloudEvent, extensions: dict) -> CloudEvent: + """Write transformer.""" + if event._attributes["extensions"].get(ArtifactIdExtension): + event._attributes["extensions"].set(ArtifactIdExtension, extensions.get(ArtifactIdExtension)) + if event._attributes["extensions"].get(ArtifactNameExtension): + event._attributes["extensions"].set(ArtifactNameExtension, extensions.get(ArtifactNameExtension)) + if event._attributes["extensions"].get(ArtifactVersionExtension): + event._attributes["extensions"].set(ArtifactVersionExtension, extensions.get(ArtifactVersionExtension)) + return event + diff --git a/core/cdevents/core/extensions/service_extension.py b/core/cdevents/core/extensions/service_extension.py new file mode 100644 index 0000000..23474db --- /dev/null +++ b/core/cdevents/core/extensions/service_extension.py @@ -0,0 +1,28 @@ +"""ServiceExtension represents the extension for extension context.""" + +from cloudevents.http import CloudEvent + +ServiceEnvIdExtension = "serviceenvid" +ServiceNameExtension = "servicename" +ServiceVersionExtension = "serviceversion" + + +class ServiceExtension(): + """Service Extension.""" + def __init__(self) -> None: + pass + + def read_transformer(): + """Read transformer.""" + pass + + def write_transformer(event: CloudEvent, extensions: dict) -> CloudEvent: + """Write transformer.""" + if event._attributes["extensions"].get(ServiceEnvIdExtension): + event._attributes["extensions"].set(ServiceEnvIdExtension, extensions.get(ServiceEnvIdExtension)) + if event._attributes["extensions"].get(ServiceNameExtension): + event._attributes["extensions"].set(ServiceNameExtension, extensions.get(ServiceNameExtension)) + if event._attributes["extensions"].get(ServiceVersionExtension): + event._attributes["extensions"].set(ServiceVersionExtension, extensions.get(ServiceVersionExtension)) + return event + diff --git a/core/cdevents/core/http_handlar.py b/core/cdevents/core/http_handlar.py new file mode 100644 index 0000000..3e9351c --- /dev/null +++ b/core/cdevents/core/http_handlar.py @@ -0,0 +1,157 @@ +import typing +import json + +# from cloudevents.http import from_http +import cloudevents.exceptions as cloud_exceptions +from cloudevents.http.event import CloudEvent +from cloudevents.http.event_type import is_binary +from cloudevents.http.mappings import _obj_by_version +from cloudevents.http.util import _json_or_string +from cloudevents.sdk import marshaller, types + + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +from cdevents.core.artifact import ArtifactPackagedEvent, ArtifactPublishedEvent +from cdevents.core.branch import BranchCreatedEvent, BranchDeletedEvent +from cdevents.core.build import BuildStartedEvent, BuildFinishedEvent, BuildQueuedEvent +from cdevents.core.env import EnvEventCreatedEvent, EnvEventModifiedEvent, EnvEventDeletedEvent +from cdevents.core.pipelinerun import PipelinerunStartedEvent, PipelinerunFinishedEvent, PipelinerunQueuedEvent +from cdevents.core.repository import RepositoryCreatedEvent, RepositoryModifiedEvent, RepositoryDeletedEvent +from cdevents.core.service import ServiceDeployedEvent, ServiceUpgradedEvent, ServiceRolledbackEvent, ServiceRemovedEvent +from cdevents.core.taskrun import TaskRunStartedEvent, TaskRunFinishedEvent + +class HttpHandlar(): + """Http Handlar.""" + + def get_attrs( + headers: typing.Dict[str, str], + data: typing.Union[str, bytes, None], + data_unmarshaller: types.UnmarshallerType = None, + ): + """ + Unwrap a CD_evnets (binary or structured) from an HTTP request. + :param headers: the HTTP headers + :type headers: typing.Dict[str, str] + :param data: the HTTP request body. If set to None, "" or b'', the returned + event's data field will be set to None + :type data: typing.IO + :param data_unmarshaller: Callable function to map data to a python object + e.g. lambda x: x or lambda x: json.loads(x) + :type data_unmarshaller: types.UnmarshallerType + """ + if data is None or data == b"": + # Empty string will cause data to be marshalled into None + data = "" + + if not isinstance(data, (str, bytes, bytearray)): + raise cloud_exceptions.InvalidStructuredJSON( + "Expected json of type (str, bytes, bytearray), " + f"but instead found type {type(data)}" + ) + + headers = {key.lower(): value for key, value in headers.items()} + if data_unmarshaller is None: + data_unmarshaller = _json_or_string + + marshall = marshaller.NewDefaultHTTPMarshaller() + + if is_binary(headers): + specversion = headers.get("ce-specversion", None) + else: + try: + raw_ce = json.loads(data) + except json.decoder.JSONDecodeError: + raise cloud_exceptions.MissingRequiredFields( + "Failed to read specversion from both headers and data. " + f"The following can not be parsed as json: {data}" + ) + if hasattr(raw_ce, "get"): + specversion = raw_ce.get("specversion", None) + else: + raise cloud_exceptions.MissingRequiredFields( + "Failed to read specversion from both headers and data. " + f"The following deserialized data has no 'get' method: {raw_ce}" + ) + + if specversion is None: + raise cloud_exceptions.MissingRequiredFields( + "Failed to find specversion in HTTP request" + ) + + event_handler = _obj_by_version.get(specversion, None) + + if event_handler is None: + raise cloud_exceptions.InvalidRequiredFields( + f"Found invalid specversion {specversion}" + ) + + event = marshall.FromRequest( + event_handler(), headers, data, data_unmarshaller=data_unmarshaller + ) + # if event.data == "" or event.data == b"": + # # TODO: Check binary unmarshallers to debug why setting data to "" + # # returns an event with data set to None, but structured will return "" + # data = None + attrs = event.Properties() + return attrs + + + def event_from_http(headers: typing.Dict[str, str], + data: typing.Union[str, bytes, None], + data_unmarshaller: types.UnmarshallerType = None + ): + attrs = HttpHandlar.get_attrs(headers, data, data_unmarshaller) + + event_data=attrs.pop("data", None) + + etype = EventType(attrs.get("type")) + if etype.value == "" or etype.value is None: + raise cloud_exceptions.MissingRequiredFields( + "Failed to find type in HTTP request" + ) + elif etype.value == EventType.ArtifactPackagedEventV1.value: + return ArtifactPackagedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.ArtifactPublishedEventV1.value: + return ArtifactPublishedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.BranchCreatedEventV1.value: + return BranchCreatedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.BranchDeletedEventV1.value: + return BranchDeletedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.BuildStartedEventV1.value: + return BuildStartedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.BuildQueuedEventV1.value: + return BuildQueuedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.BuildFinishedEventV1.value: + return BuildFinishedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.EnvironmentCreatedEventV1.value: + return EnvEventCreatedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.EnvironmentModifiedEventV1.value: + return EnvEventModifiedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.EnvironmentDeletedEventV1.value: + return EnvEventDeletedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.PipelineRunStartedEventV1.value: + return PipelinerunStartedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.PipelineRunFinishedEventV1.value: + return PipelinerunFinishedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.PipelineRunQueuedEventV1.value: + return PipelinerunQueuedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.RepositoryCreatedEventV1.value: + return RepositoryCreatedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.RepositoryModifiedEventV1.value: + return RepositoryModifiedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.RepositoryDeletedEventV1.value: + return RepositoryDeletedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.ServiceDeployedEventV1.value: + return ServiceDeployedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.ServiceUpgradedEventV1.value: + return ServiceUpgradedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.ServiceRolledbackEventV1.value: + return ServiceRolledbackEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.ServiceRemovedEventV1.value: + return ServiceRemovedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.TaskRunStartedEventV1.value: + return TaskRunStartedEvent(attrs=attrs, data=event_data) + elif etype.value == EventType.TaskRunFinishedEventV1.value: + return TaskRunFinishedEvent(attrs=attrs, data=event_data) diff --git a/core/cdevents/core/pipelinerun.py b/core/cdevents/core/pipelinerun.py new file mode 100644 index 0000000..def4f0a --- /dev/null +++ b/core/cdevents/core/pipelinerun.py @@ -0,0 +1,57 @@ +"""pipelinerun""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class PipelinerunEvent(Event): + """Pipelinerun Event.""" + + def __init__(self, pipelinerun_type: EventType, id: str, name: str, status: str, url: str, errors: str, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = pipelinerun_type + self._id = id + self._name = name + self._status = status + self._url = url + self._errors = errors + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "pipelinerunid": self._id, + "pipelinerunname": self._name, + "pipelinerunstatus": self._status, + "pipelinerunurl": self._url, + "pipelinerunerrors": self._errors, + } + return extensions + +class PipelinerunStartedEvent(PipelinerunEvent): + + def __init__(self, id: str=None, name: str=None, status: str=None, url: str=None, errors: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.PipelineRunStartedEventV1 + + super().__init__(pipelinerun_type=self._event_type, id=id, name=name, status=status, url=url, errors=errors, attrs=attrs, data=data) + +class PipelinerunFinishedEvent(PipelinerunEvent): + + def __init__(self, id: str=None, name: str=None, status: str=None, url: str=None, errors: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.PipelineRunFinishedEventV1 + + super().__init__(pipelinerun_type=self._event_type, id=id, name=name, status=status, url=url, errors=errors, attrs=attrs, data=data) + +class PipelinerunQueuedEvent(PipelinerunEvent): + + def __init__(self, id: str=None, name: str=None, status: str=None, url: str=None, errors: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.PipelineRunQueuedEventV1 + + super().__init__(pipelinerun_type=self._event_type, id=id, name=name, status=status, url=url, errors=errors, attrs=attrs, data=data) diff --git a/core/cdevents/core/repository.py b/core/cdevents/core/repository.py new file mode 100644 index 0000000..4c44396 --- /dev/null +++ b/core/cdevents/core/repository.py @@ -0,0 +1,54 @@ +"""repository""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class RepositoryEvent(Event): + """Repository Event.""" + + def __init__(self, repository_type: EventType, id: str, name: str, url: str, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = repository_type + self._id = id + self._name = name + self._url = url + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "repositoryid": self._id, + "repositoryname": self._name, + "repositoryurl": self._url, + } + return extensions + +class RepositoryCreatedEvent(RepositoryEvent): + + def __init__(self, id: str=None, name: str=None, url: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.RepositoryCreatedEventV1 + + super().__init__(repository_type=self._event_type, id=id, name=name, url=url, attrs=attrs, data=data) + +class RepositoryModifiedEvent(RepositoryEvent): + + def __init__(self, id: str=None, name: str=None, url: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.RepositoryModifiedEventV1 + + super().__init__(repository_type=self._event_type, id=id, name=name, url=url, attrs=attrs, data=data) + +class RepositoryDeletedEvent(RepositoryEvent): + + def __init__(self, id: str=None, name: str=None, url: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.RepositoryDeletedEventV1 + + super().__init__(repository_type=self._event_type, id=id, name=name, url=url, attrs=attrs, data=data) + diff --git a/core/cdevents/core/service.py b/core/cdevents/core/service.py new file mode 100644 index 0000000..19a9e1d --- /dev/null +++ b/core/cdevents/core/service.py @@ -0,0 +1,63 @@ +"""service""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class ServiceEvent(Event): + """Service Event.""" + + def __init__(self, service_type: EventType, envid: str, name: str, version: str, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = service_type + self._envid = envid + self._name = name + self._version = version + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "serviceenvid": self._envid, + "servicename": self._name, + "serviceversion": self._version, + } + return extensions + +class ServiceDeployedEvent(ServiceEvent): + + def __init__(self, envid: str=None, name: str=None, version: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.ServiceDeployedEventV1 + + super().__init__(service_type=self._event_type, envid=envid, name=name, version=version, attrs=attrs, data=data) + +class ServiceUpgradedEvent(ServiceEvent): + + def __init__(self, envid: str=None, name: str=None, version: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.ServiceUpgradedEventV1 + + super().__init__(service_type=self._event_type, envid=envid, name=name, version=version, attrs=attrs, data=data) + +class ServiceRolledbackEvent(ServiceEvent): + + def __init__(self, envid: str=None, name: str=None, version: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.ServiceRolledbackEventV1 + + super().__init__(service_type=self._event_type, envid=envid, name=name, version=version, attrs=attrs, data=data) + +class ServiceRemovedEvent(ServiceEvent): + + def __init__(self, envid: str=None, name: str=None, version: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.ServiceRemovedEventV1 + + super().__init__(service_type=self._event_type, envid=envid, name=name, version=version, attrs=attrs, data=data) + diff --git a/core/cdevents/core/taskrun.py b/core/cdevents/core/taskrun.py new file mode 100644 index 0000000..661d9be --- /dev/null +++ b/core/cdevents/core/taskrun.py @@ -0,0 +1,44 @@ +"""taskrun""" + +from cdevents.core.event import Event +from cdevents.core.event_type import EventType + +class TaskRunEvent(Event): + """Taskrun Event.""" + + def __init__(self,taskrun_type: EventType, id: str, name: str, pipelineid: str, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type = taskrun_type + self._id= id + self._name = name + self._pipelineid = pipelineid + super().__init__(event_type=self._event_type.value, extensions=self.create_extensions(), attrs=attrs, data=data) + + def create_extensions(self) -> dict: + """Create extensions. + """ + extensions = { + "taskrunid": self._id, + "taskrunname": self._name, + "taskrunpipelineid": self._pipelineid, + } + return extensions + +class TaskRunStartedEvent(TaskRunEvent): + + def __init__(self, id: str=None, name: str=None, pipelineid: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.TaskRunStartedEventV1 + + super().__init__(taskrun_type=self._event_type, id=id, name=name, pipelineid=pipelineid, attrs=attrs, data=data) + +class TaskRunFinishedEvent(TaskRunEvent): + + def __init__(self, id: str=None, name: str=None, pipelineid: str=None, attrs=None, data: dict = {}): + """Initializes class. + """ + self._event_type: str = EventType.TaskRunFinishedEventV1 + + super().__init__(taskrun_type=self._event_type, id=id, name=name, pipelineid=pipelineid, attrs=attrs, data=data) diff --git a/core/setup.cfg b/core/setup.cfg new file mode 100644 index 0000000..bac04f5 --- /dev/null +++ b/core/setup.cfg @@ -0,0 +1,59 @@ +[bumpversion] +current_version = 0.0.1 + +[bumpversion:file:./cdevents/core/__init__.py] + +[metadata] +name = cdevents.core +description = CDEvents Core +long_description = file: README.md +version = attr: cdevents.core.__version__ +author = doWhile +author_email = info@dowhile.com +keywords = CDEvents, core +url = todo +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: Other/Proprietary License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.9 + Topic :: Software Development :: Libraries :: Python Modules + +[options] +packages = find_namespace: +zip_safe = False +include_package_data = True +install_requires = + pytest + PyYAML + requests + cloudevents + +[options.extras_require] +dev = + black + bump2version + isort + mypy + pre-commit + pydocstyle + pytest + pytest_mock + yamllint + pylint + deepdiff +build = + wheel + +[options.packages.find] +include = + cdevents.* + +[bdist_wheel] +universal = 0 diff --git a/core/setup.py b/core/setup.py new file mode 100644 index 0000000..f3d9343 --- /dev/null +++ b/core/setup.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The setup script.""" + +__requires__ = "setuptools >= 44.0.0" + +from setuptools import setup + +setup() diff --git a/core/tests/__init__.py b/core/tests/__init__.py new file mode 100644 index 0000000..149f70c --- /dev/null +++ b/core/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for core package.""" diff --git a/core/tests/test_artifact.py b/core/tests/test_artifact.py new file mode 100644 index 0000000..60fadff --- /dev/null +++ b/core/tests/test_artifact.py @@ -0,0 +1,29 @@ +import pytest + +from cdevents.core.event_type import EventType +from cdevents.core.artifact import ArtifactEvent, ArtifactPackagedEvent, ArtifactPublishedEvent + +@pytest.mark.unit +def test_artifact_created(): + artifact_event = ArtifactEvent(artifact_type=EventType.ArtifactPackagedEventV1, id="_id", name="_name", version="_version",data={"key1": "value1"}) + assert artifact_event is not None + assert artifact_event._attributes["type"] == EventType.ArtifactPackagedEventV1.value + assert artifact_event._attributes["extensions"] == {"artifactid": "_id", "artifactname": "_name", "artifactversion": "_version"} + assert artifact_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_artifact_type_packaged_v1(): + artifact_event = ArtifactPackagedEvent(id="_id", name="_name", version="_version",data={"key1": "value1"}) + assert artifact_event is not None + assert artifact_event._attributes["type"] == EventType.ArtifactPackagedEventV1.value + assert artifact_event._attributes["extensions"] == {"artifactid": "_id", "artifactname": "_name", "artifactversion": "_version"} + assert artifact_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_artifact_type_published_v1(): + artifact_event = ArtifactPublishedEvent(id="_id", name="_name", version="_version",data={"key1": "value1"}) + assert artifact_event is not None + assert artifact_event._attributes["type"] == EventType.ArtifactPublishedEventV1.value + assert artifact_event._attributes["extensions"] == {"artifactid": "_id", "artifactname": "_name", "artifactversion": "_version"} + assert artifact_event.data == {"key1": "value1"} + diff --git a/core/tests/test_branch.py b/core/tests/test_branch.py new file mode 100644 index 0000000..c90313a --- /dev/null +++ b/core/tests/test_branch.py @@ -0,0 +1,28 @@ +import pytest + +from cdevents.core.event_type import EventType +from cdevents.core.branch import BranchEvent, BranchCreatedEvent, BranchDeletedEvent + +@pytest.mark.unit +def test_branch_created(): + branch_event = BranchEvent(branch_type=EventType.BranchCreatedEventV1, id="_id", name="_name", repoid="_repoid", data={"key1": "value1"}) + assert branch_event is not None + assert branch_event._attributes["type"] == EventType.BranchCreatedEventV1.value + assert branch_event._attributes["extensions"] == {"branchid": "_id", "branchname": "_name", "branchrepositoryid": "_repoid"} + assert branch_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_branch_type_created_v1(): + branch_event = BranchCreatedEvent(id="_id", name="_name", repoid="_repoid", data={"key1": "value1"}) + assert branch_event is not None + assert branch_event._attributes["type"] == EventType.BranchCreatedEventV1.value + assert branch_event._attributes["extensions"] == {"branchid": "_id", "branchname": "_name", "branchrepositoryid": "_repoid"} + assert branch_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_branch_type_deleted_v1(): + branch_event = BranchDeletedEvent(id="_id", name="_name", repoid="_repoid", data={"key1": "value1"}) + assert branch_event is not None + assert branch_event._attributes["type"] == EventType.BranchDeletedEventV1.value + assert branch_event._attributes["extensions"] == {"branchid": "_id", "branchname": "_name", "branchrepositoryid": "_repoid"} + assert branch_event.data == {"key1": "value1"} \ No newline at end of file diff --git a/core/tests/test_build.py b/core/tests/test_build.py new file mode 100644 index 0000000..0b4415c --- /dev/null +++ b/core/tests/test_build.py @@ -0,0 +1,36 @@ +import pytest + +from cdevents.core.event_type import EventType +from cdevents.core.build import BuildEvent, BuildStartedEvent, BuildQueuedEvent, BuildFinishedEvent + +@pytest.mark.unit +def test_build_created(): + build_event = BuildEvent(build_type=EventType.BuildStartedEventV1, id="_id", name="_name", artifact="_artifact", data={"key1": "value1"}) + assert build_event is not None + assert build_event._attributes["type"] == EventType.BuildStartedEventV1.value + assert build_event._attributes["extensions"] == {"buildid": "_id", "buildname": "_name", "buildartifactid": "_artifact"} + assert build_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_build_type_started_v1(): + build_event = BuildStartedEvent(id="_id", name="_name", artifact="_artifact", data={"key1": "value1"}) + assert build_event is not None + assert build_event._attributes["type"] == EventType.BuildStartedEventV1.value + assert build_event._attributes["extensions"] == {"buildid": "_id", "buildname": "_name", "buildartifactid": "_artifact"} + assert build_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_build_type_queued_v1(): + build_event = BuildQueuedEvent(id="_id", name="_name", artifact="_artifact", data={"key1": "value1"}) + assert build_event is not None + assert build_event._attributes["type"] == EventType.BuildQueuedEventV1.value + assert build_event._attributes["extensions"] == {"buildid": "_id", "buildname": "_name", "buildartifactid": "_artifact"} + assert build_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_build_type_finished_v1(): + build_event = BuildFinishedEvent(id="_id", name="_name", artifact="_artifact", data={"key1": "value1"}) + assert build_event is not None + assert build_event._attributes["type"] == EventType.BuildFinishedEventV1.value + assert build_event._attributes["extensions"] == {"buildid": "_id", "buildname": "_name", "buildartifactid": "_artifact"} + assert build_event.data == {"key1": "value1"} diff --git a/core/tests/test_environment.py b/core/tests/test_environment.py new file mode 100644 index 0000000..bb73e93 --- /dev/null +++ b/core/tests/test_environment.py @@ -0,0 +1,38 @@ +import pytest + +from cdevents.core.env import EnvEvent, EnvEventCreatedEvent, EnvEventModifiedEvent, EnvEventDeletedEvent +from cdevents.core.event_type import EventType + +@pytest.mark.unit +def test_environment_created(): + env_event = EnvEvent(env_type=EventType.EnvironmentCreatedEventV1, id="_id", name="_name", repo="_repo", data={"key1": "value1"}) + assert env_event is not None + assert env_event._attributes["type"] == EventType.EnvironmentCreatedEventV1.value + assert env_event._attributes["extensions"] == {"envId": "_id", "envname": "_name", "envrepourl": "_repo"} + assert env_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_environment_type_created_v1(): + env_event = EnvEventCreatedEvent(id="_id", name="_name", repo="_repo", data={"key1": "value1"}) + assert env_event is not None + assert env_event._attributes["type"] == EventType.EnvironmentCreatedEventV1.value + assert env_event._attributes["extensions"] == {"envId": "_id", "envname": "_name", "envrepourl": "_repo"} + assert env_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_environment_type_modified_v1(): + env_event = EnvEventModifiedEvent(id="_id", name="_name", repo="_repo", data={"key1": "value1"}) + assert env_event is not None + assert env_event._attributes["type"] == EventType.EnvironmentModifiedEventV1.value + assert env_event._attributes["extensions"] == {"envId": "_id", "envname": "_name", "envrepourl": "_repo"} + assert env_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_environment_type_deleted_v1(): + env_event = EnvEventDeletedEvent(id="_id", name="_name", repo="_repo", data={"key1": "value1"}) + assert env_event is not None + assert env_event._attributes["type"] == EventType.EnvironmentDeletedEventV1.value + assert env_event._attributes["extensions"] == {"envId": "_id", "envname": "_name", "envrepourl": "_repo"} + assert env_event.data == {"key1": "value1"} \ No newline at end of file diff --git a/core/tests/test_events.py b/core/tests/test_events.py new file mode 100644 index 0000000..b37d1eb --- /dev/null +++ b/core/tests/test_events.py @@ -0,0 +1,11 @@ +import pytest + +from cdevents.core.event import Event + +@pytest.mark.unit +def test_create_event(): + event = Event(event_type="event_type", extensions={"test": "test"}, data={"key1": "value1"}) + assert event is not None + assert event._attributes["type"] == "event_type" + assert event._attributes["extensions"] == {"test": "test"} + assert event.data == {"key1": "value1"} diff --git a/core/tests/test_pipelinerun.py b/core/tests/test_pipelinerun.py new file mode 100644 index 0000000..54eb8a1 --- /dev/null +++ b/core/tests/test_pipelinerun.py @@ -0,0 +1,36 @@ +import pytest + +from cdevents.core.pipelinerun import PipelinerunEvent, PipelinerunStartedEvent, PipelinerunFinishedEvent, PipelinerunQueuedEvent +from cdevents.core.event_type import EventType + +@pytest.mark.unit +def test_pipelinerun_created(): + pipelinerun_event = PipelinerunEvent(pipelinerun_type=EventType.PipelineRunStartedEventV1, id="_id", name="_name", status="_status", url="_url", errors="_errors", data={"key1": "value1"}) + assert pipelinerun_event is not None + assert pipelinerun_event._attributes["type"] == EventType.PipelineRunStartedEventV1.value + assert pipelinerun_event._attributes["extensions"] == {"pipelinerunid": "_id", "pipelinerunname": "_name", "pipelinerunstatus": "_status", "pipelinerunurl": "_url", "pipelinerunerrors": "_errors"} + assert pipelinerun_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_pipelinerun_type_started_v1(): + pipelinerun_event = PipelinerunStartedEvent(id="_id", name="_name", status="_status", url="_url", errors="_errors", data={"key1": "value1"}) + assert pipelinerun_event is not None + assert pipelinerun_event._attributes["type"] == EventType.PipelineRunStartedEventV1.value + assert pipelinerun_event._attributes["extensions"] == {"pipelinerunid": "_id", "pipelinerunname": "_name", "pipelinerunstatus": "_status", "pipelinerunurl": "_url", "pipelinerunerrors": "_errors"} + assert pipelinerun_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_pipelinerun_type_finished_v1(): + pipelinerun_event = PipelinerunFinishedEvent( id="_id", name="_name", status="_status", url="_url", errors="_errors", data={"key1": "value1"}) + assert pipelinerun_event is not None + assert pipelinerun_event._attributes["type"] == EventType.PipelineRunFinishedEventV1.value + assert pipelinerun_event._attributes["extensions"] == {"pipelinerunid": "_id", "pipelinerunname": "_name", "pipelinerunstatus": "_status", "pipelinerunurl": "_url", "pipelinerunerrors": "_errors"} + assert pipelinerun_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_pipelinerun_type_queued_v1(): + pipelinerun_event = PipelinerunQueuedEvent(id="_id", name="_name", status="_status", url="_url", errors="_errors", data={"key1": "value1"}) + assert pipelinerun_event is not None + assert pipelinerun_event._attributes["type"] == EventType.PipelineRunQueuedEventV1.value + assert pipelinerun_event._attributes["extensions"] == {"pipelinerunid": "_id", "pipelinerunname": "_name", "pipelinerunstatus": "_status", "pipelinerunurl": "_url", "pipelinerunerrors": "_errors"} + assert pipelinerun_event.data == {"key1": "value1"} diff --git a/core/tests/test_repository.py b/core/tests/test_repository.py new file mode 100644 index 0000000..d8597ee --- /dev/null +++ b/core/tests/test_repository.py @@ -0,0 +1,37 @@ +import pytest + +from cdevents.core.repository import RepositoryEvent, RepositoryCreatedEvent, RepositoryModifiedEvent, RepositoryDeletedEvent +from cdevents.core.event_type import EventType + +@pytest.mark.unit +def test_repository_created(): + repository_event = RepositoryEvent(repository_type=EventType.RepositoryCreatedEventV1, id="_id", name="_name", url="_url", data={"key1": "value1"}) + assert repository_event is not None + assert repository_event._attributes["type"] == EventType.RepositoryCreatedEventV1.value + assert repository_event._attributes["extensions"] == {"repositoryid": "_id", "repositoryname": "_name", "repositoryurl": "_url"} + assert repository_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_repository_type_created_v1(): + repository_event = RepositoryCreatedEvent(id="_id", name="_name", url="_url", data={"key1": "value1"}) + assert repository_event is not None + assert repository_event._attributes["type"] == EventType.RepositoryCreatedEventV1.value + assert repository_event._attributes["extensions"] == {"repositoryid": "_id", "repositoryname": "_name", "repositoryurl": "_url"} + assert repository_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_repository_type_modified_v1(): + repository_event = RepositoryModifiedEvent(id="_id", name="_name", url="_url", data={"key1": "value1"}) + assert repository_event is not None + assert repository_event._attributes["type"] == EventType.RepositoryModifiedEventV1.value + assert repository_event._attributes["extensions"] == {"repositoryid": "_id", "repositoryname": "_name", "repositoryurl": "_url"} + assert repository_event.data == {"key1": "value1"} + +@pytest.mark.unit +def test_repository_type_deleted_v1(): + repository_event = RepositoryDeletedEvent(id="_id", name="_name", url="_url", data={"key1": "value1"}) + assert repository_event is not None + assert repository_event._attributes["type"] == EventType.RepositoryDeletedEventV1.value + assert repository_event._attributes["extensions"] == {"repositoryid": "_id", "repositoryname": "_name", "repositoryurl": "_url"} + assert repository_event.data == {"key1": "value1"} + diff --git a/core/tests/test_service.py b/core/tests/test_service.py new file mode 100644 index 0000000..7830f34 --- /dev/null +++ b/core/tests/test_service.py @@ -0,0 +1,49 @@ +import pytest + +from cdevents.core.service import ServiceEvent, ServiceDeployedEvent, ServiceUpgradedEvent, ServiceRolledbackEvent, ServiceRemovedEvent +from cdevents.core.event_type import EventType + +@pytest.mark.unit +def test_service_created(): + service_event = ServiceEvent(service_type=EventType.ServiceDeployedEventV1, envid="_envid", name="_name", version="_version", data={"key1": "value1"}) + assert service_event is not None + assert service_event._attributes["type"] == EventType.ServiceDeployedEventV1.value + assert service_event._attributes["extensions"] == {"serviceenvid": "_envid", "servicename": "_name", "serviceversion": "_version"} + assert service_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_service_type_deployed_v1(): + service_event = ServiceDeployedEvent(envid="_envid", name="_name", version="_version", data={"key1": "value1"}) + assert service_event is not None + assert service_event._attributes["type"] == EventType.ServiceDeployedEventV1.value + assert service_event._attributes["extensions"] == {"serviceenvid": "_envid", "servicename": "_name", "serviceversion": "_version"} + assert service_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_service_type_upgraded_v1(): + service_event = ServiceUpgradedEvent(envid="_envid", name="_name", version="_version", data={"key1": "value1"}) + assert service_event is not None + assert service_event._attributes["type"] == EventType.ServiceUpgradedEventV1.value + assert service_event._attributes["extensions"] == {"serviceenvid": "_envid", "servicename": "_name", "serviceversion": "_version"} + assert service_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_service_type_rolledback_v1(): + service_event = ServiceRolledbackEvent(envid="_envid", name="_name", version="_version", data={"key1": "value1"}) + assert service_event is not None + assert service_event._attributes["type"] == EventType.ServiceRolledbackEventV1.value + assert service_event._attributes["extensions"] == {"serviceenvid": "_envid", "servicename": "_name", "serviceversion": "_version"} + assert service_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_service_type_removed_v1(): + service_event = ServiceRemovedEvent(envid="_envid", name="_name", version="_version", data={"key1": "value1"}) + assert service_event is not None + assert service_event._attributes["type"] == EventType.ServiceRemovedEventV1.value + assert service_event._attributes["extensions"] == {"serviceenvid": "_envid", "servicename": "_name", "serviceversion": "_version"} + assert service_event.data == {"key1": "value1"} + diff --git a/core/tests/test_taskrun.py b/core/tests/test_taskrun.py new file mode 100644 index 0000000..7059261 --- /dev/null +++ b/core/tests/test_taskrun.py @@ -0,0 +1,30 @@ +import pytest + +from cdevents.core.taskrun import TaskRunEvent, TaskRunStartedEvent, TaskRunFinishedEvent +from cdevents.core.event_type import EventType + +@pytest.mark.unit +def test_taskrun_created(): + taskrun_event = TaskRunEvent(taskrun_type=EventType.TaskRunStartedEventV1, id="_id", name="_name", pipelineid="_pipelineid", data={"key1": "value1"}) + assert taskrun_event is not None + assert taskrun_event._attributes["type"] == EventType.TaskRunStartedEventV1.value + assert taskrun_event._attributes["extensions"] == {"taskrunid": "_id", "taskrunname": "_name", "taskrunpipelineid": "_pipelineid"} + assert taskrun_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_taskrun_type_started_v1(): + taskrun_event = TaskRunStartedEvent(id="_id", name="_name", pipelineid="_pipelineid", data={"key1": "value1"}) + assert taskrun_event is not None + assert taskrun_event._attributes["type"] == EventType.TaskRunStartedEventV1.value + assert taskrun_event._attributes["extensions"] == {"taskrunid": "_id", "taskrunname": "_name", "taskrunpipelineid": "_pipelineid"} + assert taskrun_event.data == {"key1": "value1"} + + +@pytest.mark.unit +def test_taskrun_type_finished_v1(): + taskrun_event = TaskRunFinishedEvent(id="_id", name="_name", pipelineid="_pipelineid", data={"key1": "value1"}) + assert taskrun_event is not None + assert taskrun_event._attributes["type"] == EventType.TaskRunFinishedEventV1.value + assert taskrun_event._attributes["extensions"] == {"taskrunid": "_id", "taskrunname": "_name", "taskrunpipelineid": "_pipelineid"} + assert taskrun_event.data == {"key1": "value1"} diff --git a/core/tests/test_version.py b/core/tests/test_version.py new file mode 100644 index 0000000..104f763 --- /dev/null +++ b/core/tests/test_version.py @@ -0,0 +1,16 @@ +import re + +import pytest + +from cdevents.core import __version__ + +# From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +VALID_SEMVER_REGEX = ( + r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" + r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" +) + + +@pytest.mark.unit +def test_version_is_semantic(): + assert re.fullmatch(VALID_SEMVER_REGEX, __version__) diff --git a/header.mk b/header.mk new file mode 100644 index 0000000..0adf6dd --- /dev/null +++ b/header.mk @@ -0,0 +1,27 @@ +.PHONY: clean clean-test clean-pyc clean-build help +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +try: + from urllib import pathname2url +except: + from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0739683 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +testpaths = + cli/tests + core/tests +markers = + unit: mark for unit tests + integration: integration tests diff --git a/receiver/cdevents/api/app.py b/receiver/cdevents/api/app.py new file mode 100644 index 0000000..afad282 --- /dev/null +++ b/receiver/cdevents/api/app.py @@ -0,0 +1,25 @@ +from flask import Flask, request + +from cdevents.core.http_handlar import HttpHandlar + +app = Flask(__name__) + + +# create an endpoint at http://localhost:/8080/ +@app.route("/", methods=["POST"]) +def home(): + # create a CloudEvent + event = HttpHandlar.event_from_http(headers=request.headers, data=request.get_data()) + + # you can access cloudevent fields as seen below + print( + f"Found {event['id']} from {event['source']} with type " + f"{event['type']} and specversion {event['specversion']}" + ) + print() + print(event) + return "", 204 + + +if __name__ == "__main__": + app.run(port=8080) \ No newline at end of file diff --git a/targets.mk b/targets.mk new file mode 100644 index 0000000..fbd6617 --- /dev/null +++ b/targets.mk @@ -0,0 +1,65 @@ +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test clean-mypy ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + # N.B. line below removes editable intallation of package in venv + # find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + +clean-mypy: ## remove MyPy cache files + rm -fr .mypy_cache/ + +package-install: ## install the package without dev dependencies + pip install -e . + +test: ## run tests quickly with the default Python + python -m pytest -m unit -vv + +dist: clean ## builds source and wheel package + python setup.py sdist + python setup.py bdist_wheel + ls -l dist + +format: ## format all Python code using black + python -m black --line-length=100 . + +.ONESHELL: +lint: ## run pylint + python -m pylint $(SRC) --rcfile=$(ABS_ROOT_PATH)/.pylintrc + exit_code=$$? + python -m pylint \ + --disable duplicate-code \ + --disable missing-module-docstring \ + --disable missing-function-docstring \ + tests --rcfile=$(ABS_ROOT_PATH)/.pylintrc + exit_code=`expr $${exit_code} + $$?` + if [ "$${exit_code}" != 0 ]; then + exit "$${exit_code}" + fi + +bump = patch +bumpversion: ## Bumps the (default: patch) version of this package. To bump minor or major, add bump=minor or bump=major to the make call. + bumpversion --allow-dirty $(bump) + - pre-commit run trailing-whitespace --file setup.cfg + - pre-commit run remove-tabs --file setup.cfg + + +.ONESHELL: +pre-commit: + pre-commit run --all-files