stinger-ipc 0.0.1__py3-none-any.whl → 0.0.2__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.
- {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/METADATA +3 -3
- stinger_ipc-0.0.2.dist-info/RECORD +48 -0
- stinger_ipc-0.0.2.dist-info/entry_points.txt +4 -0
- stingeripc/args.py +6 -5
- stingeripc/asyncapi.py +249 -167
- stingeripc/components.py +301 -136
- stingeripc/connection.py +2 -1
- stingeripc/exceptions.py +1 -2
- stingeripc/interface.py +8 -4
- stingeripc/lang_symb.py +68 -0
- stingeripc/templates/cpp/CMakeLists.txt.jinja2 +26 -0
- stingeripc/templates/cpp/examples/client_main.cpp.jinja2 +47 -0
- stingeripc/templates/cpp/examples/server_main.cpp.jinja2 +35 -0
- stingeripc/templates/cpp/include/broker.hpp.jinja2 +132 -0
- stingeripc/templates/cpp/include/client.hpp.jinja2 +53 -0
- stingeripc/templates/cpp/include/enums.hpp.jinja2 +17 -0
- stingeripc/templates/cpp/include/ibrokerconnection.hpp.jinja2 +42 -0
- stingeripc/templates/cpp/include/return_types.hpp.jinja2 +14 -0
- stingeripc/templates/cpp/include/server.hpp.jinja2 +44 -0
- stingeripc/templates/cpp/include/structs.hpp.jinja2 +13 -0
- stingeripc/templates/cpp/src/broker.cpp.jinja2 +243 -0
- stingeripc/templates/cpp/src/client.cpp.jinja2 +202 -0
- stingeripc/templates/cpp/src/server.cpp.jinja2 +170 -0
- stingeripc/templates/markdown/index.md.jinja2 +142 -0
- stingeripc/templates/python/__init__.py.jinja2 +1 -0
- stingeripc/templates/python/client.py.jinja2 +309 -0
- stingeripc/templates/python/connection.py.jinja2 +164 -0
- stingeripc/templates/python/interface_types.py.jinja2 +48 -0
- stingeripc/templates/python/method_codes.py.jinja2 +30 -0
- stingeripc/templates/python/pyproject.toml.jinja2 +9 -0
- stingeripc/templates/python/server.py.jinja2 +214 -0
- stingeripc/templates/rust/Cargo.toml.jinja2 +4 -0
- stingeripc/templates/rust/client/Cargo.toml.jinja2 +25 -0
- stingeripc/templates/rust/client/examples/client.rs.jinja2 +53 -0
- stingeripc/templates/rust/client/src/lib.rs.jinja2 +247 -0
- stingeripc/templates/rust/connection/Cargo.toml.jinja2 +21 -0
- stingeripc/templates/rust/connection/examples/pub_and_recv.rs.jinja2 +44 -0
- stingeripc/templates/rust/connection/src/handler.rs.jinja2 +0 -0
- stingeripc/templates/rust/connection/src/lib.rs.jinja2 +262 -0
- stingeripc/templates/rust/connection/src/payloads.rs.jinja2 +131 -0
- stingeripc/templates/rust/server/Cargo.toml.jinja2 +19 -0
- stingeripc/templates/rust/server/examples/server.rs.jinja2 +83 -0
- stingeripc/templates/rust/server/src/lib.rs.jinja2 +272 -0
- stingeripc/topic.py +11 -8
- stinger_ipc-0.0.1.dist-info/RECORD +0 -13
- {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/WHEEL +0 -0
- {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/licenses/LICENSE +0 -0
- {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
# _{{stinger.name}}_ API Overview
|
2
|
+
|
3
|
+
{{stinger.description}}
|
4
|
+
{%macro argrow(arg) -%}
|
5
|
+
|{{arg.name|center(15)}}|
|
6
|
+
{%-if arg.arg_type.name.lower() == "enum" or arg.arg_type.name.lower() == "struct"%}{{arg.markdown_type|center(10)}}
|
7
|
+
{%-else%}{{arg.json_type|center(10)}}
|
8
|
+
{%-endif%}{%if arg.optional%} (optional){%endif%}|
|
9
|
+
{{- arg.description or ''}}|
|
10
|
+
{%- endmacro %}
|
11
|
+
{%macro argtable(arglist) -%}
|
12
|
+
| Name | Type |Description|
|
13
|
+
|---------------|----------|-----------|
|
14
|
+
{%-for arg in arglist%}
|
15
|
+
{{argrow(arg)}}
|
16
|
+
{%- endfor %}
|
17
|
+
{%- endmacro %}
|
18
|
+
{%if stinger.signals | length > 0 %}
|
19
|
+
## Signals
|
20
|
+
|
21
|
+
Signals are messages from the server to clients.
|
22
|
+
|
23
|
+
```plantuml
|
24
|
+
@startuml
|
25
|
+
Client <<- Server : Signal(Parameters)
|
26
|
+
@enduml
|
27
|
+
```
|
28
|
+
{%for sig_name, signal in stinger.signals.items()%}
|
29
|
+
### Signal `{{sig_name}}`
|
30
|
+
|
31
|
+
{{signal.documentation or "_No documentation for this signal_"}}
|
32
|
+
|
33
|
+
#### Signal Parameters for `{{sig_name}}`
|
34
|
+
|
35
|
+
{{argtable(signal.arg_list)}}
|
36
|
+
|
37
|
+
#### Code Examples
|
38
|
+
|
39
|
+
<details>
|
40
|
+
<summary>Python Client</summary>
|
41
|
+
|
42
|
+
```python
|
43
|
+
@client.receive_{{sig_name | snake_case }}
|
44
|
+
def on_{{sig_name | snake_case }}({%for arg in signal.arg_list%}{{arg.name}}: {{arg.python_type}}{%if not loop.last%}, {%endif%}{%endfor%}):
|
45
|
+
print(f"Got a '{{sig_name}}' signal: {%for arg in signal.arg_list%}{{arg.name}}={ {{arg.name}} } {%endfor%}")
|
46
|
+
```
|
47
|
+
|
48
|
+
</details>
|
49
|
+
|
50
|
+
<details>
|
51
|
+
<summary>Rust Server</summary>
|
52
|
+
|
53
|
+
```rust
|
54
|
+
server.emit_{{sig_name|snake_case}}({%for arg in signal.arg_list%}{{arg.get_random_example_value(lang="rust")}}{%if not loop.last%}, {%endif%}{%endfor%}).await;
|
55
|
+
```
|
56
|
+
|
57
|
+
</details>
|
58
|
+
|
59
|
+
{%endfor%}{#- end for each signal -#}
|
60
|
+
{%endif%}{#- end if there are signals -#}
|
61
|
+
|
62
|
+
{#- ------------------------------------------------------- -#}
|
63
|
+
{%if stinger.methods | length > 0 %}
|
64
|
+
## Methods
|
65
|
+
|
66
|
+
Methods are requests from a client to a server and the server provides a response back to the client:
|
67
|
+
|
68
|
+
```plantuml
|
69
|
+
@startuml
|
70
|
+
Client ->> Server : Request(Parameters)
|
71
|
+
Client <<-- Server: Response(Parameters)
|
72
|
+
@enduml
|
73
|
+
```
|
74
|
+
|
75
|
+
{%for method_name, method in stinger.methods.items()%}
|
76
|
+
### Method `{{method_name}}`
|
77
|
+
|
78
|
+
{{method.documentation or "_No documentation for this method_"}}
|
79
|
+
|
80
|
+
#### Request Parameters
|
81
|
+
{%if method.arg_list | length == 0%}
|
82
|
+
There are no arguments for this request.
|
83
|
+
{%else-%}
|
84
|
+
{{argtable(method.arg_list)}}
|
85
|
+
{%endif%}{# number of args #}
|
86
|
+
#### Return Parameters
|
87
|
+
|
88
|
+
{%if method.return_value_type == "struct" %}
|
89
|
+
{{argtable(method.return_value)}}
|
90
|
+
{%elif method.return_value_type is false -%}
|
91
|
+
There is no return value for this method call.
|
92
|
+
{%else-%}
|
93
|
+
The return value type is `{{method.return_value.json_type}}`.
|
94
|
+
{%endif-%}
|
95
|
+
{%endfor-%}
|
96
|
+
{%endif%}
|
97
|
+
{#- ------------------------------------------------------- #}
|
98
|
+
{%if stinger.properties | length > 0 %}## Properties
|
99
|
+
|
100
|
+
Properties are values (or a set of values) held by the server. They are re-published when the value changes.
|
101
|
+
|
102
|
+
```plantuml
|
103
|
+
@startuml
|
104
|
+
Server -> Server : Set Property
|
105
|
+
Client <<- Server: Property Updated
|
106
|
+
@enduml
|
107
|
+
```
|
108
|
+
{%for prop_name, prop in stinger.properties.items()%}
|
109
|
+
### Property `{{prop_name}}`
|
110
|
+
|
111
|
+
{{prop.documentation or "_No documentation is available for this property_"}}{%if prop.read_only %}
|
112
|
+
|
113
|
+
This property is **read-only**. It can only be modified by the server.{%endif%}
|
114
|
+
|
115
|
+
{{argtable(prop.arg_list)}}
|
116
|
+
{%endfor%}{# for each property #}
|
117
|
+
{%endif%}{#end condition that there are a number of properties#}
|
118
|
+
{%if stinger.enums | length > 0%}## Enums
|
119
|
+
{%endif%}
|
120
|
+
{%for ie_name, ie in stinger.enums.items() %}
|
121
|
+
{#- ------------------------------------------------------- -#}
|
122
|
+
### Enum `{{ie.class_name }}`
|
123
|
+
|
124
|
+
<a name="Enum-{{ie.class_name}}"></a>{{ie.description or "_No description exists for this enumeration._"}}
|
125
|
+
|
126
|
+
{%for value in ie.values -%}
|
127
|
+
* {{value}} ({{loop.index}})
|
128
|
+
{%- if ie.value_descriptions and ie.value_descriptions|length >= loop.index %}
|
129
|
+
- {{ie.value_descriptions[loop.index0]}}{% endif %}
|
130
|
+
{%endfor%}
|
131
|
+
{%endfor%}
|
132
|
+
{#- ------------------------------------------------------- #}
|
133
|
+
## Structures
|
134
|
+
|
135
|
+
Structures are a group of values and may be used as an argument in signals, methods, or properties. Defining a structure allows for easy reuse.
|
136
|
+
{%for istruct_name, istruct in stinger.structs.items() %}
|
137
|
+
### Struct `{{istruct.class_name }}`
|
138
|
+
|
139
|
+
<a name="Struct-{{istruct.class_name}}"></a>{{istruct.description or "_No general description exists for this structure_"}}
|
140
|
+
|
141
|
+
{{argtable(istruct.members)}}
|
142
|
+
{%endfor%}
|
@@ -0,0 +1 @@
|
|
1
|
+
# Nothing to see here.
|
@@ -0,0 +1,309 @@
|
|
1
|
+
"""
|
2
|
+
DO NOT MODIFY THIS FILE. {# Unless you see this, which means you are editing the template. #} It is automatically generated and changes will be over-written
|
3
|
+
on the next generation.
|
4
|
+
|
5
|
+
This is the Client for the {{stinger.name}} interface.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Dict, Callable, List, Any
|
9
|
+
from uuid import uuid4
|
10
|
+
from functools import partial
|
11
|
+
import json
|
12
|
+
import logging
|
13
|
+
{%if stinger.methods | length > 0 %}
|
14
|
+
import asyncio
|
15
|
+
import concurrent.futures as futures
|
16
|
+
from method_codes import *
|
17
|
+
{%endif%}
|
18
|
+
from connection import BrokerConnection
|
19
|
+
{%if stinger.uses_enums()%}import {{stinger.get_enum_module_name()}} as {{stinger.get_enum_module_alias()}}{%endif%}
|
20
|
+
|
21
|
+
logging.basicConfig(level=logging.DEBUG)
|
22
|
+
|
23
|
+
{%for sig_name, sig in stinger.signals.items()-%}
|
24
|
+
{{sig_name | UpperCamelCase}}SignalCallbackType = Callable[[{%for arg in sig.arg_list%}{{arg.python_type}}{%if not loop.last%}, {%endif%}{%endfor%}], None]
|
25
|
+
{%endfor%}
|
26
|
+
{%-for method_name, method in stinger.methods.items()-%}
|
27
|
+
{{method_name | UpperCamelCase}}MethodResponseCallbackType = Callable[[{%if method.return_value%}{{method.return_value.python_type}}{%endif%}], None]
|
28
|
+
{%endfor%}
|
29
|
+
{%for prop_name, prop in stinger.properties.items()-%}
|
30
|
+
{{prop_name | UpperCamelCase}}PropertyUpdatedCallbackType = Callable[[{{prop.python_class}}], None]
|
31
|
+
{%endfor%}
|
32
|
+
|
33
|
+
class {{stinger.python.client_class_name}}:
|
34
|
+
|
35
|
+
def __init__(self, connection: BrokerConnection):
|
36
|
+
""" Constructor for a `{{stinger.python.client_class_name}}` object.
|
37
|
+
"""
|
38
|
+
self._logger = logging.getLogger('{{stinger.python.client_class_name}}')
|
39
|
+
self._logger.setLevel(logging.DEBUG)
|
40
|
+
self._logger.debug("Initializing {{stinger.python.client_class_name}}")
|
41
|
+
self._client_id = str(uuid4())
|
42
|
+
self._conn = connection
|
43
|
+
self._conn.set_message_callback(self._receive_message)
|
44
|
+
{%if stinger.methods | length > 0 %}
|
45
|
+
self._pending_method_responses: dict[str, Callable[..., None]] = {}
|
46
|
+
{%endif%}
|
47
|
+
{%for prop_name, prop_spec in stinger.properties.items()-%}
|
48
|
+
self._property_{{prop_name}}: {{prop_spec.python_class}}|None = None
|
49
|
+
self._{{prop_name}}_prop_subscription_id: int = self._conn.subscribe("{{prop_spec.value_topic}}")
|
50
|
+
self._changed_value_callbacks_for_{{prop_name}}: list[{{prop_name | UpperCamelCase}}PropertyUpdatedCallbackType] = []
|
51
|
+
{%endfor-%}
|
52
|
+
{%-for sig_name in stinger.signals.keys()-%}
|
53
|
+
self._signal_recv_callbacks_for_{{sig_name | snake_case }}: list[{{sig_name | UpperCamelCase}}SignalCallbackType] = []
|
54
|
+
{%endfor-%}
|
55
|
+
{%-for method_name, method in stinger.methods.items()-%}
|
56
|
+
self._{{method_name}}_method_call_subscription_id: int = self._conn.subscribe(f"{{method.response_topic('{self._client_id}')}}")
|
57
|
+
{%endfor%}
|
58
|
+
|
59
|
+
{%for prop_name, prop_spec in stinger.properties.items()-%}
|
60
|
+
@property
|
61
|
+
def {{prop_name}}(self) -> {{prop_spec.python_class}} | None:
|
62
|
+
""" Property '{{prop_name}}' getter.
|
63
|
+
"""
|
64
|
+
return self._property_{{prop_name}}
|
65
|
+
{%if not prop_spec.read_only %}
|
66
|
+
@{{prop_name}}.setter
|
67
|
+
def {{prop_name}}(self, value: {{prop_spec.python_class}}):
|
68
|
+
""" Serializes and publishes the '{{prop_name}}' property.
|
69
|
+
"""
|
70
|
+
if not isinstance(value, {{prop_spec.python_class}}):
|
71
|
+
raise ValueError("The '{{prop_name}}' property must be a {{prop_spec.python_class}}")
|
72
|
+
{%if prop_spec.arg_list | length > 1 -%}
|
73
|
+
serialized = value.model_dump_json(exclude_none=True)
|
74
|
+
{%else-%}
|
75
|
+
serialized = json.dumps({ "{{prop_spec.arg_list[0].name}}": value.{{prop_spec.arg_list[0].name}} })
|
76
|
+
{%endif-%}
|
77
|
+
self._logger.debug("Setting '{{prop_name}}' property to %s", serialized)
|
78
|
+
self._conn.publish("{{prop_spec.update_topic}}", serialized, qos=1)
|
79
|
+
{%endif%}
|
80
|
+
def {{prop_name | snake_case}}_changed(self, handler: {{prop_name | UpperCamelCase}}PropertyUpdatedCallbackType, call_immediately: bool=False):
|
81
|
+
""" Sets a callback to be called when the '{{prop_name}}' property changes.
|
82
|
+
Can be used as a decorator.
|
83
|
+
"""
|
84
|
+
self._changed_value_callbacks_for_{{prop_name}}.append(handler)
|
85
|
+
if call_immediately and self._property_{{prop_name}} is not None:
|
86
|
+
handler(self._property_{{prop_name}})
|
87
|
+
return handler
|
88
|
+
|
89
|
+
{%endfor%}
|
90
|
+
|
91
|
+
def _do_callbacks_for(self, callbacks: List[Callable[..., None]], **kwargs):
|
92
|
+
""" Call each callback in the callback dictionary with the provided args.
|
93
|
+
"""
|
94
|
+
for cb in callbacks:
|
95
|
+
cb(**kwargs)
|
96
|
+
|
97
|
+
@staticmethod
|
98
|
+
def _filter_for_args(args: Dict[str, Any], allowed_args: List[str]) -> Dict[str, Any]:
|
99
|
+
""" Given a dictionary, reduce the dictionary so that it only has keys in the allowed list.
|
100
|
+
"""
|
101
|
+
filtered_args = {}
|
102
|
+
for k, v in args.items():
|
103
|
+
if k in allowed_args:
|
104
|
+
filtered_args[k] = v
|
105
|
+
return filtered_args
|
106
|
+
|
107
|
+
def _receive_message(self, topic: str, payload: str, properties: Dict[str, Any]):
|
108
|
+
""" New MQTT messages are passed to this method, which, based on the topic,
|
109
|
+
calls the appropriate handler method for the message.
|
110
|
+
"""
|
111
|
+
self._logger.debug("Receiving message sent to %s", topic)
|
112
|
+
{%if stinger.signals | length > 0 -%}
|
113
|
+
{%for sig_name, sig in stinger.signals.items()-%}
|
114
|
+
# Handle '{{sig_name}}' signal.
|
115
|
+
{%if not loop.first%}el{%endif%}if self._conn.is_topic_sub(topic, "{{sig.topic}}"):
|
116
|
+
if 'ContentType' not in properties or properties['ContentType'] != 'application/json':
|
117
|
+
self._logger.warning("Received '{{sig_name}}' signal with non-JSON content type")
|
118
|
+
return
|
119
|
+
allowed_args = [{%for arg in sig.arg_list%}"{{arg.name}}", {%endfor%}]
|
120
|
+
kwargs = self._filter_for_args(json.loads(payload), allowed_args)
|
121
|
+
{%for arg in sig.arg_list-%}
|
122
|
+
kwargs["{{arg.name}}"] = {{arg.python_class}}(kwargs["{{arg.name}}"]){%if arg.optional %} if kwargs.get("{{arg.name}}") else None {%endif%}
|
123
|
+
{%endfor%}
|
124
|
+
self._do_callbacks_for(self._signal_recv_callbacks_for_{{sig_name | snake_case}}, **kwargs)
|
125
|
+
{%endfor%}{# end signal loop #}
|
126
|
+
|
127
|
+
{%-else%}pass{%-endif%}
|
128
|
+
{%-for method_name, method in stinger.methods.items()%}
|
129
|
+
# Handle '{{method_name}}' method response.
|
130
|
+
if self._conn.is_topic_sub(topic, f"{{method.response_topic('{self._client_id}')}}"):
|
131
|
+
result_code = MethodResultCode.SUCCESS
|
132
|
+
if "UserProperty" in properties:
|
133
|
+
user_properties = properties["UserProperty"]
|
134
|
+
if "DebugInfo" in user_properties:
|
135
|
+
self._logger.info("Received Debug Info: %s", user_properties["DebugInfo"])
|
136
|
+
if "ReturnValue" in user_properties:
|
137
|
+
result_code = MethodResultCode(int(user_properties["ReturnValue"]))
|
138
|
+
response = json.loads(payload)
|
139
|
+
if "CorrelationData" in properties:
|
140
|
+
correlation_id = properties["CorrelationData"].decode()
|
141
|
+
if correlation_id in self._pending_method_responses:
|
142
|
+
cb = self._pending_method_responses[correlation_id]
|
143
|
+
del self._pending_method_responses[correlation_id]
|
144
|
+
cb(response, result_code)
|
145
|
+
else:
|
146
|
+
self._logger.warning("Correlation id %s was not in the list of pending method responses... %s", correlation_id, [k for k in self._pending_method_responses.keys()])
|
147
|
+
else:
|
148
|
+
self._logger.warning("No correlation data in properties sent to %s... %s", topic, [s for s in properties.keys()])
|
149
|
+
{%endfor%}{# end method loop #}
|
150
|
+
{%-for prop_name, prop_spec in stinger.properties.items()%}
|
151
|
+
# Handle '{{prop_name}}' property change.
|
152
|
+
{%if not loop.first%}el{%endif%}if self._conn.is_topic_sub(topic, "{{prop_spec.value_topic}}"):
|
153
|
+
if 'ContentType' not in properties or properties['ContentType'] != 'application/json':
|
154
|
+
self._logger.warning("Received '{{prop_name}}' property change with non-JSON content type")
|
155
|
+
return
|
156
|
+
try:
|
157
|
+
{%if prop_spec.arg_list | length > 1 -%}
|
158
|
+
prop_value = {{prop_spec.python_class}}.model_validate_json(payload)
|
159
|
+
{%else-%}
|
160
|
+
payload_obj = json.loads(payload)
|
161
|
+
prop_value = {{prop_spec.arg_list[0].python_class}}(payload_obj["{{prop_spec.arg_list[0].name}}"])
|
162
|
+
{%endif-%}
|
163
|
+
self._property_{{prop_name}} = prop_value
|
164
|
+
self._do_callbacks_for(self._changed_value_callbacks_for_{{prop_name}}, value=self._property_{{prop_name}})
|
165
|
+
except Exception as e:
|
166
|
+
self._logger.error("Error processing '{{prop_name}}' property change: %s", e)
|
167
|
+
{%endfor%}{# end property loop #}
|
168
|
+
|
169
|
+
{%for sig_name, sig in stinger.signals.items()%}
|
170
|
+
def receive_{{sig_name | snake_case }}(self, handler: {{sig_name | UpperCamelCase}}SignalCallbackType):
|
171
|
+
""" Used as a decorator for methods which handle particular signals.
|
172
|
+
"""
|
173
|
+
self._signal_recv_callbacks_for_{{sig_name | snake_case }}.append(handler)
|
174
|
+
if len(self._signal_recv_callbacks_for_{{sig_name | snake_case}}) == 1:
|
175
|
+
self._conn.subscribe("{{sig.topic}}")
|
176
|
+
return handler
|
177
|
+
{%endfor%}
|
178
|
+
|
179
|
+
{%for method_name, method in stinger.methods.items()%}
|
180
|
+
def {{method_name | snake_case}}(self, {%for arg in method.arg_list%}{{arg.name}}: {{arg.python_type}}{%if not loop.last%}, {%endif%}{%endfor%}) -> futures.Future:
|
181
|
+
""" Calling this initiates a `{{method_name}}` IPC method call.
|
182
|
+
"""
|
183
|
+
{%for arg in method.arg_list%}
|
184
|
+
if not isinstance({{arg.name}}, {{arg.python_class}}){%- if not arg.optional -%} and {{arg.name}} is not None{%endif-%}:
|
185
|
+
raise ValueError("The '{{arg.name}}' argument wasn't a {{arg.python_type}}")
|
186
|
+
{%endfor%}
|
187
|
+
fut = futures.Future() # type: futures.Future
|
188
|
+
correlation_id = str(uuid4())
|
189
|
+
self._pending_method_responses[correlation_id] = partial(self._handle_{{method_name | snake_case}}_response, fut)
|
190
|
+
payload = { {%-for arg in method.arg_list%}
|
191
|
+
"{{arg.name}}": {{arg.name}},{%endfor%}
|
192
|
+
}
|
193
|
+
self._conn.publish("{{method.topic}}", json.dumps(payload), qos=2, retain=False,
|
194
|
+
correlation_id=correlation_id, response_topic=f"{{method.response_topic('{self._client_id}')}}")
|
195
|
+
return fut
|
196
|
+
|
197
|
+
def _handle_{{method_name | snake_case}}_response(self, fut: futures.Future, response_json: Dict[str, Any], return_value: MethodResultCode):
|
198
|
+
""" This called with the response to a `{{method_name}}` IPC method call.
|
199
|
+
"""
|
200
|
+
self._logger.debug("Handling {{method_name | snake_case}} response message %s", fut)
|
201
|
+
try:
|
202
|
+
if return_value != MethodResultCode.SUCCESS.value:
|
203
|
+
raise stinger_exception_factory(return_value, response_json['debugResultMessage'] if 'debugResultMessage' in response_json else None)
|
204
|
+
{%if method.return_value is none%}
|
205
|
+
fut.set_result(None)
|
206
|
+
{%elif method.return_value_type == 'struct' %}
|
207
|
+
return_args = self._filter_for_args(response_json, [{%for m in method.return_value%}"{{m.name}}", {%endfor%}])
|
208
|
+
{%for arg in method.return_value-%}
|
209
|
+
return_args["{{arg.name}}"] = {{arg.python_type}}(return_args["{{arg.name}}"])
|
210
|
+
{%endfor%}
|
211
|
+
return_obj = {{method.return_value_python_type}}(**return_args)
|
212
|
+
fut.set_result(return_obj)
|
213
|
+
{%else%}
|
214
|
+
if "{{method.return_value_property_name}}" in response_json:
|
215
|
+
if not isinstance(response_json["{{method.return_value_property_name}}"], {{method.return_value.python_type}}):
|
216
|
+
raise ValueError("Return value '{{method.return_value_property_name}}'' had wrong type")
|
217
|
+
self._logger.debug("Setting future result")
|
218
|
+
fut.set_result(response_json["{{method.return_value_property_name}}"])
|
219
|
+
else:
|
220
|
+
raise Exception("Response message didn't have the return value")
|
221
|
+
{%endif%}
|
222
|
+
except Exception as e:
|
223
|
+
self._logger.info("Exception while handling {{method_name | snake_case}}", exc_info=e)
|
224
|
+
fut.set_exception(e)
|
225
|
+
if not fut.done():
|
226
|
+
fut.set_exception(Exception("No return value set"))
|
227
|
+
{%endfor%}
|
228
|
+
|
229
|
+
class {{stinger.python.client_class_name}}Builder:
|
230
|
+
|
231
|
+
def __init__(self, broker: BrokerConnection):
|
232
|
+
""" Creates a new {{stinger.python.client_class_name}}Builder.
|
233
|
+
"""
|
234
|
+
self._conn = broker
|
235
|
+
self._logger = logging.getLogger('{{stinger.python.client_class_name}}Builder')
|
236
|
+
{%for sig_name in stinger.signals.keys()-%}
|
237
|
+
self._signal_recv_callbacks_for_{{sig_name | snake_case}} = [] # type: List[{{sig_name | UpperCamelCase}}SignalCallbackType]
|
238
|
+
{%endfor-%}
|
239
|
+
{%for prop_name in stinger.properties.keys()-%}
|
240
|
+
self._property_updated_callbacks_for_{{prop_name | snake_case}}: list[{{prop_name | UpperCamelCase}}PropertyUpdatedCallbackType] = []
|
241
|
+
{%endfor-%}
|
242
|
+
|
243
|
+
{%for sig_name, sig in stinger.signals.items()%}
|
244
|
+
def receive_{{sig_name | snake_case }}(self, handler):
|
245
|
+
""" Used as a decorator for methods which handle particular signals.
|
246
|
+
"""
|
247
|
+
self._signal_recv_callbacks_for_{{sig_name | snake_case }}.append(handler)
|
248
|
+
{%endfor%}
|
249
|
+
|
250
|
+
{%for prop_name, prop in stinger.properties.items()%}
|
251
|
+
def {{prop_name|snake_case}}_updated(self, handler: {{prop_name | UpperCamelCase}}PropertyUpdatedCallbackType):
|
252
|
+
""" Used as a decorator for methods which handle updates to properties.
|
253
|
+
"""
|
254
|
+
self._property_updated_callbacks_for_{{prop_name | snake_case}}.append(handler)
|
255
|
+
{%endfor%}
|
256
|
+
|
257
|
+
def build(self) -> {{stinger.python.client_class_name}}:
|
258
|
+
""" Builds a new {{stinger.python.client_class_name}}.
|
259
|
+
"""
|
260
|
+
self._logger.debug("Building {{stinger.python.client_class_name}}")
|
261
|
+
client = {{stinger.python.client_class_name}}(self._conn)
|
262
|
+
{%for sig_name, sig in stinger.signals.items()%}
|
263
|
+
for cb in self._signal_recv_callbacks_for_{{sig_name | snake_case }}:
|
264
|
+
client.receive_{{sig_name | snake_case}}(cb)
|
265
|
+
{%endfor%}
|
266
|
+
{%for prop_name, prop in stinger.properties.items()%}
|
267
|
+
for cb in self._property_updated_callbacks_for_{{prop_name | snake_case}}:
|
268
|
+
client.{{prop_name | snake_case}}_changed(cb)
|
269
|
+
{%endfor%}
|
270
|
+
return client
|
271
|
+
|
272
|
+
|
273
|
+
if __name__ == '__main__':
|
274
|
+
import signal
|
275
|
+
|
276
|
+
{%set broker = stinger.get_example_broker()%}from connection import {{broker.class_name}}
|
277
|
+
conn = {{broker.class_name}}({%if broker.hostname is none%}'localhost', 1883{%endif%})
|
278
|
+
client_builder = {{stinger.python.client_class_name}}Builder(conn)
|
279
|
+
{%for sig_name, sig in stinger.signals.items()%}
|
280
|
+
@client_builder.receive_{{sig_name | snake_case }}
|
281
|
+
def print_{{sig_name}}_receipt({%for arg in sig.arg_list%}{{arg.name}}: {{arg.python_type}}{%if not loop.last%}, {%endif%}{%endfor%}):
|
282
|
+
"""{{sig.description}}
|
283
|
+
{%for arg in sig.arg_list-%}
|
284
|
+
@param {{arg.name}} {{arg.python_type}} {{arg.description or ''}}
|
285
|
+
{%endfor%}"""
|
286
|
+
print(f"Got a '{{sig_name}}' signal: {%for arg in sig.arg_list%}{{arg.name}}={ {{arg.name}} } {%endfor%}")
|
287
|
+
{%endfor%}
|
288
|
+
{%for prop_name, prop in stinger.properties.items()%}
|
289
|
+
@client_builder.{{prop_name | snake_case}}_updated
|
290
|
+
def print_new_{{prop_name}}_value(value: {{prop.python_class}}):
|
291
|
+
"""{{prop.description}}
|
292
|
+
"""
|
293
|
+
print(f"Property '{{prop_name}}' has been updated to: {value}")
|
294
|
+
{%endfor%}
|
295
|
+
|
296
|
+
client = client_builder.build()
|
297
|
+
{%if stinger.methods | length > 0 %}
|
298
|
+
{%-for method_name, method in stinger.methods.items()%}
|
299
|
+
print("Making call to '{{method_name|snake_case}}'")
|
300
|
+
future = client.{{method_name|snake_case}}({%for arg in method.arg_list%}{{arg.name}}={{arg.get_random_example_value()}}{%if not loop.last%}, {%endif%}{%endfor%})
|
301
|
+
try:
|
302
|
+
print(f"RESULT: {future.result(5)}")
|
303
|
+
except futures.TimeoutError:
|
304
|
+
print(f"Timed out waiting for response to '{{method_name|snake_case}}' call")
|
305
|
+
{%endfor%}
|
306
|
+
{%endif%}
|
307
|
+
|
308
|
+
print("Ctrl-C will stop the program.")
|
309
|
+
signal.pause()
|
@@ -0,0 +1,164 @@
|
|
1
|
+
|
2
|
+
import logging
|
3
|
+
from typing import Callable, Optional, Tuple, Any, Union
|
4
|
+
from paho.mqtt.client import Client as MqttClient, topic_matches_sub
|
5
|
+
from paho.mqtt.enums import MQTTProtocolVersion, CallbackAPIVersion
|
6
|
+
from paho.mqtt.properties import Properties as MqttProperties
|
7
|
+
from paho.mqtt.packettypes import PacketTypes
|
8
|
+
from queue import Queue, Empty
|
9
|
+
from abc import ABC, abstractmethod
|
10
|
+
from method_codes import *
|
11
|
+
|
12
|
+
|
13
|
+
logging.basicConfig(level=logging.DEBUG)
|
14
|
+
|
15
|
+
MessageCallback = Callable[[str, str, dict[str, Any]], None]
|
16
|
+
|
17
|
+
class BrokerConnection(ABC):
|
18
|
+
|
19
|
+
@abstractmethod
|
20
|
+
def publish(self, topic: str, msg: str, qos: int=1, retain: bool=False,
|
21
|
+
correlation_id: Union[str, bytes, None] = None, response_topic: Optional[str] = None,
|
22
|
+
return_value: Optional[MethodResultCode] = None, debug_info: Optional[str] = None):
|
23
|
+
pass
|
24
|
+
|
25
|
+
@abstractmethod
|
26
|
+
def subscribe(self, topic) -> int:
|
27
|
+
pass
|
28
|
+
|
29
|
+
@abstractmethod
|
30
|
+
def set_message_callback(self, callback: MessageCallback) -> None:
|
31
|
+
pass
|
32
|
+
|
33
|
+
@abstractmethod
|
34
|
+
def is_topic_sub(self, topic: str, sub: str) -> bool:
|
35
|
+
pass
|
36
|
+
|
37
|
+
@abstractmethod
|
38
|
+
def set_last_will(self, topic: str, payload: Optional[str]=None, qos: int=1, retain: bool=True):
|
39
|
+
pass
|
40
|
+
|
41
|
+
|
42
|
+
{%for broker in stinger.brokers.values()%}
|
43
|
+
class {{broker.class_name}}(BrokerConnection):
|
44
|
+
|
45
|
+
class PendingSubscription:
|
46
|
+
def __init__(self, topic: str, subscription_id: int):
|
47
|
+
self.topic = topic
|
48
|
+
self.subscription_id
|
49
|
+
|
50
|
+
def __init__(self{%if broker.hostname is none%}, host: str, port: int{%endif%}):
|
51
|
+
self._logger = logging.getLogger('Connection')
|
52
|
+
self._logger.setLevel(logging.DEBUG)
|
53
|
+
self._host: str = {%if broker.hostname is none %}host{%else%}"{{broker.hostname}}"{%endif%}
|
54
|
+
self._port: int = {%if broker.port is none %}port{%else%}{{broker.port}}{%endif%}
|
55
|
+
self._last_will: Optional[Tuple[str, Optional[str], int, bool]] = None
|
56
|
+
self._queued_messages = Queue() # type: Queue[Tuple[str, str, int, bool, MqttProperties]]
|
57
|
+
self._queued_subscriptions = Queue() # type: Queue[{{broker.class_name}}.PendingSubscription]
|
58
|
+
self._connected: bool = False
|
59
|
+
self._client = MqttClient(CallbackAPIVersion.VERSION2, protocol=MQTTProtocolVersion.MQTTv5)
|
60
|
+
self._client.on_connect = self._on_connect
|
61
|
+
self._client.on_message = self._on_message
|
62
|
+
self._client.connect(self._host, self._port)
|
63
|
+
self._message_callback: Optional[MessageCallback] = None
|
64
|
+
self._client.loop_start()
|
65
|
+
self._next_subscription_id = 10
|
66
|
+
|
67
|
+
def __del__(self):
|
68
|
+
if self._last_will is not None:
|
69
|
+
self._client.publish(*self._last_will).wait_for_publish()
|
70
|
+
self._client.disconnect()
|
71
|
+
self._client.loop_stop()
|
72
|
+
|
73
|
+
def get_next_subscription_id(self) -> int:
|
74
|
+
sub_id = self._next_subscription_id
|
75
|
+
self._next_subscription_id += 1
|
76
|
+
return sub_id
|
77
|
+
|
78
|
+
def set_last_will(self, topic: str, payload: Optional[str]=None, qos: int=1, retain: bool=True):
|
79
|
+
self._last_will = (topic, payload, qos, retain)
|
80
|
+
self._client.will_set(*self._last_will)
|
81
|
+
|
82
|
+
def set_message_callback(self, callback: MessageCallback):
|
83
|
+
self._message_callback = callback
|
84
|
+
|
85
|
+
def _on_message(self, client, userdata, msg):
|
86
|
+
if self._message_callback:
|
87
|
+
properties = msg.properties.__dict__ if hasattr(msg, 'properties') else {}
|
88
|
+
if "UserProperty" in properties:
|
89
|
+
properties["UserProperty"] = dict(properties["UserProperty"])
|
90
|
+
self._message_callback(msg.topic, msg.payload.decode(), properties)
|
91
|
+
|
92
|
+
def _on_connect(self, client, userdata, flags, reason_code, properties):
|
93
|
+
if reason_code == 0: # Connection successful
|
94
|
+
self._connected = True
|
95
|
+
self._logger.info("Connected to %s:%d", self._host, self._port)
|
96
|
+
while not self._queued_subscriptions.empty():
|
97
|
+
try:
|
98
|
+
pending_subscr = self._queued_subscriptions.get_nowait()
|
99
|
+
except Empty:
|
100
|
+
break
|
101
|
+
else:
|
102
|
+
self._logger.debug("Connected and subscribing to %s", pending_subscr.topic)
|
103
|
+
sub_props = MqttProperties(PacketTypes.SUBSCRIBE)
|
104
|
+
sub_props.SubscriptionIdentifier = pending_subscr.subscription_id
|
105
|
+
self._client.subscribe(pending_subscr.topic, qos=1, properties=sub_props)
|
106
|
+
while not self._queued_messages.empty():
|
107
|
+
try:
|
108
|
+
msg = self._queued_messages.get_nowait()
|
109
|
+
except Empty:
|
110
|
+
break
|
111
|
+
else:
|
112
|
+
self._logger.info(f"Publishing queued up message")
|
113
|
+
self._client.publish(*msg)
|
114
|
+
else:
|
115
|
+
self._logger.error("Connection failed with reason code %d", reason_code)
|
116
|
+
self._connected = False
|
117
|
+
|
118
|
+
|
119
|
+
def publish(self, topic: str, msg: str, qos: int=1, retain: bool=False,
|
120
|
+
correlation_id: Union[str, bytes, None] = None, response_topic: Optional[str] = None,
|
121
|
+
return_value: Optional[MethodResultCode] = None, debug_info: Optional[str] = None):
|
122
|
+
""" Publish a message to mqtt.
|
123
|
+
"""
|
124
|
+
properties = MqttProperties(PacketTypes.PUBLISH)
|
125
|
+
properties.ContentType = "application/json"
|
126
|
+
if isinstance(correlation_id, str):
|
127
|
+
properties.CorrelationData = correlation_id.encode('utf-8')
|
128
|
+
elif isinstance(correlation_id, bytes):
|
129
|
+
properties.CorrelationData = correlation_id
|
130
|
+
if response_topic is not None:
|
131
|
+
properties.ResponseTopic = response_topic
|
132
|
+
user_properties = []
|
133
|
+
if return_value is not None:
|
134
|
+
user_properties.append(("ReturnValue", str(return_value.value)))
|
135
|
+
if debug_info is not None:
|
136
|
+
user_properties.append(("DebugInfo", debug_info))
|
137
|
+
if len(user_properties) > 0:
|
138
|
+
properties.UserProperty = user_properties
|
139
|
+
if self._connected:
|
140
|
+
self._logger.info("Publishing %s", topic)
|
141
|
+
self._client.publish(topic, msg, qos, retain, properties)
|
142
|
+
else:
|
143
|
+
self._logger.info("Queueing %s for publishing later", topic)
|
144
|
+
self._queued_messages.put((topic, msg, qos, retain, properties))
|
145
|
+
|
146
|
+
def subscribe(self, topic: str) -> int:
|
147
|
+
""" Subscribes to a topic. If the connection is not established, the subscription is queued.
|
148
|
+
Returns the subscription ID.
|
149
|
+
"""
|
150
|
+
sub_id = self.get_next_subscription_id()
|
151
|
+
if self._connected:
|
152
|
+
self._logger.debug("Subscribing to %s", topic)
|
153
|
+
sub_props = MqttProperties(PacketTypes.SUBSCRIBE)
|
154
|
+
sub_props.SubscriptionIdentifier = sub_id
|
155
|
+
self._client.subscribe(topic, qos=1, properties=sub_props)
|
156
|
+
else:
|
157
|
+
self._logger.debug("Pending subscription to %s", topic)
|
158
|
+
self._queued_subscriptions.put(self.PendingSubscription(topic, sub_id))
|
159
|
+
return sub_id
|
160
|
+
|
161
|
+
def is_topic_sub(self, topic: str, sub: str) -> bool:
|
162
|
+
return topic_matches_sub(sub, topic)
|
163
|
+
|
164
|
+
{%endfor%}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"""
|
2
|
+
DO NOT MODIFY THIS FILE. It is automatically generated and changes will be over-written
|
3
|
+
on the next generation.
|
4
|
+
|
5
|
+
It contains enumerations used by the {{stinger.name}} interface.
|
6
|
+
"""
|
7
|
+
from pydantic import BaseModel
|
8
|
+
{%if stinger.uses_enums()%}from enum import IntEnum{%endif%}
|
9
|
+
|
10
|
+
{%for ie_name, ie in stinger.enums.items() %}
|
11
|
+
class {{ie_name | UpperCamelCase }}(IntEnum):
|
12
|
+
""" Interface enum `{{ie_name}}`."""
|
13
|
+
{%-for value in ie.values %}
|
14
|
+
{{value | CONST_CASE}} = {{loop.index}}
|
15
|
+
{%-endfor%}
|
16
|
+
{%endfor%}
|
17
|
+
|
18
|
+
{%-for istruct_name, istruct in stinger.structs.items() %}
|
19
|
+
class {{istruct_name | UpperCamelCase }}(BaseModel):
|
20
|
+
""" Interface struct `{{istruct_name}}`. """
|
21
|
+
{%-for arg in istruct.members%}
|
22
|
+
{{arg.name}}: {{arg.python_local_type}}
|
23
|
+
{%-endfor%}
|
24
|
+
{%endfor%}
|
25
|
+
|
26
|
+
{%-for method_name, method in stinger.methods.items()%}
|
27
|
+
{%-if method.return_value_type == 'struct'%}
|
28
|
+
class {{method.return_value_python_local_type}}(BaseModel):
|
29
|
+
""" Interface method `{{method_name}}` return value struct. """
|
30
|
+
{%-for arg in method.return_value%}
|
31
|
+
{{arg.name}}: {{arg.python_local_type}}
|
32
|
+
{%-endfor%}
|
33
|
+
{%endif-%}
|
34
|
+
{%endfor%}
|
35
|
+
|
36
|
+
{%-for prop_name, prop in stinger.properties.items()%}{%if prop.arg_list | length > 1%}
|
37
|
+
class {{prop.python_local_type}}(BaseModel):
|
38
|
+
""" Interface property `{{prop_name}}` (multi-value struct)."""
|
39
|
+
{%for arg in prop.arg_list%}
|
40
|
+
{%-if arg.arg_type.name.lower() == 'primitive' -%}
|
41
|
+
{{arg.name}}: {{arg.python_local_type}}
|
42
|
+
{%elif arg.arg_type.name.lower() == 'enum'-%}
|
43
|
+
{{arg.name}}: {{arg.python_local_type}}
|
44
|
+
{%elif arg.arg_type.name.lower() == 'struct'-%}
|
45
|
+
{{arg.name}}: {{arg.python_local_type}}
|
46
|
+
{%endif-%}
|
47
|
+
{%endfor%}
|
48
|
+
{%endif%}{%endfor%}
|