stinger-ipc 0.0.1__py3-none-any.whl → 0.0.3__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.
Files changed (52) hide show
  1. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.3.dist-info}/METADATA +4 -4
  2. stinger_ipc-0.0.3.dist-info/RECORD +52 -0
  3. stinger_ipc-0.0.3.dist-info/entry_points.txt +4 -0
  4. stingeripc/args.py +6 -5
  5. stingeripc/asyncapi.py +249 -167
  6. stingeripc/components.py +301 -136
  7. stingeripc/connection.py +2 -1
  8. stingeripc/exceptions.py +1 -2
  9. stingeripc/interface.py +8 -4
  10. stingeripc/lang_symb.py +68 -0
  11. stingeripc/templates/cpp/CMakeLists.txt.jinja2 +26 -0
  12. stingeripc/templates/cpp/examples/client_main.cpp.jinja2 +47 -0
  13. stingeripc/templates/cpp/examples/server_main.cpp.jinja2 +35 -0
  14. stingeripc/templates/cpp/include/broker.hpp.jinja2 +132 -0
  15. stingeripc/templates/cpp/include/client.hpp.jinja2 +53 -0
  16. stingeripc/templates/cpp/include/enums.hpp.jinja2 +17 -0
  17. stingeripc/templates/cpp/include/ibrokerconnection.hpp.jinja2 +42 -0
  18. stingeripc/templates/cpp/include/return_types.hpp.jinja2 +14 -0
  19. stingeripc/templates/cpp/include/server.hpp.jinja2 +44 -0
  20. stingeripc/templates/cpp/include/structs.hpp.jinja2 +13 -0
  21. stingeripc/templates/cpp/src/broker.cpp.jinja2 +243 -0
  22. stingeripc/templates/cpp/src/client.cpp.jinja2 +202 -0
  23. stingeripc/templates/cpp/src/server.cpp.jinja2 +170 -0
  24. stingeripc/templates/markdown/index.md.jinja2 +142 -0
  25. stingeripc/templates/python/__init__.py.jinja2 +1 -0
  26. stingeripc/templates/python/client.py.jinja2 +309 -0
  27. stingeripc/templates/python/connection.py.jinja2 +164 -0
  28. stingeripc/templates/python/interface_types.py.jinja2 +48 -0
  29. stingeripc/templates/python/method_codes.py.jinja2 +30 -0
  30. stingeripc/templates/python/pyproject.toml.jinja2 +9 -0
  31. stingeripc/templates/python/server.py.jinja2 +214 -0
  32. stingeripc/templates/rust/Cargo.toml.jinja2 +4 -0
  33. stingeripc/templates/rust/client/Cargo.toml.jinja2 +25 -0
  34. stingeripc/templates/rust/client/examples/client.rs.jinja2 +53 -0
  35. stingeripc/templates/rust/client/src/lib.rs.jinja2 +247 -0
  36. stingeripc/templates/rust/connection/Cargo.toml.jinja2 +21 -0
  37. stingeripc/templates/rust/connection/examples/pub_and_recv.rs.jinja2 +44 -0
  38. stingeripc/templates/rust/connection/src/handler.rs.jinja2 +0 -0
  39. stingeripc/templates/rust/connection/src/lib.rs.jinja2 +262 -0
  40. stingeripc/templates/rust/connection/src/payloads.rs.jinja2 +131 -0
  41. stingeripc/templates/rust/server/Cargo.toml.jinja2 +19 -0
  42. stingeripc/templates/rust/server/examples/server.rs.jinja2 +83 -0
  43. stingeripc/templates/rust/server/src/lib.rs.jinja2 +272 -0
  44. stingeripc/tools/__init__.py +0 -0
  45. stingeripc/tools/markdown_generator.py +25 -0
  46. stingeripc/tools/python_generator.py +41 -0
  47. stingeripc/tools/rust_generator.py +50 -0
  48. stingeripc/topic.py +11 -8
  49. stinger_ipc-0.0.1.dist-info/RECORD +0 -13
  50. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.3.dist-info}/WHEEL +0 -0
  51. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.3.dist-info}/licenses/LICENSE +0 -0
  52. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.3.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%}