actinia-cloudevent-plugin 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2018-2025 mundialis GmbH & Co. KG.
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ actinia plugin initalization
18
+ """
19
+
20
+ __license__ = "GPLv3"
21
+ __author__ = "Carmen Tawalika, Anika Weinmann"
22
+ __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG"
23
+ __maintainer__ = "mundialis GmbH & Co. KG"
24
+
25
+
26
+ import importlib.metadata
27
+
28
+ try:
29
+ # Change here if project is renamed and does not equal the package name
30
+ DIST_NAME = __name__
31
+ __version__ = importlib.metadata.version(DIST_NAME)
32
+ except Exception():
33
+ __version__ = "unknown"
@@ -0,0 +1,4 @@
1
+ """actinia-cloudevent-plugin API part of package.
2
+
3
+ This part provides the API part of the actinia-cloudevent-plugin.
4
+ """
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2025 mundialis GmbH & Co. KG.
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ Hello World class
18
+ """
19
+
20
+ __license__ = "GPLv3"
21
+ __author__ = "Lina Krisztian"
22
+ __copyright__ = "Copyright 2025 mundialis GmbH & Co. KG"
23
+ __maintainer__ = "mundialis GmbH & Co. KG"
24
+
25
+ from flask import jsonify, make_response
26
+ from flask_restful_swagger_2 import Resource, swagger
27
+ from requests.exceptions import ConnectionError # noqa: A004
28
+
29
+ from actinia_cloudevent_plugin.apidocs import cloudevent
30
+ from actinia_cloudevent_plugin.core.processing import (
31
+ cloud_event_to_process_chain,
32
+ receive_cloud_event,
33
+ send_binary_cloud_event,
34
+ # send_structured_cloud_event,
35
+ )
36
+ from actinia_cloudevent_plugin.model.response_models import (
37
+ SimpleStatusCodeResponseModel,
38
+ )
39
+ from actinia_cloudevent_plugin.resources.config import EVENTRECEIVER
40
+
41
+
42
+ class Cloudevent(Resource):
43
+ """Cloudevent handling."""
44
+
45
+ def __init__(self) -> None:
46
+ """Cloudevent class initialisation."""
47
+ self.msg = (
48
+ "Received event <EVENT1> and returned event <EVENT2>"
49
+ " with actinia-job <ACTINIA_JOB>."
50
+ )
51
+
52
+ def get(self):
53
+ """Cloudevent get method: not allowed response."""
54
+ res = jsonify(
55
+ SimpleStatusCodeResponseModel(
56
+ status=405,
57
+ message="Method Not Allowed",
58
+ ),
59
+ )
60
+ return make_response(res, 405)
61
+
62
+ @swagger.doc(cloudevent.describe_cloudevent_post_docs)
63
+ def post(self) -> SimpleStatusCodeResponseModel:
64
+ """Cloudevent post method with cloudevent from postbody.
65
+
66
+ Receives cloudevent, transforms to process chain (pc),
67
+ sends pc to actinia + start process,
68
+ and returns cloudevent with queue name.
69
+ """
70
+ # Transform postbody to cloudevent
71
+ event_received = receive_cloud_event()
72
+ # With received process chain start actinia process + return cloudevent
73
+ actinia_job = cloud_event_to_process_chain(event_received)
74
+ # URL to which the generated cloudevent is sent
75
+ url = EVENTRECEIVER.url
76
+ # TODO: binary or structured cloud event?
77
+ # From https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#message
78
+ # A "structured-mode message" is one where the entire event (attributes and data)
79
+ # are encoded in the message body, according to a specific event format.
80
+ # A "binary-mode message" is one where the event data is stored in the message body,
81
+ # and event attributes are stored as part of message metadata.
82
+ # Often, binary mode is used when the producer of the CloudEvent wishes to add the
83
+ # CloudEvent's metadata to an existing event without impacting the message's body.
84
+ # In most cases a CloudEvent encoded as a binary-mode message will not break an
85
+ # existing receiver's processing of the event because the message's metadata
86
+ # typically allows for extension attributes.
87
+ # In other words, a binary formatted CloudEvent would work for both
88
+ # a CloudEvents enabled receiver as well as one that is unaware of CloudEvents.
89
+ try:
90
+ event_returned = send_binary_cloud_event(
91
+ event_received,
92
+ actinia_job,
93
+ url,
94
+ )
95
+ return SimpleStatusCodeResponseModel(
96
+ status=204,
97
+ message=self.msg.replace("<EVENT1>", event_received["id"])
98
+ .replace("<EVENT2>", event_returned["id"])
99
+ .replace("<ACTINIA_JOB>", actinia_job),
100
+ )
101
+ except ConnectionError as e:
102
+ return f"Connection ERROR when returning cloudevent: {e}"
103
+ except Exception() as e:
104
+ return f"ERROR when returning cloudevent: {e}"
@@ -0,0 +1,4 @@
1
+ """actinia-cloudevent-plugin API DOCs part of package.
2
+
3
+ This part provides the API DOCs part of the actinia-cloudevent-plugin.
4
+ """
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2025 mundialis GmbH & Co. KG.
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ Hello World class
18
+ """
19
+
20
+ __license__ = "GPLv3"
21
+ __author__ = "Lina Krisztian"
22
+ __copyright__ = "Copyright 2025 mundialis GmbH & Co. KG"
23
+ __maintainer__ = "mundialis GmbH & Co. KG"
24
+
25
+
26
+ from actinia_cloudevent_plugin.model.response_models import (
27
+ SimpleStatusCodeResponseModel,
28
+ )
29
+
30
+ describe_cloudevent_post_docs = {
31
+ # "summary" is taken from the description of the get method
32
+ "tags": ["cloudevent"],
33
+ "description": (
34
+ "Receives cloudevent, transforms and starts pc and returns cloudevent."
35
+ ),
36
+ "responses": {
37
+ "200": {
38
+ "description": (
39
+ "This response returns received, and returned events, "
40
+ "generated queue name and the status"
41
+ ),
42
+ "schema": SimpleStatusCodeResponseModel,
43
+ },
44
+ "400": {
45
+ "description": "This response returns an error message",
46
+ "schema": SimpleStatusCodeResponseModel,
47
+ },
48
+ },
49
+ }
@@ -0,0 +1,4 @@
1
+ """actinia-cloudevent-plugin core part of package.
2
+
3
+ This part provides the core part of the actinia-cloudevent-plugin.
4
+ """
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2025 mundialis GmbH & Co. KG.
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ Example core functionality
18
+ """
19
+
20
+ __license__ = "GPLv3"
21
+ __author__ = "Lina Krisztian"
22
+ __copyright__ = "Copyright 2025 mundialis GmbH & Co. KG"
23
+ __maintainer__ = "mundialis GmbH & Co. KG"
24
+
25
+
26
+ import requests
27
+ from cloudevents.conversion import to_binary, to_structured
28
+ from cloudevents.http import CloudEvent, from_http
29
+ from flask import request
30
+
31
+
32
+ def receive_cloud_event():
33
+ """Return cloudevent from postpody."""
34
+ # Parses CloudEvent 'data' and 'headers' into a CloudEvent.
35
+ event = from_http(request.headers, request.get_data())
36
+
37
+ # ? TODO
38
+ # eventually Filter the event (see example below)
39
+ event_type = event["type"]
40
+ if event_type == "com.example.object.created":
41
+ print("Object created event received!")
42
+
43
+ return event
44
+
45
+
46
+ def cloud_event_to_process_chain(event) -> str:
47
+ """Return queue name for process chain of event."""
48
+ # (Remove ruff-exception, when pc variable used)
49
+ pc = event.get_data()["list"][0] # noqa: F841
50
+ # !! TODO !!: pc to job
51
+ # NOTE: as standalone app -> consider for queue name creation
52
+ # HTTP POST pc to actinia-module plugin processing endpoint
53
+ # # # include an identifier for grouping cloudevents of same actinia process (?)
54
+ # # # (e.g. new metadata field "queue_name", or within data, or use existign id)
55
+ # -> actinia core returns resource-url, including resource_id (and queue name)
56
+ # (queuename = xx_<resource_id>; if configured accordingly within actinia -> each job own queue)
57
+ # via knative jobsink: start actinia worker (with queue name)
58
+ # (https://knative.dev/docs/eventing/sinks/job-sink/#usage)
59
+ # e.g. HTTP POST with queue name
60
+ # kubectl run curl --image=curlimages/curl --rm=true --restart=Never -ti -- -X POST -v \
61
+ # -H "content-type: application/json" \
62
+ # -H "ce-specversion: 1.0" \
63
+ # -H "ce-source: my/curl/command" \
64
+ # -H "ce-type: my.demo.event" \
65
+ # -H "ce-id: 123" \
66
+ # -d '{"details":"queuename"}' \
67
+ # http://job-sink.knative-eventing.svc.cluster.local/default/job-sink-logger
68
+ return "<queue_name>_<resource_id>" # queue name and resource id
69
+
70
+
71
+ def send_binary_cloud_event(event, actinia_job, url):
72
+ """Return posted binary event with actinia_job."""
73
+ attributes = {
74
+ "specversion": event["specversion"],
75
+ "source": "/actinia-cloudevent-plugin",
76
+ "type": "com.mundialis.actinia.process.started",
77
+ "subject": event["subject"],
78
+ "datacontenttype": "application/json",
79
+ }
80
+ data = {"actinia_job": actinia_job}
81
+
82
+ event = CloudEvent(attributes, data)
83
+ headers, body = to_binary(event)
84
+ # send event
85
+ requests.post(url, headers=headers, data=body)
86
+
87
+ return event
88
+
89
+
90
+ def send_structured_cloud_event(event, actinia_job, url):
91
+ """Return posted structured event with actinia_job."""
92
+ attributes = {
93
+ "specversion": event["specversion"],
94
+ "source": "/actinia-cloudevent-plugin",
95
+ "type": "com.mundialis.actinia.process.started",
96
+ "subject": event["subject"],
97
+ "datacontenttype": "application/json",
98
+ }
99
+ data = {"actinia_job": actinia_job}
100
+
101
+ event = CloudEvent(attributes, data)
102
+ headers, body = to_structured(event)
103
+ # send event
104
+ requests.post(url, headers=headers, data=body)
105
+
106
+ return event
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2018-2025 mundialis GmbH & Co. KG.
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ Add endpoints to flask app with endpoint definitions and routes
18
+ """
19
+
20
+ __license__ = "GPLv3"
21
+ __author__ = "Carmen Tawalika, Anika Weinmann, Lina Krisztian"
22
+ __copyright__ = "Copyright 2022-2024 mundialis GmbH & Co. KG"
23
+ __maintainer__ = "mundialis GmbH & Co. KG"
24
+
25
+
26
+ import sys
27
+
28
+ import werkzeug
29
+ from flask import current_app, send_from_directory
30
+ from flask_restful_swagger_2 import Api
31
+
32
+ from actinia_cloudevent_plugin.api.cloudevent import Cloudevent
33
+ from actinia_cloudevent_plugin.resources.logging import log
34
+
35
+
36
+ # endpoints loaded if run as actinia-core plugin as well as standalone app
37
+ def create_endpoints(flask_api: Api) -> None:
38
+ """Create plugin endpoints."""
39
+ app = flask_api.app
40
+ apidoc = flask_api
41
+
42
+ package = sys._getframe().f_back.f_globals["__package__"] # noqa: SLF001
43
+ if package != "actinia_core":
44
+
45
+ @app.route("/")
46
+ def index():
47
+ try:
48
+ return current_app.send_static_file("index.html")
49
+ except werkzeug.exceptions.NotFound:
50
+ log.debug("No index.html found. Serving backup.")
51
+ # when actinia-cloudevent-plugin is installed in single mode,
52
+ # the swagger endpoint would be "latest/api/swagger.json".
53
+ # As api docs exist in single mode,
54
+ # use this fallback for plugin mode.
55
+ return """<h1 style='color:red'>actinia-metadata-plugin</h1>
56
+ <a href="api/v1/swagger.json">API docs</a>"""
57
+
58
+ @app.route("/<path:filename>")
59
+ def static_content(filename):
60
+ # WARNING: all content from folder "static" will be accessible!
61
+ return send_from_directory(app.static_folder, filename)
62
+
63
+ apidoc.add_resource(Cloudevent, "/")
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2025 mundialis GmbH & Co. KG.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+
17
+ Application entrypoint. Creates Flask app and swagger docs, adds endpoints
18
+ """
19
+
20
+ __author__ = "Carmen Tawalika, Lina Krisztian"
21
+ __copyright__ = "2025-present mundialis GmbH & Co. KG"
22
+ __license__ = "Apache-2.0"
23
+
24
+
25
+ from flask import Flask
26
+ from flask_cors import CORS
27
+ from flask_restful_swagger_2 import Api
28
+
29
+ from actinia_cloudevent_plugin.endpoints import create_endpoints
30
+ from actinia_cloudevent_plugin.resources.logging import log
31
+
32
+ flask_app = Flask(__name__)
33
+ # allows endpoints with and without trailing slashes
34
+ flask_app.url_map.strict_slashes = False
35
+ CORS(flask_app)
36
+
37
+
38
+ API_VERSION = "v1"
39
+
40
+ URL_PREFIX = f"/api/{API_VERSION}"
41
+
42
+ apidoc = Api(
43
+ flask_app,
44
+ title="actinia-cloudevent-plugin",
45
+ prefix=URL_PREFIX,
46
+ api_version=API_VERSION,
47
+ api_spec_url=f"{URL_PREFIX}/swagger",
48
+ schemes=["https", "http"],
49
+ consumes=["application/json"],
50
+ description="""Receives cloudevent,
51
+ transforms it to an actinia process chain
52
+ and returns cloudevent back.
53
+ """,
54
+ )
55
+
56
+ create_endpoints(apidoc)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ # call this for development only with:
61
+ # `python3 -m actinia_cloudevent_plugin.main`
62
+ log.debug("starting app in development mode...")
63
+ # ruff: S201 :Use of `debug=True` in Flask app detected
64
+ flask_app.run(debug=True, use_reloader=False) # noqa: S201
65
+ # for production environent use application in wsgi.py
@@ -0,0 +1,4 @@
1
+ """actinia-cloudevent-plugin model part of package.
2
+
3
+ This part provides the model part of the actinia-cloudevent-plugin.
4
+ """
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2018-2024 mundialis GmbH & Co. KG.
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ Response models
18
+ """
19
+
20
+ __license__ = "GPLv3"
21
+ __author__ = "Anika Weinmann"
22
+ __copyright__ = "Copyright 2022 mundialis GmbH & Co. KG"
23
+ __maintainer__ = "mundialis GmbH & Co. KG"
24
+
25
+
26
+ from typing import ClassVar
27
+
28
+ from flask_restful_swagger_2 import Schema
29
+
30
+
31
+ class SimpleStatusCodeResponseModel(Schema):
32
+ """Simple response schema to inform about status."""
33
+
34
+ type: str = "object"
35
+ properties: ClassVar[dict] = {
36
+ "status": {
37
+ "type": "number",
38
+ "description": "The status code of the request.",
39
+ },
40
+ "message": {
41
+ "type": "string",
42
+ "description": "A short message to describes the status",
43
+ },
44
+ }
45
+ required: ClassVar[list[str]] = ["status", "message"]
46
+
47
+
48
+ simple_response_example = SimpleStatusCodeResponseModel(
49
+ status=200,
50
+ message="success",
51
+ )
52
+ SimpleStatusCodeResponseModel.example = simple_response_example
@@ -0,0 +1,4 @@
1
+ """actinia-cloudevent-plugin resources part of package.
2
+
3
+ This part provides the Resources part of the actinia-cloudevent-plugin.
4
+ """
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python
2
+ """Copyright (c) 2018-2025 mundialis GmbH & Co. KG.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+
17
+ Configuration file
18
+ """
19
+
20
+ __author__ = "Carmen Tawalika, Lina Krisztian"
21
+ __copyright__ = "2018-2025 mundialis GmbH & Co. KG"
22
+ __license__ = "Apache-2.0"
23
+
24
+
25
+ import configparser
26
+ from pathlib import Path
27
+
28
+ # config can be overwritten by mounting *.ini files into folders inside
29
+ # the config folder.
30
+ DEFAULT_CONFIG_PATH = "config"
31
+ CONFIG_FILES = [
32
+ str(f) for f in Path(DEFAULT_CONFIG_PATH).glob("**/*.ini") if f.is_file()
33
+ ]
34
+ GENERATED_CONFIG = DEFAULT_CONFIG_PATH + "/actinia-cloudevent-plugin.cfg"
35
+
36
+
37
+ class EVENTRECEIVER:
38
+ """Default config for cloudevent receiver."""
39
+
40
+ url = "http://localhost:3000/"
41
+
42
+
43
+ class LOGCONFIG:
44
+ """Default config for logging."""
45
+
46
+ logfile = "actinia-cloudevent-plugin.log"
47
+ level = "INFO"
48
+ type = "stdout"
49
+
50
+
51
+ class Configfile:
52
+ """Configuration file."""
53
+
54
+ def __init__(self) -> None:
55
+ """Overwrite config classes.
56
+
57
+ Will overwrite the config classes above when config files
58
+ named DEFAULT_CONFIG_PATH/**/*.ini exist.
59
+ On first import of the module it is initialized.
60
+ """
61
+ config = configparser.ConfigParser()
62
+ config.read(CONFIG_FILES)
63
+ if len(config) <= 1:
64
+ print("Could not find any config file, using default values.")
65
+ return
66
+ print("Loading config files: " + str(CONFIG_FILES) + " ...")
67
+
68
+ with open( # noqa: PTH123
69
+ GENERATED_CONFIG,
70
+ "w",
71
+ encoding="utf-8",
72
+ ) as configfile:
73
+ config.write(configfile)
74
+ print("Configuration written to " + GENERATED_CONFIG)
75
+
76
+ # LOGGING
77
+ if config.has_section("LOGCONFIG"):
78
+ if config.has_option("LOGCONFIG", "logfile"):
79
+ LOGCONFIG.logfile = config.get("LOGCONFIG", "logfile")
80
+ if config.has_option("LOGCONFIG", "level"):
81
+ LOGCONFIG.level = config.get("LOGCONFIG", "level")
82
+ if config.has_option("LOGCONFIG", "type"):
83
+ LOGCONFIG.type = config.get("LOGCONFIG", "type")
84
+
85
+ # EVENTRECEIVER
86
+ if config.has_section("EVENTRECEIVER"):
87
+ if config.has_option("EVENTRECEIVER", "url"):
88
+ EVENTRECEIVER.url = config.get("EVENTRECEIVER", "url")
89
+
90
+
91
+ init = Configfile()