1use std::{any::Any, fmt::Debug, rc::Rc, sync::LazyLock};
21
22use ahash::AHashMap;
23use nautilus_core::{
24 UUID4,
25 python::{to_pyruntime_err, to_pyvalue_err},
26};
27use nautilus_model::identifiers::TraderId;
28use pyo3::{Py, Python, prelude::*, types::PyBytes};
29use ustr::Ustr;
30
31use crate::{
32 enums::SerializationEncoding,
33 msgbus::{
34 self as msgbus_api, BusMessage, MessageBus, MessageBusBackingFactory, MessageBusConfig,
35 core::Subscription,
36 get_message_bus,
37 matching::is_matching,
38 mstr::{Endpoint, MStr, Pattern, Topic},
39 typed_handler::{Handler, ShareableMessageHandler, TypedHandler},
40 },
41 python::{
42 config_error_to_pyvalue_err,
43 factory::{FactoryExtractor, FactoryRegistry},
44 },
45};
46
47pub type MessageBusFactoryExtractor = FactoryExtractor<dyn MessageBusBackingFactory>;
49
50#[derive(Debug)]
52pub struct MessageBusFactoryRegistry {
53 inner: FactoryRegistry<dyn MessageBusBackingFactory>,
54}
55
56impl MessageBusFactoryRegistry {
57 #[must_use]
59 pub fn new() -> Self {
60 Self {
61 inner: FactoryRegistry::new("message bus factory"),
62 }
63 }
64
65 pub fn register(
72 &self,
73 type_name: String,
74 extractor: MessageBusFactoryExtractor,
75 ) -> anyhow::Result<()> {
76 self.inner.register(type_name, extractor)
77 }
78
79 pub fn extract(
86 &self,
87 py: Python<'_>,
88 factory: Py<PyAny>,
89 ) -> PyResult<Box<dyn MessageBusBackingFactory>> {
90 self.inner.extract(py, factory)
91 }
92}
93
94impl Default for MessageBusFactoryRegistry {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100static GLOBAL_MSGBUS_FACTORY_REGISTRY: LazyLock<MessageBusFactoryRegistry> =
101 LazyLock::new(MessageBusFactoryRegistry::new);
102
103#[must_use]
105pub fn get_global_msgbus_factory_registry() -> &'static MessageBusFactoryRegistry {
106 &GLOBAL_MSGBUS_FACTORY_REGISTRY
107}
108
109#[pymethods]
110#[pyo3_stub_gen::derive::gen_stub_pymethods]
111impl BusMessage {
112 #[getter]
113 #[pyo3(name = "topic")]
114 fn py_topic(&self) -> String {
115 self.topic.to_string()
116 }
117
118 #[getter]
119 #[pyo3(name = "payload_type")]
120 fn py_payload_type(&self) -> String {
121 self.payload_type.to_string()
122 }
123
124 #[getter]
125 #[pyo3(name = "payload")]
126 fn py_payload(&self, py: Python<'_>) -> Py<PyBytes> {
127 PyBytes::new(py, self.payload.as_ref()).into()
128 }
129
130 #[getter]
131 #[pyo3(name = "encoding")]
132 fn py_encoding(&self) -> SerializationEncoding {
133 self.encoding
134 }
135
136 fn __repr__(&self) -> String {
137 format!("{}('{}')", stringify!(BusMessage), self)
138 }
139
140 fn __str__(&self) -> String {
141 self.to_string()
142 }
143}
144
145#[pymethods]
146#[pyo3_stub_gen::derive::gen_stub_pymethods]
147impl MessageBusConfig {
148 #[new]
150 #[expect(clippy::too_many_arguments)]
151 #[pyo3(signature = (encoding=None, encoding_market_data=None, encoding_builtin=None, timestamps_as_iso8601=None, buffer_interval_ms=None, autotrim_mins=None, autotrim_maxlen=None, use_trader_prefix=None, use_trader_id=None, use_instance_id=None, streams_prefix=None, stream_per_topic=None, external_streams=None, types_filter=None, heartbeat_interval_secs=None))]
152 fn py_new(
153 encoding: Option<SerializationEncoding>,
154 encoding_market_data: Option<SerializationEncoding>,
155 encoding_builtin: Option<SerializationEncoding>,
156 timestamps_as_iso8601: Option<bool>,
157 buffer_interval_ms: Option<u32>,
158 autotrim_mins: Option<u32>,
159 autotrim_maxlen: Option<u32>,
160 use_trader_prefix: Option<bool>,
161 use_trader_id: Option<bool>,
162 use_instance_id: Option<bool>,
163 streams_prefix: Option<String>,
164 stream_per_topic: Option<bool>,
165 external_streams: Option<Vec<String>>,
166 types_filter: Option<Vec<String>>,
167 heartbeat_interval_secs: Option<u16>,
168 ) -> PyResult<Self> {
169 let default = Self::default();
170 let config = Self {
171 encoding: encoding.unwrap_or(default.encoding),
172 encoding_market_data,
173 encoding_builtin,
174 timestamps_as_iso8601: timestamps_as_iso8601.unwrap_or(default.timestamps_as_iso8601),
175 buffer_interval_ms,
176 autotrim_mins,
177 autotrim_maxlen,
178 use_trader_prefix: use_trader_prefix.unwrap_or(default.use_trader_prefix),
179 use_trader_id: use_trader_id.unwrap_or(default.use_trader_id),
180 use_instance_id: use_instance_id.unwrap_or(default.use_instance_id),
181 streams_prefix: streams_prefix.unwrap_or(default.streams_prefix),
182 stream_per_topic: stream_per_topic.unwrap_or(default.stream_per_topic),
183 external_streams,
184 types_filter,
185 heartbeat_interval_secs,
186 };
187
188 config.validate().map_err(config_error_to_pyvalue_err)?;
189 Ok(config)
190 }
191
192 fn __repr__(&self) -> String {
193 format!("{self:?}")
194 }
195
196 fn __str__(&self) -> String {
197 format!("{self:?}")
198 }
199
200 #[getter]
201 fn encoding(&self) -> SerializationEncoding {
202 self.encoding
203 }
204
205 #[getter]
206 fn encoding_market_data(&self) -> Option<SerializationEncoding> {
207 self.encoding_market_data
208 }
209
210 #[getter]
211 fn encoding_builtin(&self) -> Option<SerializationEncoding> {
212 self.encoding_builtin
213 }
214
215 #[getter]
216 fn timestamps_as_iso8601(&self) -> bool {
217 self.timestamps_as_iso8601
218 }
219
220 #[getter]
221 fn buffer_interval_ms(&self) -> Option<u32> {
222 self.buffer_interval_ms
223 }
224
225 #[getter]
226 fn autotrim_mins(&self) -> Option<u32> {
227 self.autotrim_mins
228 }
229
230 #[getter]
231 fn autotrim_maxlen(&self) -> Option<u32> {
232 self.autotrim_maxlen
233 }
234
235 #[getter]
236 fn use_trader_prefix(&self) -> bool {
237 self.use_trader_prefix
238 }
239
240 #[getter]
241 fn use_trader_id(&self) -> bool {
242 self.use_trader_id
243 }
244
245 #[getter]
246 fn use_instance_id(&self) -> bool {
247 self.use_instance_id
248 }
249
250 #[getter]
251 fn streams_prefix(&self) -> &str {
252 &self.streams_prefix
253 }
254
255 #[getter]
256 fn stream_per_topic(&self) -> bool {
257 self.stream_per_topic
258 }
259
260 #[getter]
261 fn external_streams(&self) -> Option<Vec<String>> {
262 self.external_streams.clone()
263 }
264
265 #[getter]
266 fn types_filter(&self) -> Option<Vec<String>> {
267 self.types_filter.clone()
268 }
269
270 #[getter]
271 fn heartbeat_interval_secs(&self) -> Option<u16> {
272 self.heartbeat_interval_secs
273 }
274}
275
276pub struct PyMessage(pub Py<PyAny>);
278
279impl Debug for PyMessage {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 f.debug_tuple(stringify!(PyMessage))
282 .field(&"<PyObject>")
283 .finish()
284 }
285}
286
287pub struct PyCallableHandler {
292 id: Ustr,
293 callable: Py<PyAny>,
294}
295
296impl Debug for PyCallableHandler {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 f.debug_struct(stringify!(PyCallableHandler))
299 .field("id", &self.id)
300 .finish()
301 }
302}
303
304impl PyCallableHandler {
305 pub fn new(py: Python<'_>, callable: Py<PyAny>) -> PyResult<Self> {
310 let repr_str = callable.bind(py).repr()?.to_string();
311 let id = Ustr::from(&repr_str);
312 Ok(Self { id, callable })
313 }
314}
315
316impl Handler<dyn Any> for PyCallableHandler {
317 fn id(&self) -> Ustr {
318 self.id
319 }
320
321 fn handle(&self, message: &dyn Any) {
322 if let Some(py_msg) = message.downcast_ref::<PyMessage>() {
323 Python::attach(|py| {
324 if let Err(e) = self.callable.call1(py, (&py_msg.0,)) {
325 log::error!("Python handler {id} failed: {e}", id = self.id);
326 }
327 });
328 } else {
329 log::error!(
330 "Python handler {id} received non-PyMessage type",
331 id = self.id
332 );
333 }
334 }
335}
336
337fn make_handler(py: Python<'_>, callable: Py<PyAny>) -> PyResult<ShareableMessageHandler> {
338 let handler = PyCallableHandler::new(py, callable)?;
339 Ok(TypedHandler(Rc::new(handler) as Rc<dyn Handler<dyn Any>>))
340}
341
342#[pyclass(module = "nautilus_trader.common", name = "MessageBus", unsendable)]
348#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
349pub struct PyMessageBus {
350 trader_id: TraderId,
351 instance_id: UUID4,
352 name: String,
353 has_backing: bool,
354 serializer: Option<Py<PyAny>>,
355 backing: Option<Py<PyAny>>,
356 listeners: Vec<Py<PyAny>>,
357 types_filter: Option<Py<PyAny>>,
358 streaming_types: Vec<Py<PyAny>>,
359 correlation_index: AHashMap<UUID4, Py<PyAny>>,
360 sent_count: u64,
361 req_count: u64,
362 res_count: u64,
363 pub_count: u64,
364}
365
366impl Debug for PyMessageBus {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 f.debug_struct(stringify!(PyMessageBus))
369 .field("trader_id", &self.trader_id)
370 .field("name", &self.name)
371 .finish()
372 }
373}
374
375#[pymethods]
376#[pyo3_stub_gen::derive::gen_stub_pymethods]
377impl PyMessageBus {
378 #[new]
383 #[pyo3(signature = (trader_id, clock=None, instance_id=None, name=None, serializer=None, backing=None, config=None))]
384 #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
385 fn py_new(
386 py: Python<'_>,
387 trader_id: TraderId,
388 clock: Option<Py<PyAny>>,
389 instance_id: Option<UUID4>,
390 name: Option<String>,
391 serializer: Option<Py<PyAny>>,
392 backing: Option<Py<PyAny>>,
393 config: Option<Py<PyAny>>,
394 ) -> PyResult<Self> {
395 let _ = clock;
396 let instance_id = instance_id.unwrap_or_default();
397 let bus_name = name.clone();
398 let has_backing = backing.is_some();
399
400 let msgbus = MessageBus::new(trader_id, instance_id, bus_name, None);
401 msgbus.register_message_bus();
402
403 let types_filter = if let Some(ref cfg) = config {
404 let tf = cfg.getattr(py, "types_filter")?;
405 if tf.is_none(py) {
406 None
407 } else {
408 let tuple = py
410 .import("builtins")?
411 .call_method1("tuple", (tf,))?
412 .unbind();
413 Some(tuple)
414 }
415 } else {
416 None
417 };
418
419 Ok(Self {
420 trader_id,
421 instance_id,
422 name: name.unwrap_or_else(|| "MessageBus".to_owned()),
423 has_backing,
424 serializer,
425 backing,
426 listeners: Vec::new(),
427 types_filter,
428 streaming_types: Vec::new(),
429 correlation_index: AHashMap::new(),
430 sent_count: 0,
431 req_count: 0,
432 res_count: 0,
433 pub_count: 0,
434 })
435 }
436
437 #[getter]
439 #[pyo3(name = "trader_id")]
440 fn py_trader_id(&self) -> TraderId {
441 self.trader_id
442 }
443
444 #[getter]
446 #[pyo3(name = "instance_id")]
447 fn py_instance_id(&self) -> UUID4 {
448 self.instance_id
449 }
450
451 #[getter]
453 #[pyo3(name = "name")]
454 fn py_name(&self) -> &str {
455 &self.name
456 }
457
458 #[getter]
460 #[pyo3(name = "has_backing")]
461 fn py_has_backing(&self) -> bool {
462 self.has_backing
463 }
464
465 #[getter]
467 #[pyo3(name = "sent_count")]
468 fn py_sent_count(&self) -> u64 {
469 self.sent_count
470 }
471
472 #[getter]
474 #[pyo3(name = "req_count")]
475 fn py_req_count(&self) -> u64 {
476 self.req_count
477 }
478
479 #[getter]
481 #[pyo3(name = "res_count")]
482 fn py_res_count(&self) -> u64 {
483 self.res_count
484 }
485
486 #[getter]
488 #[pyo3(name = "pub_count")]
489 fn py_pub_count(&self) -> u64 {
490 self.pub_count
491 }
492
493 #[pyo3(name = "endpoints")]
495 fn py_endpoints(&self) -> Vec<String> {
496 let bus = get_message_bus();
497 let bus_ref = bus.borrow();
498 bus_ref.endpoints().into_iter().map(String::from).collect()
499 }
500
501 #[pyo3(name = "topics")]
503 fn py_topics(&self) -> Vec<String> {
504 let bus = get_message_bus();
505 let bus_ref = bus.borrow();
506 let mut topics: Vec<String> = bus_ref.patterns().into_iter().map(String::from).collect();
507 topics.sort();
508 topics.dedup();
509 topics
510 }
511
512 #[pyo3(name = "subscriptions")]
514 #[pyo3(signature = (pattern=None))]
515 fn py_subscriptions(&self, pattern: Option<&str>) -> PyResult<Vec<String>> {
516 let filter = pattern.map(parse_pattern).transpose()?;
517
518 let bus = get_message_bus();
519 let bus_ref = bus.borrow();
520 let subs: Vec<&Subscription> = bus_ref.subscriptions();
521
522 Ok(subs
523 .into_iter()
524 .filter(|s| filter.is_none_or(|f| is_matching(s.pattern.as_bytes(), f.as_bytes())))
525 .map(|s| {
526 format!(
527 "Subscription(topic={}, handler={})",
528 s.pattern, s.handler_id
529 )
530 })
531 .collect())
532 }
533
534 #[pyo3(name = "has_subscribers")]
536 #[pyo3(signature = (pattern=None))]
537 fn py_has_subscribers(&self, pattern: Option<&str>) -> PyResult<bool> {
538 let filter = pattern.map(parse_pattern).transpose()?;
539
540 let bus = get_message_bus();
541 let bus_ref = bus.borrow();
542
543 Ok(match filter {
544 Some(filter) => bus_ref
545 .subscriptions()
546 .iter()
547 .any(|s| is_matching(s.pattern.as_bytes(), filter.as_bytes())),
548 None => !bus_ref.subscriptions().is_empty(),
549 })
550 }
551
552 #[pyo3(name = "is_subscribed")]
554 fn py_is_subscribed(&self, py: Python<'_>, topic: &str, handler: Py<PyAny>) -> PyResult<bool> {
555 let pattern = parse_pattern(topic)?;
556 let handler = make_handler(py, handler)?;
557 let sub = Subscription::new(pattern, handler, None);
558 Ok(get_message_bus().borrow().subscriptions.contains(&sub))
559 }
560
561 #[pyo3(name = "is_pending_request")]
563 fn py_is_pending_request(&self, request_id: UUID4) -> bool {
564 self.correlation_index.contains_key(&request_id)
565 }
566
567 #[pyo3(name = "is_streaming_type")]
569 #[expect(clippy::needless_pass_by_value)]
570 fn py_is_streaming_type(&self, py: Python<'_>, cls: Py<PyAny>) -> bool {
571 let cls_ref = cls.bind(py);
572 self.streaming_types.iter().any(|t| t.bind(py).is(cls_ref))
573 }
574
575 #[pyo3(name = "streaming_types")]
577 fn py_streaming_types(&self, py: Python<'_>) -> Vec<Py<PyAny>> {
578 self.streaming_types
579 .iter()
580 .map(|t| t.clone_ref(py))
581 .collect()
582 }
583
584 #[pyo3(name = "register")]
586 fn py_register(&self, py: Python<'_>, endpoint: &str, handler: Py<PyAny>) -> PyResult<()> {
587 let endpoint = parse_endpoint(endpoint)?;
588 let handler = make_handler(py, handler)?;
589 msgbus_api::register_any(endpoint, handler);
590 Ok(())
591 }
592
593 #[pyo3(name = "deregister")]
595 #[pyo3(signature = (endpoint, handler=None))]
596 #[expect(clippy::needless_pass_by_value)]
597 fn py_deregister(&self, endpoint: &str, handler: Option<Py<PyAny>>) -> PyResult<()> {
598 let _ = handler;
599 let endpoint = parse_endpoint(endpoint)?;
600 msgbus_api::deregister_any(endpoint);
601 Ok(())
602 }
603
604 #[pyo3(name = "send")]
606 fn py_send(&mut self, endpoint: &str, msg: Py<PyAny>) -> PyResult<()> {
607 let endpoint = parse_endpoint(endpoint)?;
608 let py_msg = PyMessage(msg);
609 msgbus_api::send_any(endpoint, &py_msg);
610 self.sent_count += 1;
611 Ok(())
612 }
613
614 #[pyo3(name = "request")]
616 fn py_request(&mut self, py: Python<'_>, endpoint: &str, request: Py<PyAny>) -> PyResult<()> {
617 let endpoint = parse_endpoint(endpoint)?;
618 let request_ref = request.bind(py);
619
620 let request_id: UUID4 = request_ref.getattr("id")?.extract()?;
621 let callback = request_ref.getattr("callback")?;
622
623 if self.correlation_index.contains_key(&request_id) {
624 log::error!(
625 "Cannot handle request: duplicate ID {request_id} found in correlation index"
626 );
627 return Ok(());
628 }
629
630 if !callback.is_none() {
631 self.correlation_index.insert(request_id, callback.unbind());
632 }
633
634 let py_msg = PyMessage(request);
635 msgbus_api::send_any(endpoint, &py_msg);
636 self.req_count += 1;
637
638 Ok(())
639 }
640
641 #[pyo3(name = "response")]
643 #[expect(clippy::needless_pass_by_value)]
644 fn py_response(&mut self, py: Python<'_>, response: Py<PyAny>) -> PyResult<()> {
645 let correlation_id: UUID4 = response.getattr(py, "correlation_id")?.extract(py)?;
646
647 if let Some(callback) = self.correlation_index.remove(&correlation_id) {
648 callback.call1(py, (&response,))?;
649 } else {
650 log::debug!("No callback for correlation_id {correlation_id}");
651 }
652
653 self.res_count += 1;
654 Ok(())
655 }
656
657 #[pyo3(name = "subscribe")]
659 #[pyo3(signature = (topic, handler, priority=0))]
660 fn py_subscribe(
661 &self,
662 py: Python<'_>,
663 topic: &str,
664 handler: Py<PyAny>,
665 priority: u32,
666 ) -> PyResult<()> {
667 let pattern = parse_pattern(topic)?;
668 let handler = make_handler(py, handler)?;
669 msgbus_api::subscribe_any(pattern, handler, Some(priority));
670 Ok(())
671 }
672
673 #[pyo3(name = "unsubscribe")]
675 fn py_unsubscribe(&self, py: Python<'_>, topic: &str, handler: Py<PyAny>) -> PyResult<()> {
676 let pattern = parse_pattern(topic)?;
677 let handler = make_handler(py, handler)?;
678 msgbus_api::unsubscribe_any(pattern, &handler);
679 Ok(())
680 }
681
682 #[pyo3(name = "publish")]
684 #[pyo3(signature = (topic, msg, external_pub=true))]
685 #[expect(clippy::needless_pass_by_value)]
686 fn py_publish(
687 &mut self,
688 py: Python<'_>,
689 topic: &str,
690 msg: Py<PyAny>,
691 external_pub: bool,
692 ) -> PyResult<()> {
693 let topic_mstr = MStr::<Topic>::topic(topic).map_err(to_pyruntime_err)?;
694
695 let py_msg = PyMessage(msg.clone_ref(py));
696 msgbus_api::publish_any(topic_mstr, &py_msg);
697
698 if external_pub {
699 self.publish_external(py, topic, &msg)?;
700 }
701
702 self.pub_count += 1;
703 Ok(())
704 }
705
706 #[pyo3(name = "dispose")]
708 fn py_dispose(&mut self, py: Python<'_>) -> PyResult<()> {
709 log::debug!("Closing message bus");
710
711 get_message_bus().borrow_mut().dispose();
712
713 self.correlation_index.clear();
714 self.listeners.clear();
715 self.streaming_types.clear();
716
717 if let Some(ref backing) = self.backing {
718 let db = backing.bind(py);
719 if !db.call_method0("is_closed")?.extract::<bool>()? {
720 db.call_method0("close")?;
721 }
722 }
723
724 log::info!("Closed message bus");
725 Ok(())
726 }
727
728 #[pyo3(name = "add_streaming_type")]
730 fn py_add_streaming_type(&mut self, cls: Py<PyAny>) {
731 self.streaming_types.push(cls);
732 }
733
734 #[pyo3(name = "add_listener")]
736 fn py_add_listener(&mut self, listener: Py<PyAny>) {
737 self.listeners.push(listener);
738 }
739}
740
741impl PyMessageBus {
742 fn publish_external(&self, py: Python<'_>, topic: &str, msg: &Py<PyAny>) -> PyResult<()> {
743 if let Some(ref filter) = self.types_filter {
744 let is_excluded = py
745 .import("builtins")?
746 .call_method1("isinstance", (msg, filter))?
747 .extract::<bool>()?;
748
749 if is_excluded {
750 return Ok(());
751 }
752 }
753
754 let msg_ref = msg.bind(py);
756 let payload: Py<PyAny> = if msg_ref.is_instance_of::<pyo3::types::PyBytes>() {
757 msg.clone_ref(py)
758 } else if let Some(ref serializer) = self.serializer {
759 serializer.call_method1(py, "serialize", (msg,))?
760 } else {
761 return Ok(());
762 };
763
764 if let Some(ref backing) = self.backing {
765 let db = backing.bind(py);
766 if !db.call_method0("is_closed")?.extract::<bool>()? {
767 db.call_method1("publish", (topic, &payload))?;
768 }
769 }
770
771 for listener in &self.listeners {
772 let l = listener.bind(py);
773 if l.call_method0("is_closed")?.extract::<bool>()? {
774 continue;
775 }
776 l.call_method1("publish", (topic, &payload))?;
777 }
778
779 Ok(())
780 }
781}
782
783fn parse_endpoint(endpoint: &str) -> PyResult<MStr<Endpoint>> {
784 MStr::<Endpoint>::endpoint(endpoint).map_err(to_pyvalue_err)
785}
786
787fn parse_pattern(pattern: &str) -> PyResult<MStr<Pattern>> {
788 MStr::<Pattern>::pattern_checked(pattern).map_err(to_pyvalue_err)
789}
790
791#[cfg(test)]
792mod tests {
793 use std::any::Any;
794
795 use pyo3::{exceptions::PyValueError, ffi::c_str};
796 use rstest::rstest;
797
798 use super::*;
799
800 #[rstest]
801 fn test_message_bus_factory_registry_compatibility_constructors() {
802 let registry = MessageBusFactoryRegistry::new();
803 let default_registry = MessageBusFactoryRegistry::default();
804
805 assert_eq!(format!("{registry:?}"), format!("{default_registry:?}"));
806 assert!(format!("{registry:?}").contains("message bus factory"));
807 }
808
809 #[rstest]
810 fn message_bus_config_py_new_maps_validate_error_to_value_error() {
811 pyo3::Python::initialize();
812 Python::attach(|py| {
813 let err = MessageBusConfig::py_new(
814 Some(SerializationEncoding::Json),
815 None,
816 Some(SerializationEncoding::Capnp),
817 None,
818 None,
819 None,
820 None,
821 None,
822 None,
823 None,
824 None,
825 None,
826 None,
827 None,
828 None,
829 )
830 .unwrap_err();
831
832 assert!(err.is_instance_of::<PyValueError>(py));
833 assert_eq!(
834 err.value(py).to_string(),
835 format!(
836 "MessageBusConfig.encoding_builtin has unsupported value: {} is not supported by AccountState, OrderEventAny, PositionEvent, PortfolioSnapshot",
837 SerializationEncoding::Capnp
838 )
839 );
840 });
841 }
842
843 #[rstest]
844 fn test_py_message_downcast() {
845 pyo3::Python::initialize();
846 Python::attach(|py| {
847 let py_obj = py.eval(c_str!("42"), None, None).unwrap();
848 let msg = PyMessage(py_obj.unbind());
849
850 let any_ref: &dyn Any = &msg;
851 let downcasted = any_ref.downcast_ref::<PyMessage>();
852 assert!(downcasted.is_some());
853
854 let inner = &downcasted.unwrap().0;
855 let value: i64 = inner.extract(py).unwrap();
856 assert_eq!(value, 42);
857 });
858 }
859
860 #[rstest]
861 fn test_py_callable_handler_id_stability() {
862 pyo3::Python::initialize();
863 Python::attach(|py| {
864 let callable = py.eval(c_str!("lambda x: x"), None, None).unwrap().unbind();
865
866 let handler1 = PyCallableHandler::new(py, callable.clone_ref(py)).unwrap();
867 let handler2 = PyCallableHandler::new(py, callable).unwrap();
868
869 assert_eq!(handler1.id(), handler2.id());
870 });
871 }
872
873 #[rstest]
874 fn test_py_callable_handler_dispatch() {
875 pyo3::Python::initialize();
876 Python::attach(|py| {
877 let main = py.import("__main__").unwrap();
878 let globals = main.dict();
879 py.run(
880 c_str!("results = []\ndef handler(x): results.append(x)"),
881 Some(&globals),
882 None,
883 )
884 .unwrap();
885
886 let handler_fn = globals.get_item("handler").unwrap().unwrap().unbind();
887 let handler = PyCallableHandler::new(py, handler_fn).unwrap();
888
889 let py_obj = py.eval(c_str!("'hello'"), None, None).unwrap();
890 let msg = PyMessage(py_obj.unbind());
891
892 let any_ref: &dyn Any = &msg;
893 handler.handle(any_ref);
894
895 let results = globals.get_item("results").unwrap().unwrap();
896 let len: usize = results.len().unwrap();
897 assert_eq!(len, 1);
898 });
899 }
900}