diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index fe8b17f0..2aff6c20 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -4,6 +4,8 @@ on:
push:
branches:
- 'main'
+ tags-ignore:
+ - '**'
jobs:
ci:
@@ -46,7 +48,7 @@ jobs:
- name: Documentation coverage with interrogate
run: |
- interrogate -vv bobocep --fail-under 98
+ interrogate -vv bobocep --fail-under 100
- name: Upload code coverage to Code Climate
uses: paambaati/codeclimate-action@v2.5.3
@@ -87,7 +89,6 @@ jobs:
--outdir dist/
- name: Publish to PyPI
- # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index acf90d30..3c6d180a 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -4,6 +4,8 @@ on:
push:
branches:
- 'main'
+ tags-ignore:
+ - '**'
schedule:
# At 06:00 on Monday.
- cron: '0 6 * * 1'
diff --git a/README.md b/README.md
index f5c4ed30..b3df3324 100755
--- a/README.md
+++ b/README.md
@@ -4,10 +4,10 @@
https://github.com/r3w0p/bobocep/actions/workflows/cicd.yml)
[![Security](https://github.com/r3w0p/bobocep/actions/workflows/security.yml/badge.svg)](
https://github.com/r3w0p/bobocep/actions/workflows/security.yml)
+[![Code Repository](https://img.shields.io/badge/code-github-171515)](
+https://github.com/r3w0p/bobocep/)
[![Documentation Status](https://readthedocs.org/projects/bobocep/badge/?version=latest)](
https://bobocep.readthedocs.io/)
-[![License Scan](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep.svg?type=shield)](
-https://app.fossa.com/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep?ref=badge_shield)
[![Donate](https://img.shields.io/badge/donate-ko--fi-red?label=donate)](
https://ko-fi.com/r3w0p)
@@ -24,12 +24,12 @@ https://github.com/r3w0p/bobocep/pulse/)
[![Issues](https://img.shields.io/github/issues/r3w0p/bobocep?label=issues)](
https://github.com/r3w0p/bobocep/issues/)
-[![Dependencies](https://img.shields.io/librariesio/github/r3w0p/bobocep?label=dependencies)](
-https://libraries.io/pypi/bobocep/)
[![Coverage](https://img.shields.io/codeclimate/coverage/r3w0p/bobocep?label=coverage)](
https://codeclimate.com/github/r3w0p/bobocep/)
[![Maintainability](https://img.shields.io/codeclimate/maintainability/r3w0p/bobocep?label=maintainability)](
https://codeclimate.com/github/r3w0p/bobocep/)
+[![License Scan](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep.svg?type=shield)](
+https://app.fossa.com/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep?ref=badge_shield)
`BoboCEP` is a [Complex Event Processing](https://en.wikipedia.org/wiki/Complex_event_processing) (CEP) engine
@@ -43,7 +43,7 @@ partially-completed complex events across multiple instances of the software.
## License
-`BoboCEP` is open source, as per
-[The Open Source Definition](https://opensource.org/osd).
+`BoboCEP` is open source, as per the
+[Open Source Definition](https://opensource.org/osd).
The code in this repository can be redistributed and/or modified under the terms of the
[MIT License](https://github.com/r3w0p/bobocep/blob/main/LICENSE).
diff --git a/SECURITY.md b/SECURITY.md
index 99eb61b8..ea84f009 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,6 +1,6 @@
# Security Policy
-> _Last updated: 2023-01-04_
+> _Last updated: 2023-09-16_
## Supported Versions
@@ -10,26 +10,27 @@ Below are the versions that are tested for support.
### Python
-| Version | Supported |
-|:------------:|:---------:|
-| `> 3.10` | ✗ |
-| `3.9 - 3.10` | ✓ |
-| `< 3.9` | ✗ |
+| Version | Tested |
+|:------------:|:-------:|
+| `> 3.10` | ✗ |
+| `3.9 - 3.10` | ✓ |
+| `< 3.9` | ✗ |
### OS
-| Version | Supported |
-|:----------------:|:---------:|
-| `windows-latest` | ✓ |
-| `macos-latest` | ✓ |
-| `ubuntu-latest` | ✓ |
+| Version | Tested |
+|:----------------:|:-------:|
+| `windows-latest` | ✓ |
+| `macos-latest` | ✓ |
+| `ubuntu-latest` | ✓ |
The `*-latest` version refers to the `*-latest`
-[available environments](https://github.com/actions/virtual-environments#available-environments)
+[available environments](https://github.com/actions/runner-images)
from GitHub Actions Virtual Environments.
## Reporting a Vulnerability
-Please report vulnerabilities using the contact information [here](https://r3w0p.github.io/contact/).
+Please report vulnerabilities using the contact information
+[here](https://r3w0p.github.io/contact/).
diff --git a/bobocep/__init__.py b/bobocep/__init__.py
index 1a4541ab..42b92383 100644
--- a/bobocep/__init__.py
+++ b/bobocep/__init__.py
@@ -8,6 +8,6 @@
__author__ = """r3w0p"""
__email__ = "rr33ww00pp@gmail.com"
-__version__ = "1.0.1"
+__version__ = "1.1.0"
-from bobocep.bobocep import BoboError
+from bobocep.bobocep import BoboError, BoboJSONable, BoboJSONableError
diff --git a/bobocep/bobocep.py b/bobocep/bobocep.py
index 57b9f671..de2276ed 100644
--- a/bobocep/bobocep.py
+++ b/bobocep/bobocep.py
@@ -5,6 +5,7 @@
"""
Core classes.
"""
+
from abc import ABC, abstractmethod
@@ -22,7 +23,7 @@ class BoboJSONableError(BoboError):
class BoboJSONable(ABC):
"""
- A abstract interface for JSONable types.
+ An abstract interface for JSONable types.
"""
@abstractmethod
diff --git a/bobocep/cep/action/common/__init__.py b/bobocep/cep/action/common/__init__.py
new file mode 100644
index 00000000..f09d45db
--- /dev/null
+++ b/bobocep/cep/action/common/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
+"""
+Common actions.
+"""
diff --git a/bobocep/cep/action/common/multi.py b/bobocep/cep/action/common/multi.py
new file mode 100644
index 00000000..b77bb4db
--- /dev/null
+++ b/bobocep/cep/action/common/multi.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
+"""
+Multi actions.
+"""
+from abc import ABC
+from typing import Tuple, Any, List
+
+from bobocep.cep.action import BoboAction, BoboActionError
+from bobocep.cep.event import BoboEventComplex
+
+_ACTIONS_MIN: int = 1
+
+
+class BoboActionMulti(BoboAction, ABC):
+ """
+ An abstract multi action.
+ """
+
+ def __init__(self, name: str, *args, **kwargs):
+ """
+ :param name: The action name.
+ :param args: Action arguments.
+ :param kwargs: Action keyword arguments.
+ """
+ super().__init__(name=name, args=args, kwargs=kwargs)
+
+
+class BoboActionMultiSequential(BoboActionMulti):
+ """
+ An abstract sequential multi action.
+ """
+
+ def __init__(self,
+ name: str,
+ actions: List[BoboAction],
+ stop_on_fail: bool,
+ *args,
+ **kwargs):
+ """
+ :param name: The action name.
+ :param actions: The list of actions to execute.
+ :param stop_on_fail: If True, the multi-action stops processing its
+ action list if its current action fails. If False, it continues
+ to process its remaining actions. Note: failure of any action in
+ its list will cause the multi-action's success to be False.
+ :param args: Action arguments.
+ :param kwargs: Action keyword arguments.
+ """
+ super().__init__(name=name, args=args, kwargs=kwargs)
+
+ if len(actions) < 1:
+ raise BoboActionError(
+ f"multi sequential action {name} "
+ f"must contain at least {_ACTIONS_MIN} action")
+
+ self._actions: List[BoboAction] = actions
+ self._stop_on_fail: bool = stop_on_fail
+
+ def execute(self, event: BoboEventComplex) \
+ -> Tuple[bool, List[Tuple[bool, Any]]]:
+ """
+ :param event: The complex event that triggered action.
+ :return: Whether the action execution was successful, and
+ any additional data.
+ """
+ success = True
+ data: List[Tuple[bool, Any]] = []
+
+ for action in self._actions:
+ output: Tuple[bool, Any] = action.execute(event)
+ data.append(output)
+
+ # If action was unsuccessful
+ if not output[0]:
+ success = False
+
+ if self._stop_on_fail:
+ break
+
+ return success, data
diff --git a/bobocep/cep/action/handler.py b/bobocep/cep/action/handler.py
index d7ec5fb7..138bd51b 100644
--- a/bobocep/cep/action/handler.py
+++ b/bobocep/cep/action/handler.py
@@ -5,8 +5,8 @@
"""
Handlers that coordinate the execution of actions.
"""
+
import logging
-import multiprocessing
from abc import ABC, abstractmethod
from queue import Queue
from threading import RLock
diff --git a/bobocep/cep/engine/engine.py b/bobocep/cep/engine/engine.py
index 33effb62..e2931c3a 100644
--- a/bobocep/cep/engine/engine.py
+++ b/bobocep/cep/engine/engine.py
@@ -160,8 +160,10 @@ def run(self) -> None:
def update(self) -> bool:
"""
- Performs a single update from the receiver, to the decider, to the
- producer, and finally to the forwarder.
+ Updates the receiver, then the decider, then the producer, and
+ finally to the forwarder.
+ It updates each task `n` times, depending on how many times were
+ chosen during engine instantiation.
:return: `True` if engine is not set to close; `False` otherwise.
"""
diff --git a/bobocep/cep/engine/receiver/validator.py b/bobocep/cep/engine/receiver/validator.py
index cafa9b9a..039703e7 100644
--- a/bobocep/cep/engine/receiver/validator.py
+++ b/bobocep/cep/engine/receiver/validator.py
@@ -10,9 +10,19 @@
from json import dumps
from typing import Any, List, Tuple
+from jsonschema import validate as jsonschema_validate # type: ignore
+from jsonschema.exceptions import ValidationError, SchemaError # type: ignore
+
+from bobocep import BoboError
from bobocep.cep.event import BoboEvent
+class BoboValidatorError(BoboError):
+ """
+ A validator error.
+ """
+
+
class BoboValidator(ABC):
"""An abstract validator."""
@@ -89,3 +99,37 @@ def is_valid(self, data: Any) -> bool:
return any(isinstance(data, t) for t in self._types)
else:
return any(type(data) == t for t in self._types)
+
+
+class BoboValidatorJSONSchema(BoboValidatorJSONable):
+ """
+ Validates whether the data type is valid with respect to
+ a given JSON Schema. If the data are a BoboEvent,
+ then the event's data are checked instead.
+ """
+
+ def __init__(self, schema: dict):
+ """
+ :param schema: The JSON schema against which to compare data.
+ """
+ super().__init__()
+
+ self._schema: dict = schema
+
+ def is_valid(self, data: Any) -> bool:
+ """
+ :return: `True` if data are valid as per the JSON schema;
+ `False` otherwise.
+
+ :raises: BoboValidatorError: Invalid JSON schema.
+ """
+ try:
+ jsonschema_validate(instance=data, schema=self._schema)
+
+ except ValidationError:
+ return False
+
+ except SchemaError as e:
+ raise BoboValidatorError(e)
+
+ return True
diff --git a/bobocep/cep/event/event.py b/bobocep/cep/event/event.py
index 3f7616ed..8a77da25 100644
--- a/bobocep/cep/event/event.py
+++ b/bobocep/cep/event/event.py
@@ -9,8 +9,7 @@
from abc import ABC, abstractmethod
from typing import Any
-from bobocep import BoboError
-from bobocep.bobocep import BoboJSONable
+from bobocep import BoboError, BoboJSONable
_EXC_ID_LEN = "event ID must have a length greater than 0"
diff --git a/bobocep/cep/phenom/phenom.py b/bobocep/cep/phenom/phenom.py
index 7b38a5db..ded9cae8 100644
--- a/bobocep/cep/phenom/phenom.py
+++ b/bobocep/cep/phenom/phenom.py
@@ -67,6 +67,7 @@ def __init__(self,
self._patterns: Tuple[BoboPattern, ...] = tuple(patterns)
self._datagen: Optional[Callable] = datagen
self._action: Optional[BoboAction] = action
+ self._retain: bool = retain
@property
def name(self) -> str:
@@ -95,3 +96,10 @@ def action(self) -> Optional[BoboAction]:
:return: Phenomenon action, or `None`.
"""
return self._action
+
+ @property
+ def retain(self) -> bool:
+ """
+ :return: True if retains datagen callable; False otherwise.
+ """
+ return self._retain
diff --git a/bobocep/dist/pubsub.py b/bobocep/dist/pubsub.py
index 95dc4b35..da58c89d 100644
--- a/bobocep/dist/pubsub.py
+++ b/bobocep/dist/pubsub.py
@@ -1,6 +1,7 @@
# Copyright (c) 2019-2023 r3w0p
# The following code can be redistributed and/or
# modified under the terms of the MIT License.
+
"""
Distributed publish-subscribe classes.
"""
diff --git a/docs/conf.py b/docs/conf.py
index aca1bea5..9c63146d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,7 +16,7 @@
project = "BoboCEP"
copyright = "2019-2023 r3w0p"
author = "r3w0p"
-version = "1.0.1"
+version = "1.1.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
diff --git a/docs/developer_guide.rst b/docs/developer_guide.rst
new file mode 100644
index 00000000..f3d5cda3
--- /dev/null
+++ b/docs/developer_guide.rst
@@ -0,0 +1,128 @@
+Developer Guide
+***************
+
+Dependencies
+============
+
+You will need to install the core :code:`BoboCEP` requirements from both
+:code:`requirements.txt` and its additional development requirements from
+:code:`requirements-dev.txt`.
+For example:
+
+.. code:: console
+
+ pip install -r requirements.txt
+ pip install -r requirements-dev.txt
+
+
+Development Tools
+=================
+
+:code:`BoboCEP` uses GitHub Actions for *Continuous Integration* (CI) and
+*Continuous Deployment* (CD).
+It uses two YAML scripts to trigger the respective action workflows, namely:
+
+1. :code:`.github/workflows/cicd.yml` for CI/CD tasks, including:
+ linting, type checking, code coverage, documentation coverage, and
+ deployment to PyPI.
+2. :code:`.github/workflows/security.yml` for security checks.
+
+These scripts are triggered on a push to the :code:`main` branch.
+The security script also runs periodically.
+
+It is recommended that you run the individual CI/CD tasks manually before
+committing.
+These are discussed next.
+
+
+Code Linting
+------------
+
+:code:`flake8` is used for code linting.
+Run the following two commands to lint :code:`BoboCEP` and its test suite,
+respectively.
+
+.. code::
+
+ flake8 ./bobocep --count --select=E9,F63,F7,F82 --show-source --statistics
+ flake8 ./tests --count --select=E9,F63,F7,F82 --show-source --statistics
+
+
+Code Testing and Coverage
+-------------------------
+
+:code:`coverage` is used for code testing and coverage.
+Results are uploaded to
+`Code Climate `_.
+Run the following command to test :code:`BoboCEP`.
+
+.. code::
+
+ coverage run --source=bobocep -m pytest tests
+
+GitHub Actions additionally enforces a minimum coverage of 98%.
+You can check that this requirement has been satisfied using the following.
+
+.. code::
+
+ coverage report --fail-under=98
+
+You can locally inspect the code coverage with an HTML output by running
+the following.
+
+.. code::
+
+ coverage html
+
+
+Documentation
+-------------
+
+Documentation is built using :code:`sphinx` and is deployed via
+`Read the Docs `_.
+You can compile documentation locally via the following.
+
+.. code::
+
+ cd docs
+ make html
+
+Or, for Windows (PowerShell).
+
+.. code::
+
+ cd docs
+ .\make.bat html
+
+
+Documentation Coverage
+----------------------
+
+:code:`interrogate` is used for testing and code coverage.
+Run the following command to check :code:`BoboCEP` documentation coverage.
+It requires a minimum documentation coverage of 100%.
+
+.. code::
+
+ interrogate -vv bobocep --fail-under 100
+
+
+Type Checking
+-------------
+
+:code:`mypy` is used for type checking.
+Run the following two commands to check :code:`BoboCEP` and its test suite,
+respectively.
+
+.. code::
+
+ mypy ./bobocep
+ mypy ./tests
+
+
+Versioning
+----------
+
+:code:`BoboCEP` uses `Semantic Versioning `_ and
+the :code:`bump2version` tool for editing the software version.
+See `here `_ for more information.
diff --git a/docs/index.rst b/docs/index.rst
index c2a38264..9fbbf608 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -28,4 +28,5 @@
:hidden:
source_code
+ developer_guide
contributing
diff --git a/docs/installation.rst b/docs/installation.rst
index 527ad1f2..c0a9d42a 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -85,12 +85,6 @@ You can also build :code:`BoboCEP` manually with:
Development
===========
-If you are installing :code:`BoboCEP` for development purposes, you will
-need to install its core requirements from both
-:code:`requirements.txt` and its additional development requirements from
-:code:`requirements-dev.txt`. For example:
-
-.. code:: console
-
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
+If you want to develop :code:`BoboCEP`, see
+`Developer Guide `_
+for more information.
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f3f16880..537145c2 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,10 +1,10 @@
bump2version==1.0.1
-coverage==7.0.5
-flake8==6.0.0
-Flask==2.2.3
+coverage==7.3.1
+flake8==6.1.0
+Flask==2.3.3
interrogate==1.5.0
-mypy==0.991
-pytest==7.2.0
-sphinx==4.1.1
+mypy==1.5.1
+pytest==7.4.2
+sphinx==4.2.0
sphinx-mdinclude==0.5.3
sphinx-rtd-theme==1.1.1
diff --git a/requirements.txt b/requirements.txt
index 41904a73..e084a3d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
-pycryptodome==3.17.0
+jsonschema==4.19.0
+pycryptodome==3.18.0
diff --git a/setup.cfg b/setup.cfg
index 7b609a74..8205bb30 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,9 +1,6 @@
[bumpversion]
-current_version = 1.0.1
+current_version = 1.1.0
commit = True
-tag = True
-sign_tags = True
-tag_name = {new_version}
[bumpversion:file:setup.py]
search = version="{current_version}"
diff --git a/setup.py b/setup.py
index 478ba249..f46c5907 100644
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@
setup(
name="bobocep",
- version="1.0.1",
+ version="1.1.0",
author="r3w0p",
author_email="rr33ww00pp@gmail.com",
description="A fault-tolerant Complex Event Processing engine "
@@ -19,11 +19,11 @@
url="https://github.com/r3w0p/bobocep",
keywords=[
"complex event processing",
- "internet of things",
- "web of things",
- "fault tolerance",
+ "distributed systems",
"edge computing",
- "distributed systems"
+ "fault tolerance",
+ "internet of things",
+ "web of things"
],
classifiers=[
"Development Status :: 5 - Production/Stable",
@@ -34,6 +34,8 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Scientific/Engineering :: Information Analysis",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: System :: Distributed Computing",
"Topic :: System :: Monitoring"
],
install_requires=install_requires,
diff --git a/tests/postman/__init__.py b/tests/postman/__init__.py
index 3a80490d..24cdd84c 100644
--- a/tests/postman/__init__.py
+++ b/tests/postman/__init__.py
@@ -1,3 +1,7 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
"""
Tests for data-input testing via Postman.
"""
diff --git a/tests/postman/flask_123/run.py b/tests/postman/flask_123/run.py
index 5e321b00..5c6b9ac9 100644
--- a/tests/postman/flask_123/run.py
+++ b/tests/postman/flask_123/run.py
@@ -1,3 +1,7 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
from datetime import datetime
from threading import RLock, Thread
from typing import Tuple, Any
diff --git a/tests/postman/flask_dist_abc_def/run_dist_1.py b/tests/postman/flask_dist_abc_def/run_dist_1.py
index 9a21a89d..b306c543 100644
--- a/tests/postman/flask_dist_abc_def/run_dist_1.py
+++ b/tests/postman/flask_dist_abc_def/run_dist_1.py
@@ -1,3 +1,7 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
import logging
from datetime import datetime
from threading import Thread, RLock
@@ -12,7 +16,7 @@
from bobocep.dist import BoboDevice
from bobocep.setup import BoboSetupSimpleDistributed
-app = Flask(__name__) # v2.2.3
+app = Flask(__name__)
engine: BoboEngine
diff --git a/tests/postman/flask_dist_abc_def/run_dist_2.py b/tests/postman/flask_dist_abc_def/run_dist_2.py
index 0b20b879..11b95d1a 100644
--- a/tests/postman/flask_dist_abc_def/run_dist_2.py
+++ b/tests/postman/flask_dist_abc_def/run_dist_2.py
@@ -1,3 +1,7 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
import logging
from datetime import datetime
from threading import Thread, RLock
@@ -12,7 +16,7 @@
from bobocep.dist import BoboDevice
from bobocep.setup import BoboSetupSimpleDistributed
-app = Flask(__name__) # v2.2.3
+app = Flask(__name__)
engine: BoboEngine
diff --git a/tests/test_bobocep/__init__.py b/tests/test_bobocep/__init__.py
index 38e37ec1..e9de1422 100644
--- a/tests/test_bobocep/__init__.py
+++ b/tests/test_bobocep/__init__.py
@@ -1,3 +1,7 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
"""
Tests for unit testing via pytest.
"""
diff --git a/tests/test_bobocep/test_cep/test_action/test_common/__init__.py b/tests/test_bobocep/test_cep/test_action/test_common/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_bobocep/test_cep/test_action/test_common/test_BoboActionMultiSequential.py b/tests/test_bobocep/test_cep/test_action/test_common/test_BoboActionMultiSequential.py
new file mode 100644
index 00000000..719fee40
--- /dev/null
+++ b/tests/test_bobocep/test_cep/test_action/test_common/test_BoboActionMultiSequential.py
@@ -0,0 +1,94 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
+import pytest
+
+from bobocep.cep.action import BoboActionError
+from bobocep.cep.action.common.multi import BoboActionMultiSequential
+from tests.test_bobocep.test_cep.test_action import BoboActionTrue, \
+ BoboActionFalse
+from tests.test_bobocep.test_cep.test_event import tc_event_complex
+
+
+class TestValid:
+
+ def test_3_actions_all_success_stop(self):
+ actions = [
+ BoboActionTrue(),
+ BoboActionTrue(),
+ BoboActionTrue()
+ ]
+ multi = BoboActionMultiSequential(
+ name="test_multi",
+ actions=actions,
+ stop_on_fail=True)
+
+ success, data = multi.execute(tc_event_complex())
+
+ assert success
+ assert len(data) == len(actions)
+
+ a1_success, a1_data = data[0]
+ a2_success, a2_data = data[1]
+ a3_success, a3_data = data[2]
+
+ assert a1_success
+ assert a2_success
+ assert a3_success
+
+ def test_3_actions_middle_fail_stop(self):
+ actions = [
+ BoboActionTrue(),
+ BoboActionFalse(),
+ BoboActionTrue()
+ ]
+ multi = BoboActionMultiSequential(
+ name="test_multi",
+ actions=actions,
+ stop_on_fail=True)
+
+ success, data = multi.execute(tc_event_complex())
+
+ assert not success
+ assert len(data) == (len(actions) - 1)
+
+ a1_success, a1_data = data[0]
+ a2_success, a2_data = data[1]
+
+ assert a1_success
+ assert not a2_success
+
+ def test_3_actions_middle_fail_no_stop(self):
+ actions = [
+ BoboActionTrue(),
+ BoboActionFalse(),
+ BoboActionTrue()
+ ]
+ multi = BoboActionMultiSequential(
+ name="test_multi",
+ actions=actions,
+ stop_on_fail=False)
+
+ success, data = multi.execute(tc_event_complex())
+
+ assert not success
+ assert len(data) == len(actions)
+
+ a1_success, a1_data = data[0]
+ a2_success, a2_data = data[1]
+ a3_success, a3_data = data[2]
+
+ assert a1_success
+ assert not a2_success
+ assert a3_success
+
+
+class TestInvalid:
+
+ def test_0_actions(self):
+ with pytest.raises(BoboActionError):
+ BoboActionMultiSequential(
+ name="test_multi",
+ actions=[],
+ stop_on_fail=True)
diff --git a/tests/test_bobocep/test_cep/test_engine/test_receiver/test_BoboValidatorJSONSchema.py b/tests/test_bobocep/test_cep/test_engine/test_receiver/test_BoboValidatorJSONSchema.py
new file mode 100644
index 00000000..ebcf4942
--- /dev/null
+++ b/tests/test_bobocep/test_cep/test_engine/test_receiver/test_BoboValidatorJSONSchema.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+import pytest
+
+from bobocep.cep.engine.receiver.validator import BoboValidatorError, \
+ BoboValidatorJSONSchema
+
+SCHEMA_VALID: dict = {
+ "type": "object",
+ "required": ["forename", "surname"],
+ "properties": {
+ "forename": {
+ "type": "string"
+ },
+ "surname": {
+ "type": "string"
+ }
+ }
+}
+
+SCHEMA_INVALID: str = "abc123"
+
+
+class TestValid:
+
+ def test_valid_schema_valid_instance(self):
+ data: dict = {
+ "forename": "Foo",
+ "surname": "Bar"
+ }
+
+ validator = BoboValidatorJSONSchema(schema=SCHEMA_VALID)
+
+ assert validator.is_valid(data=data)
+
+
+class TestInvalid:
+
+ def test_invalid_schema(self):
+ data: dict = {}
+
+ validator = BoboValidatorJSONSchema(schema=SCHEMA_INVALID)
+
+ with pytest.raises(BoboValidatorError):
+ assert validator.is_valid(data=data)
+
+ def test_valid_schema_invalid_instance(self):
+ data: dict = {
+ "forename": "Foo"
+ }
+
+ validator = BoboValidatorJSONSchema(schema=SCHEMA_VALID)
+
+ assert not validator.is_valid(data=data)
diff --git a/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py b/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py
index 71a5e1de..e42a8c20 100644
--- a/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py
+++ b/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py
@@ -4,8 +4,38 @@
import pytest
-from bobocep.cep.phenom.phenom import BoboPhenomenonError
-from tests.test_bobocep.test_cep.test_phenom import tc_phenomenon
+from bobocep.cep.phenom.phenom import BoboPhenomenonError, BoboPhenomenon
+from tests.test_bobocep.test_cep.test_action import BoboActionTrue
+from tests.test_bobocep.test_cep.test_phenom import tc_phenomenon, tc_pattern
+
+
+class TestValid:
+
+ def test_one_pattern(self):
+ name_phenom = "name_phenom"
+ name_pattern = "name_pattern"
+ name_action = "name_action"
+
+ pattern = tc_pattern(name_pattern)
+ action = BoboActionTrue(name_action)
+ datagen = lambda p, h: 123
+ retain = True
+
+ phenom = BoboPhenomenon(
+ name=name_phenom,
+ patterns=[pattern],
+ action=action,
+ datagen=datagen,
+ retain=retain)
+
+ assert phenom.name == name_phenom
+ assert len(phenom.patterns) == 1
+ assert phenom.patterns[0].name == name_pattern
+ assert phenom.action is not None
+ assert phenom.action.name == name_action
+ assert phenom.datagen is not None
+ assert phenom.datagen(None, None) == 123
+ assert phenom.retain
class TestInvalid:
diff --git a/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py b/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py
index e8413f58..e0d24916 100644
--- a/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py
+++ b/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py
@@ -10,6 +10,18 @@
tc_predicate
+# TODO tests for valid patterns, different
+class TestValid:
+
+ def test_name_0_length(self):
+ with pytest.raises(BoboPatternError):
+ BoboPattern(name="",
+ blocks=[tc_block(group="a")],
+ preconditions=[tc_predicate()],
+ haltconditions=[tc_predicate()])
+
+
+
class TestInvalid:
def test_name_0_length(self):
diff --git a/tests/test_examples.py b/tests/test_examples.py
new file mode 100644
index 00000000..b1e016d9
--- /dev/null
+++ b/tests/test_examples.py
@@ -0,0 +1,435 @@
+# Copyright (c) 2019-2023 r3w0p
+# The following code can be redistributed and/or
+# modified under the terms of the MIT License.
+
+"""
+Tests that ensure complex event generation occurs when expected.
+The tests contain typical patterns that may be built by the user
+with the pattern builder.
+
+This is not an exhaustive list due to the sheer number of possible
+combinations. However, the tests are representative of some of the
+common patterns that are likely to frequently occur.
+"""
+
+from threading import RLock
+from typing import Tuple, Any, List
+
+from bobocep.cep.action import BoboAction, BoboActionHandlerBlocking
+from bobocep.cep.event import BoboEventComplex
+from bobocep.cep.phenom import BoboPattern, BoboPhenomenon
+from bobocep.cep.phenom.pattern.builder import BoboPatternBuilder
+from bobocep.setup import BoboSetupSimple
+
+
+class BoboActionCounter(BoboAction):
+ """
+ An action that counts how many times it has been executed.
+ """
+
+ def __init__(self, name: str):
+ """
+ :param name: The name of the action.
+ """
+ super().__init__(name)
+ self._lock: RLock = RLock()
+ self._counter: int = 0
+
+ def execute(self, event: BoboEventComplex) -> Tuple[bool, Any]:
+ """
+ Increments the counter value.
+
+ :param event: The complex event.
+
+ :return: True, and the new counter value
+ """
+ with self._lock:
+ self._counter += 1
+
+ return True, None
+
+ @property
+ def counter(self) -> int:
+ """
+ Get counter.
+ """
+ with self._lock:
+ return self._counter
+
+
+def _setup(patterns: List[BoboPattern]):
+ action = BoboActionCounter(name="action")
+
+ phenom = BoboPhenomenon(
+ name="phenom",
+ patterns=patterns,
+ action=action)
+
+ engine = BoboSetupSimple(
+ phenomena=[phenom],
+ handler=BoboActionHandlerBlocking()
+ ).generate()
+
+ return engine, action
+
+
+class TestValid:
+
+ def test_fb_fb_fb_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_nfb_fb_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .not_followed_by(lambda e, h: int(e.data) == 2) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 6, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_nfb_fb_data_not_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .not_followed_by(lambda e, h: int(e.data) == 2) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 6, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fba_fb_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by_any([
+ lambda e, h: int(e.data) == 21,
+ lambda e, h: int(e.data) == 22,
+ lambda e, h: int(e.data) == 23
+ ]) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 22, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fba_fb_data_not_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by_any([
+ lambda e, h: int(e.data) == 21,
+ lambda e, h: int(e.data) == 22,
+ lambda e, h: int(e.data) == 23
+ ]) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 6, 23, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_nfba_fb_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .not_followed_by_any([
+ lambda e, h: int(e.data) == 21,
+ lambda e, h: int(e.data) == 22,
+ lambda e, h: int(e.data) == 23
+ ]) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_nfba_fb_data_not_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .not_followed_by_any([
+ lambda e, h: int(e.data) == 21,
+ lambda e, h: int(e.data) == 22,
+ lambda e, h: int(e.data) == 23
+ ]) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 21, 22, 23, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_n_n_n_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .next(lambda e, h: int(e.data) == 1) \
+ .next(lambda e, h: int(e.data) == 2) \
+ .next(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_n_nn_n_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .next(lambda e, h: int(e.data) == 1) \
+ .not_next(lambda e, h: int(e.data) == 2) \
+ .next(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 6, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_pre_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .precondition(lambda e, h: int(e.data) > 0) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_halt_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .haltcondition(lambda e, h: int(e.data) == 10) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_times_data_exact(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2, times=3) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 2, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_loop_minimum(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2, loop=True) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_loop_once(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2, loop=True) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_loop_many(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2, loop=True) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_optional_included(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2, optional=True) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+ def test_fb_fb_fb_optional_not_included(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2, optional=True) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 1
+
+
+class TestInvalid:
+
+ def test_fb_fb_fb_pre(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .precondition(lambda e, h: int(e.data) > 0) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 0, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 0
+
+ def test_fb_fb_fb_halt(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .followed_by(lambda e, h: int(e.data) == 1) \
+ .followed_by(lambda e, h: int(e.data) == 2) \
+ .followed_by(lambda e, h: int(e.data) == 3) \
+ .haltcondition(lambda e, h: int(e.data) == 10) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 10, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 0
+
+ def test_n_n_n(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .next(lambda e, h: int(e.data) == 1) \
+ .next(lambda e, h: int(e.data) == 2) \
+ .next(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 6, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 0
+
+ def test_n_nn_n(self):
+ pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \
+ .next(lambda e, h: int(e.data) == 1) \
+ .not_next(lambda e, h: int(e.data) == 2) \
+ .next(lambda e, h: int(e.data) == 3) \
+ .generate()
+
+ engine, action = _setup([pattern_1])
+
+ for i in [1, 2, 3]:
+ engine.receiver.add_data(i)
+ engine.update()
+
+ assert len(engine.decider.all_runs()) == 0
+ assert action.counter == 0