kelvin-python-sdk 0.2.0__tar.gz → 0.2.3__tar.gz

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.
Files changed (114) hide show
  1. {kelvin_python_sdk-0.2.0/kelvin_python_sdk.egg-info → kelvin_python_sdk-0.2.3}/PKG-INFO +3 -2
  2. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/csv_publisher/csv_publisher.py +27 -24
  3. kelvin_python_sdk-0.2.3/examples/custom_action/action_generator.py +29 -0
  4. kelvin_python_sdk-0.2.3/examples/custom_action/app.yaml +16 -0
  5. kelvin_python_sdk-0.2.3/examples/custom_action/main.py +80 -0
  6. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/application/client.py +22 -11
  7. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/application/filters.py +5 -0
  8. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/application/window.py +16 -7
  9. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/common.py +14 -3
  10. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/exporter.py +11 -1
  11. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/importer.py +11 -1
  12. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/manifest.py +13 -1
  13. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/smart_app.py +17 -1
  14. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/__init__.py +10 -2
  15. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/base_messages.py +100 -41
  16. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/evidences.py +1 -1
  17. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/message.py +9 -3
  18. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/msg_builders.py +245 -34
  19. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/msg_type.py +28 -0
  20. kelvin_python_sdk-0.2.3/kelvin/publisher/__init__.py +3 -0
  21. kelvin_python_sdk-0.2.3/kelvin/publisher/csv_publisher.py +116 -0
  22. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/publisher/main.py +13 -15
  23. kelvin_python_sdk-0.2.0/kelvin/publisher/publisher.py → kelvin_python_sdk-0.2.3/kelvin/publisher/server.py +85 -315
  24. kelvin_python_sdk-0.2.3/kelvin/publisher/simulator.py +119 -0
  25. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3/kelvin_python_sdk.egg-info}/PKG-INFO +3 -2
  26. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin_python_sdk.egg-info/SOURCES.txt +8 -1
  27. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin_python_sdk.egg-info/requires.txt +1 -0
  28. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/pyproject.toml +1 -0
  29. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_convert_exporter.py +13 -1
  30. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_convert_importer.py +14 -2
  31. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_convert_smart_app.py +13 -1
  32. kelvin_python_sdk-0.2.3/tests/test_evidences.py +87 -0
  33. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_message.py +4 -6
  34. kelvin_python_sdk-0.2.3/tests/test_message_builders.py +154 -0
  35. kelvin_python_sdk-0.2.3/tests/test_msg_type.py +50 -0
  36. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_window.py +12 -10
  37. kelvin_python_sdk-0.2.0/kelvin/publisher/__init__.py +0 -3
  38. kelvin_python_sdk-0.2.0/tests/test_message_builders.py +0 -76
  39. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/.gitignore +0 -0
  40. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/Jenkinsfile +0 -0
  41. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/README.md +0 -0
  42. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/constraints.txt +0 -0
  43. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/csv_publisher/app.yaml +0 -0
  44. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/csv_publisher/data.csv +0 -0
  45. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/csv_test/assets.csv +0 -0
  46. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/csv_test/data2.csv +0 -0
  47. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/csv_test/smart_app.yaml +0 -0
  48. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/data_tags/app.yaml +0 -0
  49. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/data_tags/main.py +0 -0
  50. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/evidence/app.yaml +0 -0
  51. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/evidence/main.py +0 -0
  52. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/example/app.yaml +0 -0
  53. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/example/example.py +0 -0
  54. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/mygenerator.py +0 -0
  55. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/random_publisher/app.yaml +0 -0
  56. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/random_publisher/random_publisher.py +0 -0
  57. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/rec_app.py +0 -0
  58. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/rec_app.yaml +0 -0
  59. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/hopping-window/.dockerignore +0 -0
  60. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/hopping-window/Dockerfile +0 -0
  61. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/hopping-window/app.yaml +0 -0
  62. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/hopping-window/data.csv +0 -0
  63. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/hopping-window/main.py +0 -0
  64. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/hopping-window/requirements.txt +0 -0
  65. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/rolling-window/.dockerignore +0 -0
  66. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/rolling-window/Dockerfile +0 -0
  67. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/rolling-window/app.yaml +0 -0
  68. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/rolling-window/data.csv +0 -0
  69. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/rolling-window/main.py +0 -0
  70. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/rolling-window/requirements.txt +0 -0
  71. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/tumbling-window/.dockerignore +0 -0
  72. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/tumbling-window/Dockerfile +0 -0
  73. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/tumbling-window/app.yaml +0 -0
  74. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/tumbling-window/data.csv +0 -0
  75. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/tumbling-window/main.py +0 -0
  76. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/examples/windows/tumbling-window/requirements.txt +0 -0
  77. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/application/__init__.py +0 -0
  78. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/application/config.py +0 -0
  79. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/application/stream.py +0 -0
  80. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/__init__.py +0 -0
  81. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/appyaml.py +0 -0
  82. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/external.py +0 -0
  83. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/config/parser.py +0 -0
  84. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/krn/__init__.py +0 -0
  85. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/krn/krn.py +0 -0
  86. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/logs.py +0 -0
  87. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/krn.py +0 -0
  88. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/primitives.py +0 -0
  89. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin/message/utils.py +0 -0
  90. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin_python_sdk.egg-info/dependency_links.txt +0 -0
  91. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin_python_sdk.egg-info/entry_points.txt +0 -0
  92. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/kelvin_python_sdk.egg-info/top_level.txt +0 -0
  93. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/setup.cfg +0 -0
  94. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/configs/docker.yaml +0 -0
  95. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/configs/exporter.json +0 -0
  96. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/configs/exporter.yaml +0 -0
  97. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/configs/importer.yaml +0 -0
  98. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/configs/invalid.yaml +0 -0
  99. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/configs/legacy_docker.yaml +0 -0
  100. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/configs/smartapp.yaml +0 -0
  101. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/schemas/configuration.json +0 -0
  102. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/schemas/parameters.json +0 -0
  103. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/schemas/test_io_cc_schema.json +0 -0
  104. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/schemas/test_io_schema.json +0 -0
  105. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_app_base_config.py +0 -0
  106. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_client.py +0 -0
  107. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_client_calbacks.py +0 -0
  108. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_convert_external.py +0 -0
  109. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_filters.py +0 -0
  110. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_krn.py +0 -0
  111. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_parse_config.py +0 -0
  112. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_process_runtime_manifest.py +0 -0
  113. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tests/test_utils.py +0 -0
  114. {kelvin_python_sdk-0.2.0 → kelvin_python_sdk-0.2.3}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: kelvin-python-sdk
3
- Version: 0.2.0
3
+ Version: 0.2.3
4
4
  Summary: Kelvin Python SDK
5
5
  Author-email: Kelvin Inc <engineering@kelvininc.com>
6
6
  Classifier: Operating System :: OS Independent
@@ -22,6 +22,7 @@ Requires-Dist: pytest-asyncio; extra == "tests"
22
22
  Requires-Dist: pytest-cov; extra == "tests"
23
23
  Requires-Dist: time-machine; extra == "tests"
24
24
  Requires-Dist: asyncmock; python_version < "3.8" and extra == "tests"
25
+ Requires-Dist: pandas; extra == "tests"
25
26
  Provides-Extra: format
26
27
  Requires-Dist: isort; extra == "format"
27
28
  Requires-Dist: black; extra == "format"
@@ -3,36 +3,39 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  from typing import List, Optional
5
5
 
6
- import yaml
7
6
  from pydantic import BaseModel
8
7
 
9
- from kelvin.application import KelvinApp
8
+ from kelvin.application import KelvinApp, Datastream
10
9
  from kelvin.krn import KRNAssetDataStream
11
10
  from kelvin.logs import logger
12
- from kelvin.message import Boolean, Message, Number, String
13
- from kelvin.publisher.publisher import AppConfig, CSVPublisher, MessageData, Metric
14
-
15
-
16
- def message_from_message_data(data: MessageData, outputs: List[Metric]) -> Optional[Message]:
17
- output = next((output for output in outputs if output.name == data.resource), None)
11
+ from kelvin.message import Message
12
+ from kelvin.publisher.server import MessageData
13
+ from kelvin.publisher.csv_publisher import CSVPublisher
14
+ from datetime import datetime
15
+ from typing import Any, Type, Union
16
+
17
+ def string_to_strict_type(value: Any, data_type: Type) -> Union[bool, float, str, dict]:
18
+ if isinstance(value, data_type):
19
+ return value
20
+ if data_type is bool:
21
+ return str(value).lower() in ["true", "1"]
22
+ if data_type is float:
23
+ return float(value)
24
+ return value
25
+
26
+ def message_from_message_data(data: MessageData, outputs: List[Datastream]) -> Optional[Message]:
27
+ output = next((output for output in outputs if output.name == data.resource.data_stream), None)
18
28
  if output is None:
19
29
  logger.error("csv metric not found in outputs", metric=data.resource)
20
30
  return None
21
-
22
- data_type = output.data_type
23
- if data_type == "boolean":
24
- msg_type = Boolean
25
- value = bool(data.value)
26
- elif data_type == "number":
27
- msg_type = Number
28
- value = float(data.value)
29
- elif data_type == "string":
30
- msg_type = String
31
- value = str(data.value)
32
- else:
33
- return None
34
-
35
- return msg_type(resource=KRNAssetDataStream(data.asset, data.resource), payload=value)
31
+
32
+ msg = Message(
33
+ type=output.type,
34
+ timestamp=data.timestamp or datetime.now().astimezone(),
35
+ resource=data.resource
36
+ )
37
+ msg.payload = string_to_strict_type(data.value, type(msg.payload))
38
+ return msg
36
39
 
37
40
 
38
41
  class AppConfiguration(BaseModel):
@@ -55,7 +58,7 @@ async def main() -> None:
55
58
  first_run = False
56
59
  async for data in publisher.run():
57
60
  for asset in assets:
58
- data.asset = asset
61
+ data.resource = KRNAssetDataStream(asset, data.resource.data_stream)
59
62
  msg = message_from_message_data(data, app.outputs)
60
63
  if msg is not None:
61
64
  await app.publish(msg)
@@ -0,0 +1,29 @@
1
+ import asyncio
2
+ from datetime import timedelta
3
+ from typing import AsyncGenerator
4
+
5
+ from kelvin.message import CustomAction, CustomActionMsg
6
+ from kelvin.publisher import DataGenerator
7
+
8
+
9
+ class CustomActionGenerator(DataGenerator):
10
+ def __init__(self) -> None:
11
+ print("Hello from MyGenerator")
12
+
13
+ async def run(self) -> AsyncGenerator[CustomActionMsg, None]:
14
+ print("Running MyGenerator")
15
+
16
+ for i in range(10):
17
+ msg = CustomAction(
18
+ resource="krn:asset:test-asset-1",
19
+ expiration_date=timedelta(seconds=30),
20
+ type="example-in",
21
+ title="hello generator",
22
+ description="big description",
23
+ payload={"big": "payload"},
24
+ )
25
+
26
+ print("msg", msg)
27
+
28
+ yield msg
29
+ await asyncio.sleep(1)
@@ -0,0 +1,16 @@
1
+ # yaml-language-server: $schema=https://apps.kelvininc.com/schemas/kelvin/5.0.0/importer.json
2
+
3
+ spec_version: 5.0.0
4
+ type: importer
5
+
6
+ name: custom-action-example
7
+ title: Custom Action Example
8
+ description: Receive and send a custom action
9
+ version: 1.0.0
10
+
11
+ custom_actions:
12
+ inputs:
13
+ - type: example-in
14
+ outputs:
15
+ - type: example-out
16
+ - type: example-out-2
@@ -0,0 +1,80 @@
1
+ import asyncio
2
+ from datetime import timedelta
3
+
4
+ from kelvin.application import KelvinApp
5
+ from kelvin.krn import KRNAsset
6
+ from kelvin.message import CustomAction, Recommendation
7
+
8
+
9
+ class CustomActionApp:
10
+ def __init__(self, app: KelvinApp = KelvinApp()) -> None:
11
+ self.app = app
12
+ self.app.on_custom_action = self.on_custom_action
13
+
14
+ async def on_custom_action(self, action: CustomAction) -> None:
15
+ """Callback when a Custom Action is received."""
16
+ print("Received Custom Action: ", action)
17
+
18
+ await self.app.publish(action.result(success=True, message="Custom Action applied"))
19
+ # OR
20
+ # await self.app.publish(
21
+ # CustomActionAckMsg(
22
+ # resource=action.resource,
23
+ # payload={
24
+ # "id": action._msg.id,
25
+ # "success": True,
26
+ # "message": "Custom Action applied",
27
+ # },
28
+ # )
29
+ # )
30
+
31
+ async def publisher_task(self) -> None:
32
+ i = 0
33
+ while True:
34
+ await self.app.publish(
35
+ CustomAction(
36
+ resource=KRNAsset("test-asset-1"),
37
+ trace_id=f"trace-{i}",
38
+ type="example-out",
39
+ title=f"Test Action {i}",
40
+ description=f"This is the test action number {i}",
41
+ expiration_date=timedelta(seconds=30),
42
+ payload={"big": "payload", "action number": i},
43
+ )
44
+ )
45
+
46
+ await asyncio.sleep(1)
47
+
48
+ await self.app.publish(
49
+ Recommendation(
50
+ resource=KRNAsset("test-asset-1"),
51
+ type="test-recommendation",
52
+ description=f"This is the recommendation number {i}",
53
+ expiration_date=timedelta(minutes=5),
54
+ trace_id=f"rec-trace-{i}",
55
+ actions=[
56
+ CustomAction(
57
+ resource=KRNAsset("test-asset-1"),
58
+ type="example-out-2",
59
+ title=f"Test Action {i}",
60
+ description=f"This is the test action number {i}",
61
+ expiration_date=timedelta(minutes=5),
62
+ payload={"big": "payload", "action number": i},
63
+ )
64
+ ],
65
+ auto_accepted=True,
66
+ )
67
+ )
68
+
69
+ i += 1
70
+ await asyncio.sleep(5)
71
+
72
+ async def run(self) -> None:
73
+ """Run the app."""
74
+ await self.app.connect()
75
+ await self.publisher_task()
76
+
77
+
78
+ if __name__ == "__main__":
79
+ app = CustomActionApp()
80
+ asyncio.run(app.run())
@@ -16,7 +16,7 @@ from kelvin.krn import KRNAsset, KRNAssetDataStream
16
16
  from kelvin.logs import configure_logger, logger
17
17
  from kelvin.message import AssetDataMessage, ControlChangeStatus, KMessageType, KMessageTypeData, Message
18
18
  from kelvin.message.base_messages import Resource, RuntimeManifest
19
- from kelvin.message.msg_builders import MessageBuilder
19
+ from kelvin.message.msg_builders import CustomAction, MessageBuilder, convert_message
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from kelvin.application.window import HoppingWindow, RollingWindow, TumblingWindow
@@ -92,6 +92,8 @@ class KelvinApp:
92
92
  """ Callback when a control change is received. """
93
93
  self.on_control_status: Optional[Callable[[ControlChangeStatus], Awaitable[None]]] = None
94
94
  """ Callback when a control status is received. """
95
+ self.on_custom_action: Optional[Callable[[CustomAction], Awaitable[None]]] = None
96
+ """ Callback when a custom action is received. """
95
97
 
96
98
  self.on_asset_change: Optional[Callable[[Optional[AssetInfo], Optional[AssetInfo]], Awaitable[None]]] = None
97
99
  """ Callback when an asset is added, removed or changed.
@@ -294,18 +296,21 @@ class KelvinApp:
294
296
  if self.on_app_configuration is not None and self._config_received.is_set():
295
297
  await self.on_app_configuration(self.app_configuration)
296
298
 
297
- inputs = dict()
298
- outputs = dict()
299
+ inputs = {}
300
+ outputs = {}
299
301
  assets_in_manifest = set()
300
302
  for resource in msg.payload.resources:
301
- if resource.type != "asset":
303
+ # check resource is asset
304
+ if not isinstance(resource.resource, KRNAsset):
302
305
  continue
303
306
 
304
- assets_in_manifest.add(resource.name)
307
+ asset_name = resource.resource.asset
305
308
 
306
- self._last_asset_resources[resource.name] = resource
309
+ assets_in_manifest.add(asset_name)
310
+
311
+ self._last_asset_resources[asset_name] = resource
307
312
  asset_info = AssetInfo(
308
- name=resource.name, properties=resource.properties, parameters=resource.parameters, datastreams={}
313
+ name=asset_name, properties=resource.properties, parameters=resource.parameters, datastreams={}
309
314
  )
310
315
 
311
316
  for ds_name, datastream in resource.datastreams.items():
@@ -315,7 +320,7 @@ class KelvinApp:
315
320
 
316
321
  name = datastream.map_to if datastream.map_to else ds_name
317
322
  asset_info.datastreams[name] = ResourceDatastream(
318
- asset=KRNAsset(resource.name), # type: ignore
323
+ asset=resource.resource,
319
324
  io_name=name,
320
325
  access=datastream.access,
321
326
  owned=datastream.owned or False,
@@ -334,8 +339,8 @@ class KelvinApp:
334
339
  name=name, type=KMessageTypeData(manif_ds.primitive_type_name) # type: ignore
335
340
  )
336
341
 
337
- old_asset_info = self._assets.get(resource.name, None)
338
- self._assets[resource.name] = asset_info
342
+ old_asset_info = self._assets.get(asset_name, None)
343
+ self._assets[asset_name] = asset_info
339
344
 
340
345
  if self.on_asset_change is not None and self._config_received.is_set():
341
346
  await self.on_asset_change(asset_info, old_asset_info)
@@ -370,11 +375,17 @@ class KelvinApp:
370
375
  await self.on_control_status(msg) # type: ignore
371
376
  return
372
377
 
378
+ if self.on_custom_action is not None and filters.is_custom_action(msg):
379
+ converted = convert_message(msg)
380
+ await self.on_custom_action(converted) # type: ignore
381
+ return
382
+
373
383
  def _route_to_filters(self, msg: Message) -> None:
374
384
  for queue, func in self._filters:
375
385
  if func(msg) is True:
386
+ converted = convert_message(msg) or msg # convert to message builder
376
387
  # todo: check if the message is reference
377
- queue.put_nowait(msg)
388
+ queue.put_nowait(converted)
378
389
 
379
390
  def filter(self, func: filters.KelvinFilterType) -> Queue[Message]:
380
391
  """Creates a filter for the received Kelvin Messages based on a filter function.
@@ -6,6 +6,7 @@ from typing_extensions import TypeAlias
6
6
 
7
7
  from kelvin.krn import KRN, KRNAssetDataStream
8
8
  from kelvin.message import ControlChangeStatus, KMessageTypeData, Message
9
+ from kelvin.message.base_messages import CustomActionMsg
9
10
 
10
11
  KelvinFilterType: TypeAlias = Callable[[Message], bool]
11
12
 
@@ -62,3 +63,7 @@ def asset_equals(asset: Union[str, List[str]]) -> KelvinFilterType:
62
63
  return msg.resource.asset == asset
63
64
 
64
65
  return _check
66
+
67
+
68
+ def is_custom_action(msg: Message) -> bool:
69
+ return isinstance(msg, CustomActionMsg)
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from datetime import datetime, timedelta
2
+ from datetime import datetime, timedelta, timezone
3
3
  from typing import AsyncGenerator, Dict, List, Optional, Tuple
4
4
 
5
5
  from kelvin.message.primitives import AssetDataMessage
@@ -12,6 +12,9 @@ except ImportError as e:
12
12
  ) from e
13
13
 
14
14
 
15
+ UTC = timezone.utc
16
+
17
+
15
18
  def round_nearest_time(dt: datetime, round_to: Optional[timedelta] = None) -> datetime:
16
19
  """
17
20
  Rounds the given datetime to the nearest time interval as specified by self.round_to.
@@ -86,7 +89,7 @@ class BaseWindow:
86
89
  Initializes the dataframes for the specified assets and datastreams.
87
90
  """
88
91
  return {
89
- asset: pd.DataFrame(columns=self.datastreams, index=pd.DatetimeIndex([], name="timestamp"))
92
+ asset: pd.DataFrame(columns=self.datastreams, index=pd.DatetimeIndex([], name="timestamp", tz=UTC))
90
93
  for asset in self.assets
91
94
  }
92
95
 
@@ -97,12 +100,16 @@ class BaseWindow:
97
100
  Args:
98
101
  message (AssetDataMessage): The message to append.
99
102
  window_start (Optional[datetime]): The start time of the window to check against.
103
+ If supplied, it must be timezone-aware.
100
104
  """
101
105
  asset = message.resource.asset
102
106
  datastream = message.resource.data_stream
103
107
  value = message.payload
104
108
  timestamp = round_nearest_time(message.timestamp, self.round_to)
105
- timestamp = timestamp.replace(tzinfo=None) # Remove timezone information for DataFrame indexing
109
+ timestamp = timestamp.astimezone(UTC) # Ensure timestamp is in UTC
110
+
111
+ if window_start is not None:
112
+ window_start = window_start.astimezone(UTC)
106
113
 
107
114
  if asset not in self.assets:
108
115
  # Ignore messages for assets not in the list
@@ -192,16 +199,18 @@ class BaseTimeWindow(BaseWindow):
192
199
  """
193
200
  Streams data windows continuously by consuming messages and yielding appropriate windows.
194
201
 
202
+ Args:
203
+ window_start (Optional[datetime]): The start time of the window. Defaults to the current time.
204
+ If provided, it must be timezone-aware.
205
+
195
206
  Yields:
196
207
  AsyncGenerator[Tuple[str, pd.DataFrame], None]: Generator yielding asset and its respective DataFrame.
197
208
  """
198
- if window_start is None:
199
- window_start = datetime.now()
200
-
209
+ window_start = datetime.now(UTC) if window_start is None else window_start.astimezone(UTC)
201
210
  window_end = window_start + self.hop_size
202
211
 
203
212
  while True:
204
- remaining_time = (window_end - datetime.now()).total_seconds()
213
+ remaining_time = (window_end - datetime.now(UTC)).total_seconds()
205
214
  if remaining_time <= 0:
206
215
  for asset, df in self.dataframes.items():
207
216
  df.sort_index(inplace=True) # Sort to ensure chronological order
@@ -4,10 +4,10 @@ import json
4
4
  import os
5
5
  from enum import Enum
6
6
  from pathlib import Path
7
- from typing import Any, Literal, Optional
7
+ from typing import Any, List, Literal, Optional
8
8
 
9
9
  from pydantic import BaseModel, StringConstraints
10
- from typing_extensions import Annotated
10
+ from typing_extensions import Annotated, override
11
11
 
12
12
  NameDNS = Annotated[str, StringConstraints(pattern="^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")]
13
13
  VersionStr = Annotated[
@@ -16,6 +16,7 @@ VersionStr = Annotated[
16
16
  pattern="^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$"
17
17
  ),
18
18
  ]
19
+ CustomActionTypeStr = Annotated[str, StringConstraints(pattern="^[a-zA-Z0-9]([-_ .a-zA-Z0-9]*[a-zA-Z0-9])?$")]
19
20
 
20
21
 
21
22
  class ConfigError(Exception):
@@ -25,7 +26,8 @@ class ConfigError(Exception):
25
26
  class ConfigBaseModel(BaseModel):
26
27
  model_config = {"populate_by_name": True}
27
28
 
28
- def model_dump(
29
+ @override
30
+ def model_dump( # type: ignore[override]
29
31
  self,
30
32
  mode: Literal["json", "python"] | str = "json",
31
33
  by_alias: bool = True,
@@ -65,6 +67,15 @@ class PrimitiveTypes(str, Enum):
65
67
  object = "object"
66
68
 
67
69
 
70
+ class CustomActionDef(ConfigBaseModel):
71
+ type: CustomActionTypeStr
72
+
73
+
74
+ class CustomActionsIO(ConfigBaseModel):
75
+ inputs: List[CustomActionDef] = []
76
+ outputs: List[CustomActionDef] = []
77
+
78
+
68
79
  class AppBaseConfig(ConfigBaseModel):
69
80
  name: NameDNS
70
81
  title: str
@@ -5,17 +5,19 @@ from typing import Dict, List, Literal, Optional
5
5
 
6
6
  from pydantic import Field
7
7
 
8
- from kelvin.config.common import AppBaseConfig, AppTypes, ConfigBaseModel, read_schema_file
8
+ from kelvin.config.common import AppBaseConfig, AppTypes, ConfigBaseModel, CustomActionsIO, read_schema_file
9
9
 
10
10
  from .manifest import (
11
11
  AppDefaults,
12
12
  AppManifest,
13
+ CustomActionWay,
13
14
  DefaultsDefinition,
14
15
  DynamicIODefinition,
15
16
  DynamicIoOwnership,
16
17
  DynamicIoType,
17
18
  Flags,
18
19
  IOSchema,
20
+ ManifCustomAction,
19
21
  RuntimeUpdateFlags,
20
22
  SchemasDefinition,
21
23
  )
@@ -27,6 +29,7 @@ class RuntimeUpdateConfig(ConfigBaseModel):
27
29
 
28
30
  class ExporterFlags(ConfigBaseModel):
29
31
  enable_runtime_update: RuntimeUpdateConfig = RuntimeUpdateConfig()
32
+ resources_required: Optional[bool] = None
30
33
 
31
34
 
32
35
  class SchemasConfig(ConfigBaseModel):
@@ -52,6 +55,7 @@ class ExporterConfig(AppBaseConfig):
52
55
  exporter_io: List[ExporterIO] = []
53
56
  ui_schemas: SchemasConfig = SchemasConfig()
54
57
  defaults: DeploymentDefaults = DeploymentDefaults()
58
+ custom_actions: CustomActionsIO = CustomActionsIO()
55
59
 
56
60
  def to_manifest(self, read_schemas: bool = True, workdir: Path = Path(".")) -> AppManifest:
57
61
  return convert_exporter_to_manifest(self, read_schemas=read_schemas, workdir=workdir)
@@ -70,6 +74,10 @@ def convert_exporter_to_manifest(
70
74
  for io, schema_path in config.ui_schemas.io_configuration.items()
71
75
  ]
72
76
 
77
+ manif_custom_actions = [
78
+ ManifCustomAction(type=cai.type, way=CustomActionWay.input_ca) for cai in config.custom_actions.inputs
79
+ ] + [ManifCustomAction(type=cai.type, way=CustomActionWay.output_ca) for cai in config.custom_actions.outputs]
80
+
73
81
  return AppManifest(
74
82
  name=config.name,
75
83
  title=config.title,
@@ -80,6 +88,7 @@ def convert_exporter_to_manifest(
80
88
  flags=Flags(
81
89
  spec_version=config.spec_version,
82
90
  enable_runtime_update=RuntimeUpdateFlags(configuration=config.flags.enable_runtime_update.configuration),
91
+ resources_required=config.flags.resources_required,
83
92
  ),
84
93
  dynamic_io=[
85
94
  DynamicIODefinition(
@@ -94,4 +103,5 @@ def convert_exporter_to_manifest(
94
103
  defaults=DefaultsDefinition(
95
104
  app=AppDefaults(configuration=config.defaults.configuration), system=config.defaults.system
96
105
  ),
106
+ custom_actions=manif_custom_actions,
97
107
  )
@@ -5,17 +5,19 @@ from typing import Dict, List, Literal, Optional
5
5
 
6
6
  from pydantic import Field
7
7
 
8
- from kelvin.config.common import AppBaseConfig, AppTypes, ConfigBaseModel, read_schema_file
8
+ from kelvin.config.common import AppBaseConfig, AppTypes, ConfigBaseModel, CustomActionsIO, read_schema_file
9
9
 
10
10
  from .manifest import (
11
11
  AppDefaults,
12
12
  AppManifest,
13
+ CustomActionWay,
13
14
  DefaultsDefinition,
14
15
  DynamicIODefinition,
15
16
  DynamicIoOwnership,
16
17
  DynamicIoType,
17
18
  Flags,
18
19
  IOSchema,
20
+ ManifCustomAction,
19
21
  RuntimeUpdateFlags,
20
22
  SchemasDefinition,
21
23
  )
@@ -27,6 +29,7 @@ class RuntimeUpdateConfig(ConfigBaseModel):
27
29
 
28
30
  class ImporterFlags(ConfigBaseModel):
29
31
  enable_runtime_update: RuntimeUpdateConfig = RuntimeUpdateConfig()
32
+ resources_required: Optional[bool] = None
30
33
 
31
34
 
32
35
  class SchemasConfig(ConfigBaseModel):
@@ -53,6 +56,7 @@ class ImporterConfig(AppBaseConfig):
53
56
  importer_io: List[ImporterIO] = []
54
57
  ui_schemas: SchemasConfig = SchemasConfig()
55
58
  defaults: DeploymentDefaults = DeploymentDefaults()
59
+ custom_actions: CustomActionsIO = CustomActionsIO()
56
60
 
57
61
  def to_manifest(self, read_schemas: bool = True, workdir: Path = Path(".")) -> AppManifest:
58
62
  return convert_importer_to_manifest(self, read_schemas=read_schemas, workdir=workdir)
@@ -71,6 +75,10 @@ def convert_importer_to_manifest(
71
75
  for io, schema_path in config.ui_schemas.io_configuration.items()
72
76
  ]
73
77
 
78
+ manif_custom_actions = [
79
+ ManifCustomAction(type=cai.type, way=CustomActionWay.input_ca) for cai in config.custom_actions.inputs
80
+ ] + [ManifCustomAction(type=cai.type, way=CustomActionWay.output_ca) for cai in config.custom_actions.outputs]
81
+
74
82
  return AppManifest(
75
83
  name=config.name,
76
84
  title=config.title,
@@ -81,6 +89,7 @@ def convert_importer_to_manifest(
81
89
  flags=Flags(
82
90
  spec_version=config.spec_version,
83
91
  enable_runtime_update=RuntimeUpdateFlags(configuration=config.flags.enable_runtime_update.configuration),
92
+ resources_required=config.flags.resources_required,
84
93
  ),
85
94
  dynamic_io=[
86
95
  DynamicIODefinition(
@@ -95,4 +104,5 @@ def convert_importer_to_manifest(
95
104
  defaults=DefaultsDefinition(
96
105
  app=AppDefaults(configuration=config.defaults.configuration), system=config.defaults.system
97
106
  ),
107
+ custom_actions=manif_custom_actions,
98
108
  )
@@ -5,7 +5,7 @@ from typing import Any, Dict, List, Literal, Optional
5
5
 
6
6
  from pydantic import Field
7
7
 
8
- from kelvin.config.common import AppBaseConfig, ConfigBaseModel, VersionStr
8
+ from kelvin.config.common import AppBaseConfig, ConfigBaseModel, CustomActionTypeStr, VersionStr
9
9
  from kelvin.krn import KRN
10
10
  from kelvin.message import ParameterType
11
11
  from kelvin.message.msg_type import PrimitiveTypes
@@ -26,6 +26,7 @@ class Flags(ConfigBaseModel):
26
26
  spec_version: VersionStr
27
27
  enable_runtime_update: RuntimeUpdateFlags = RuntimeUpdateFlags()
28
28
  deployment: DeploymentFlags = DeploymentFlags()
29
+ resources_required: Optional[bool] = None
29
30
 
30
31
 
31
32
  class IOWay(str, Enum):
@@ -74,6 +75,16 @@ class DynamicIODefinition(ConfigBaseModel):
74
75
  data_types: List[str] = []
75
76
 
76
77
 
78
+ class CustomActionWay(str, Enum):
79
+ input_ca = "input-ca"
80
+ output_ca = "output-ca"
81
+
82
+
83
+ class ManifCustomAction(ConfigBaseModel):
84
+ type: CustomActionTypeStr
85
+ way: CustomActionWay
86
+
87
+
77
88
  class ParamDefinition(ConfigBaseModel):
78
89
  name: str
79
90
  title: Optional[str] = None
@@ -136,3 +147,4 @@ class AppManifest(AppBaseConfig):
136
147
  parameters: List[ParamDefinition] = []
137
148
  schemas: SchemasDefinition = SchemasDefinition()
138
149
  defaults: DefaultsDefinition = DefaultsDefinition()
150
+ custom_actions: List[ManifCustomAction] = []
@@ -3,18 +3,27 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, List, Literal, Optional
5
5
 
6
- from kelvin.config.common import AppBaseConfig, AppTypes, ConfigBaseModel, ConfigError, read_schema_file
6
+ from kelvin.config.common import (
7
+ AppBaseConfig,
8
+ AppTypes,
9
+ ConfigBaseModel,
10
+ ConfigError,
11
+ CustomActionsIO,
12
+ read_schema_file,
13
+ )
7
14
  from kelvin.message import ParameterType
8
15
  from kelvin.message.msg_type import PrimitiveTypes
9
16
 
10
17
  from .manifest import (
11
18
  AppDefaults,
12
19
  AppManifest,
20
+ CustomActionWay,
13
21
  DefaultsDefinition,
14
22
  Flags,
15
23
  IODatastreamMapping,
16
24
  IODefinition,
17
25
  IOWay,
26
+ ManifCustomAction,
18
27
  ParamDefinition,
19
28
  RuntimeUpdateFlags,
20
29
  SchemasDefinition,
@@ -75,6 +84,7 @@ class SmartAppConfig(AppBaseConfig):
75
84
  parameters: List[SmartAppParams] = []
76
85
  ui_schemas: SchemasConfig = SchemasConfig()
77
86
  defaults: DeploymentDefaults = DeploymentDefaults()
87
+ custom_actions: CustomActionsIO = CustomActionsIO()
78
88
 
79
89
  def to_manifest(self, read_schemas: bool = True, workdir: Path = Path(".")) -> AppManifest:
80
90
  return convert_smart_app_to_manifest(self, read_schemas=read_schemas, workdir=workdir)
@@ -147,6 +157,10 @@ def convert_smart_app_to_manifest(
147
157
  read_schema_file(workdir / config.ui_schemas.parameters) if config.ui_schemas.parameters else {}
148
158
  )
149
159
 
160
+ manif_custom_actions = [
161
+ ManifCustomAction(type=cai.type, way=CustomActionWay.input_ca) for cai in config.custom_actions.inputs
162
+ ] + [ManifCustomAction(type=cai.type, way=CustomActionWay.output_ca) for cai in config.custom_actions.outputs]
163
+
150
164
  return AppManifest(
151
165
  name=config.name,
152
166
  title=config.title,
@@ -162,6 +176,7 @@ def convert_smart_app_to_manifest(
162
176
  resource_properties=config.flags.enable_runtime_update.resource_properties,
163
177
  configuration=config.flags.enable_runtime_update.configuration,
164
178
  ),
179
+ resources_required=True,
165
180
  ),
166
181
  parameters=[
167
182
  ParamDefinition(name=p.name, data_type=p.data_type, default=config.defaults.parameters.get(p.name))
@@ -173,4 +188,5 @@ def convert_smart_app_to_manifest(
173
188
  app=AppDefaults(configuration=config.defaults.configuration, io_datastream_mapping=io_map),
174
189
  system=config.defaults.system,
175
190
  ),
191
+ custom_actions=manif_custom_actions,
176
192
  )