1use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
17use nautilus_model::identifiers::{AccountId, InstrumentId};
18use nautilus_network::websocket::TransportBackend;
19use pyo3::{PyResult, pymethods};
20
21use crate::{
22 common::enums::{PolymarketSignatureType, PolymarketSignerType},
23 config::{
24 PolymarketDataClientConfig, PolymarketExecutionClientConfig,
25 PolymarketInstrumentProviderConfig, PolymarketUpDownEventSlugConfig,
26 },
27 providers::build_gamma_params_from_hashmap,
28};
29
30const PY_OPTION_U64_MISSING_SENTINEL: u64 = u64::MAX;
31
32fn resolve_optional_u64_arg(value: Option<u64>, default: Option<u64>) -> Option<u64> {
33 match value {
34 Some(PY_OPTION_U64_MISSING_SENTINEL) => default,
35 other => other,
36 }
37}
38
39#[pymethods]
40#[pyo3_stub_gen::derive::gen_stub_pymethods]
41impl PolymarketUpDownEventSlugConfig {
42 #[new]
49 #[pyo3(signature = (assets=None, interval_mins=None, periods=None, start_offset_periods=None))]
50 fn py_new(
51 assets: Option<Vec<String>>,
52 interval_mins: Option<u64>,
53 periods: Option<u64>,
54 start_offset_periods: Option<i64>,
55 ) -> Self {
56 let default = Self::default();
57 Self {
58 assets: assets.unwrap_or(default.assets),
59 interval_mins: interval_mins.unwrap_or(default.interval_mins),
60 periods: periods.unwrap_or(default.periods),
61 start_offset_periods: start_offset_periods.unwrap_or(default.start_offset_periods),
62 }
63 }
64
65 fn __repr__(&self) -> String {
66 format!("{self:?}")
67 }
68
69 fn __str__(&self) -> String {
70 format!("{self:?}")
71 }
72}
73
74#[pymethods]
75#[pyo3_stub_gen::derive::gen_stub_pymethods]
76impl PolymarketInstrumentProviderConfig {
77 #[new]
82 #[pyo3(signature = (load_all=None, load_ids=None, filters=None, event_slugs=None, market_slugs=None, event_slug_builder=None, log_warnings=None, use_gamma_markets=None, series_ids=None))]
83 #[expect(clippy::too_many_arguments)]
84 fn py_new(
85 load_all: Option<bool>,
86 load_ids: Option<Vec<InstrumentId>>,
87 filters: Option<std::collections::HashMap<String, String>>,
88 event_slugs: Option<Vec<String>>,
89 market_slugs: Option<Vec<String>>,
90 event_slug_builder: Option<PolymarketUpDownEventSlugConfig>,
91 log_warnings: Option<bool>,
92 use_gamma_markets: Option<bool>,
93 series_ids: Option<Vec<u64>>,
94 ) -> PyResult<Self> {
95 let default = Self::default();
96 let config = Self {
97 load_all: load_all.unwrap_or(default.load_all),
98 load_ids,
99 filters,
100 event_slugs,
101 market_slugs,
102 event_slug_builder,
103 series_ids,
104 log_warnings: log_warnings.unwrap_or(default.log_warnings),
105 use_gamma_markets: use_gamma_markets.unwrap_or(default.use_gamma_markets),
106 };
107
108 if let Some(filters) = config.filters.as_ref() {
109 build_gamma_params_from_hashmap(filters)
110 .map_err(|e| to_pyvalue_err(format!("Invalid Polymarket Gamma filters: {e}")))?;
111 }
112 Ok(config)
113 }
114
115 fn __repr__(&self) -> String {
116 format!("{self:?}")
117 }
118
119 fn __str__(&self) -> String {
120 format!("{self:?}")
121 }
122}
123
124#[pymethods]
125#[pyo3_stub_gen::derive::gen_stub_pymethods]
126impl PolymarketDataClientConfig {
127 #[new]
133 #[pyo3(signature = (instrument_config=None, base_url_http=None, base_url_ws=None, base_url_gamma=None, base_url_data_api=None, http_timeout_secs=None, ws_timeout_secs=None, ws_max_subscriptions=None, update_instruments_interval_mins=PY_OPTION_U64_MISSING_SENTINEL, subscribe_new_markets=None, auto_load_missing_instruments=None, auto_load_debounce_ms=None, auto_load_max_retries=None, auto_load_retry_delay_initial_secs=None, auto_load_retry_delay_max_secs=None, new_market_fetch_max_concurrency=None, resolve_poll_enabled=None, resolve_poll_interval_secs=None, resolve_poll_grace_secs=None, resolve_poll_max_wait_secs=None, base_url_reference_prices=None, transport_backend=None, drop_quotes_missing_side=None, proxy_url=None, compute_effective_deltas=None, book_snapshot_timeout_secs=None, book_stale_check_interval_secs=None, book_stale_threshold_secs=None, api_key=None, api_secret=None, passphrase=None))]
134 #[expect(clippy::too_many_arguments)]
135 fn py_new(
136 instrument_config: Option<PolymarketInstrumentProviderConfig>,
137 base_url_http: Option<String>,
138 base_url_ws: Option<String>,
139 base_url_gamma: Option<String>,
140 base_url_data_api: Option<String>,
141 http_timeout_secs: Option<u64>,
142 ws_timeout_secs: Option<u64>,
143 ws_max_subscriptions: Option<usize>,
144 update_instruments_interval_mins: Option<u64>,
145 subscribe_new_markets: Option<bool>,
146 auto_load_missing_instruments: Option<bool>,
147 auto_load_debounce_ms: Option<u64>,
148 auto_load_max_retries: Option<u32>,
149 auto_load_retry_delay_initial_secs: Option<f64>,
150 auto_load_retry_delay_max_secs: Option<f64>,
151 new_market_fetch_max_concurrency: Option<usize>,
152 resolve_poll_enabled: Option<bool>,
153 resolve_poll_interval_secs: Option<u64>,
154 resolve_poll_grace_secs: Option<u64>,
155 resolve_poll_max_wait_secs: Option<u64>,
156 base_url_reference_prices: Option<String>,
157 transport_backend: Option<TransportBackend>,
158 drop_quotes_missing_side: Option<bool>,
159 proxy_url: Option<String>,
160 compute_effective_deltas: Option<bool>,
161 book_snapshot_timeout_secs: Option<u64>,
162 book_stale_check_interval_secs: Option<u64>,
163 book_stale_threshold_secs: Option<u64>,
164 api_key: Option<String>,
165 api_secret: Option<String>,
166 passphrase: Option<String>,
167 ) -> PyResult<Self> {
168 let default = Self::default();
169
170 let config = Self {
171 instrument_config,
172 filters: Vec::new(),
173 base_url_http,
174 base_url_ws,
175 base_url_reference_prices,
176 base_url_gamma,
177 base_url_data_api,
178 api_key: api_key.map(SecretString::from),
179 api_secret: api_secret.map(SecretString::from),
180 passphrase: passphrase.map(SecretString::from),
181 proxy_url: proxy_url.map(SecretString::from),
182 http_timeout_secs: http_timeout_secs.unwrap_or(default.http_timeout_secs),
183 ws_timeout_secs: ws_timeout_secs.unwrap_or(default.ws_timeout_secs),
184 ws_max_subscriptions: ws_max_subscriptions.unwrap_or(default.ws_max_subscriptions),
185 update_instruments_interval_mins: resolve_optional_u64_arg(
186 update_instruments_interval_mins,
187 default.update_instruments_interval_mins,
188 ),
189 subscribe_new_markets: subscribe_new_markets.unwrap_or(default.subscribe_new_markets),
190 new_market_filter: None,
191 new_market_fetch_max_concurrency: new_market_fetch_max_concurrency
192 .unwrap_or(default.new_market_fetch_max_concurrency),
193 drop_quotes_missing_side: drop_quotes_missing_side
194 .unwrap_or(default.drop_quotes_missing_side),
195 auto_load_missing_instruments: auto_load_missing_instruments
196 .unwrap_or(default.auto_load_missing_instruments),
197 auto_load_debounce_ms: auto_load_debounce_ms.unwrap_or(default.auto_load_debounce_ms),
198 auto_load_max_retries: auto_load_max_retries.unwrap_or(default.auto_load_max_retries),
199 auto_load_retry_delay_initial_secs: auto_load_retry_delay_initial_secs
200 .unwrap_or(default.auto_load_retry_delay_initial_secs),
201 auto_load_retry_delay_max_secs: auto_load_retry_delay_max_secs
202 .unwrap_or(default.auto_load_retry_delay_max_secs),
203 resolve_poll_enabled: resolve_poll_enabled.unwrap_or(default.resolve_poll_enabled),
204 resolve_poll_interval_secs: resolve_poll_interval_secs
205 .unwrap_or(default.resolve_poll_interval_secs),
206 resolve_poll_grace_secs: resolve_poll_grace_secs
207 .unwrap_or(default.resolve_poll_grace_secs),
208 resolve_poll_max_wait_secs: resolve_poll_max_wait_secs
209 .unwrap_or(default.resolve_poll_max_wait_secs),
210 transport_backend: transport_backend.unwrap_or(default.transport_backend),
211 compute_effective_deltas: compute_effective_deltas
212 .unwrap_or(default.compute_effective_deltas),
213 book_snapshot_timeout_secs: book_snapshot_timeout_secs
214 .unwrap_or(default.book_snapshot_timeout_secs),
215 book_stale_check_interval_secs: book_stale_check_interval_secs
216 .unwrap_or(default.book_stale_check_interval_secs),
217 book_stale_threshold_secs: book_stale_threshold_secs
218 .unwrap_or(default.book_stale_threshold_secs),
219 };
220 config
221 .validated_proxy_url()
222 .map_err(|e| to_pyvalue_err(format!("Invalid Polymarket proxy URL: {e}")))?;
223 Ok(config)
224 }
225
226 #[getter]
227 #[pyo3(name = "has_proxy_url")]
228 const fn py_has_proxy_url(&self) -> bool {
229 self.has_proxy_url()
230 }
231
232 fn __repr__(&self) -> String {
233 format!("{self:?}")
234 }
235
236 fn __str__(&self) -> String {
237 format!("{self:?}")
238 }
239}
240
241#[pymethods]
242#[pyo3_stub_gen::derive::gen_stub_pymethods]
243impl PolymarketExecutionClientConfig {
244 #[new]
246 #[expect(clippy::too_many_arguments)]
247 #[pyo3(signature = (account_id=None, private_key=None, api_key=None, api_secret=None, passphrase=None, funder=None, signature_type=None, base_url_http=None, base_url_ws=None, base_url_data_api=None, http_timeout_secs=None, max_retries=None, retry_delay_initial_ms=None, retry_delay_max_ms=None, heartbeat_enabled=None, transport_backend=None, proxy_url=None, instrument_config=None, signer_type=None))]
248 fn py_new(
249 account_id: Option<String>,
250 private_key: Option<String>,
251 api_key: Option<String>,
252 api_secret: Option<String>,
253 passphrase: Option<String>,
254 funder: Option<String>,
255 signature_type: Option<PolymarketSignatureType>,
256 base_url_http: Option<String>,
257 base_url_ws: Option<String>,
258 base_url_data_api: Option<String>,
259 http_timeout_secs: Option<u64>,
260 max_retries: Option<u32>,
261 retry_delay_initial_ms: Option<u64>,
262 retry_delay_max_ms: Option<u64>,
263 heartbeat_enabled: Option<bool>,
264 transport_backend: Option<TransportBackend>,
265 proxy_url: Option<String>,
266 instrument_config: Option<PolymarketInstrumentProviderConfig>,
267 signer_type: Option<PolymarketSignerType>,
268 ) -> PyResult<Self> {
269 let default = Self::default();
270 let config = Self {
271 account_id: account_id.map_or(default.account_id, |s| AccountId::from(s.as_str())),
272 private_key: private_key.map(SecretString::from),
273 api_key: api_key.map(SecretString::from),
274 api_secret: api_secret.map(SecretString::from),
275 passphrase: passphrase.map(SecretString::from),
276 funder,
277 signature_type: signature_type.unwrap_or(default.signature_type),
278 signer_type: signer_type.unwrap_or_default(),
279 base_url_http,
280 base_url_ws,
281 base_url_data_api,
282 proxy_url: proxy_url.map(SecretString::from),
283 http_timeout_secs: http_timeout_secs.unwrap_or(default.http_timeout_secs),
284 max_retries: max_retries.unwrap_or(default.max_retries),
285 retry_delay_initial_ms: retry_delay_initial_ms
286 .unwrap_or(default.retry_delay_initial_ms),
287 retry_delay_max_ms: retry_delay_max_ms.unwrap_or(default.retry_delay_max_ms),
288 heartbeat_enabled: heartbeat_enabled.unwrap_or(default.heartbeat_enabled),
289 transport_backend: transport_backend.unwrap_or(default.transport_backend),
290 instrument_config,
291 };
292
293 config.validate_signer().map_err(to_pyvalue_err)?;
294 config
295 .validated_proxy_url()
296 .map_err(|e| to_pyvalue_err(format!("Invalid Polymarket proxy URL: {e}")))?;
297 Ok(config)
298 }
299
300 #[getter]
301 #[pyo3(name = "has_proxy_url")]
302 const fn py_has_proxy_url(&self) -> bool {
303 self.has_proxy_url()
304 }
305
306 fn __repr__(&self) -> String {
307 format!("{self:?}")
308 }
309
310 fn __str__(&self) -> String {
311 format!("{self:?}")
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use pyo3::{
318 Bound, IntoPyObject, Python,
319 types::{PyAnyMethods, PyDict, PyDictMethods, PyTuple},
320 };
321 use rstest::rstest;
322
323 use super::*;
324
325 fn construct_data_client_config(
326 py: Python<'_>,
327 args: Option<&Bound<'_, PyTuple>>,
328 kwargs: Option<&Bound<'_, PyDict>>,
329 ) -> PolymarketDataClientConfig {
330 let cls = py.get_type::<PolymarketDataClientConfig>();
331
332 let config = match args {
333 Some(args) => cls.call(args, kwargs),
334 None => cls.call((), kwargs),
335 }
336 .expect("construct PolymarketDataClientConfig");
337
338 config
339 .extract::<PolymarketDataClientConfig>()
340 .expect("extract PolymarketDataClientConfig")
341 }
342
343 #[rstest]
344 fn direct_pyo3_constructor_preserves_none_update_interval() {
345 Python::initialize();
346 Python::attach(|py| {
347 let kwargs = PyDict::new(py);
348 kwargs
349 .set_item("update_instruments_interval_mins", py.None())
350 .unwrap();
351
352 let config = construct_data_client_config(py, None, Some(&kwargs));
353
354 assert_eq!(config.update_instruments_interval_mins, None);
355 });
356 }
357
358 #[rstest]
359 fn direct_pyo3_constructor_uses_default_update_interval_when_omitted() {
360 Python::initialize();
361 Python::attach(|py| {
362 let config = construct_data_client_config(py, None, None);
363
364 assert_eq!(
365 config.update_instruments_interval_mins,
366 PolymarketDataClientConfig::default().update_instruments_interval_mins,
367 );
368 assert!(config.drop_quotes_missing_side);
369 });
370 }
371
372 #[rstest]
373 fn direct_pyo3_constructor_preserves_none_update_interval_for_positional_args() {
374 Python::initialize();
375 Python::attach(|py| {
376 let args = PyTuple::new(
377 py,
378 [
379 py.None(),
380 py.None(),
381 py.None(),
382 py.None(),
383 py.None(),
384 py.None(),
385 py.None(),
386 py.None(),
387 py.None(),
388 py.None(),
389 ],
390 )
391 .expect("args");
392
393 let config = construct_data_client_config(py, Some(&args), None);
394
395 assert_eq!(config.update_instruments_interval_mins, None);
396 });
397 }
398
399 #[rstest]
400 fn direct_pyo3_constructor_keeps_positional_slots_ahead_of_credentials() {
401 Python::initialize();
402 Python::attach(|py| {
403 let mut args: Vec<pyo3::Bound<'_, pyo3::PyAny>> =
407 (0..22).map(|_| py.None().into_bound(py)).collect();
408 args.push(pyo3::types::PyBool::new(py, false).to_owned().into_any());
409 let args = PyTuple::new(py, args).expect("args");
410
411 let config = construct_data_client_config(py, Some(&args), None);
412
413 assert!(!config.drop_quotes_missing_side);
414 assert!(config.api_key.is_none());
415 assert!(config.api_secret.is_none());
416 assert!(config.passphrase.is_none());
417 });
418 }
419
420 #[rstest]
421 fn direct_pyo3_constructor_sets_new_market_fetch_max_concurrency() {
422 Python::initialize();
423 Python::attach(|py| {
424 let kwargs = PyDict::new(py);
425 kwargs
426 .set_item("new_market_fetch_max_concurrency", 23)
427 .unwrap();
428
429 let config = construct_data_client_config(py, None, Some(&kwargs));
430
431 assert_eq!(config.new_market_fetch_max_concurrency, 23);
432 });
433 }
434
435 #[rstest]
436 fn direct_pyo3_constructor_sets_drop_quotes_missing_side() {
437 Python::initialize();
438 Python::attach(|py| {
439 let kwargs = PyDict::new(py);
440 kwargs.set_item("drop_quotes_missing_side", false).unwrap();
441
442 let config = construct_data_client_config(py, None, Some(&kwargs));
443
444 assert!(!config.drop_quotes_missing_side);
445 });
446 }
447
448 #[rstest]
449 fn direct_pyo3_constructor_sets_compute_effective_deltas() {
450 Python::initialize();
451 Python::attach(|py| {
452 let kwargs = PyDict::new(py);
453 kwargs.set_item("compute_effective_deltas", true).unwrap();
454
455 let config = construct_data_client_config(py, None, Some(&kwargs));
456
457 assert!(config.compute_effective_deltas);
458 });
459 }
460
461 #[rstest]
462 fn direct_pyo3_constructor_sets_base_url_reference_prices() {
463 Python::initialize();
464 Python::attach(|py| {
465 let kwargs = PyDict::new(py);
466 kwargs
467 .set_item("base_url_reference_prices", "wss://ws-live-data.example")
468 .unwrap();
469
470 let config = construct_data_client_config(py, None, Some(&kwargs));
471
472 assert_eq!(
473 config.base_url_reference_prices.as_deref(),
474 Some("wss://ws-live-data.example")
475 );
476 });
477 }
478
479 #[rstest]
480 fn direct_pyo3_constructor_preserves_existing_positional_order() {
481 Python::initialize();
482 Python::attach(|py| {
483 let args = PyTuple::new(
484 py,
485 [
486 py.None(),
487 "https://http.example"
488 .into_pyobject(py)
489 .unwrap()
490 .into_any()
491 .unbind(),
492 "wss://ws.example"
493 .into_pyobject(py)
494 .unwrap()
495 .into_any()
496 .unbind(),
497 "https://gamma.example"
498 .into_pyobject(py)
499 .unwrap()
500 .into_any()
501 .unbind(),
502 "https://data.example"
503 .into_pyobject(py)
504 .unwrap()
505 .into_any()
506 .unbind(),
507 41_u64.into_pyobject(py).unwrap().into_any().unbind(),
508 42_u64.into_pyobject(py).unwrap().into_any().unbind(),
509 512_usize.into_pyobject(py).unwrap().into_any().unbind(),
510 ],
511 )
512 .expect("args");
513
514 let config = construct_data_client_config(py, Some(&args), None);
515
516 assert_eq!(
517 config.base_url_http.as_deref(),
518 Some("https://http.example")
519 );
520 assert_eq!(config.base_url_ws.as_deref(), Some("wss://ws.example"));
521 assert_eq!(
522 config.base_url_gamma.as_deref(),
523 Some("https://gamma.example")
524 );
525 assert_eq!(
526 config.base_url_data_api.as_deref(),
527 Some("https://data.example")
528 );
529 assert_eq!(config.base_url_reference_prices, None);
530 assert_eq!(config.http_timeout_secs, 41);
531 assert_eq!(config.ws_timeout_secs, 42);
532 assert_eq!(config.ws_max_subscriptions, 512);
533 });
534 }
535
536 #[rstest]
537 fn direct_pyo3_constructor_preserves_base_url_reference_prices_positional_slot() {
538 Python::initialize();
539 Python::attach(|py| {
540 let args = PyTuple::new(
541 py,
542 [
543 py.None(),
544 py.None(),
545 py.None(),
546 py.None(),
547 py.None(),
548 py.None(),
549 py.None(),
550 py.None(),
551 py.None(),
552 py.None(),
553 py.None(),
554 py.None(),
555 py.None(),
556 py.None(),
557 py.None(),
558 py.None(),
559 py.None(),
560 py.None(),
561 py.None(),
562 py.None(),
563 "wss://ws-live-data.example"
564 .into_pyobject(py)
565 .unwrap()
566 .into_any()
567 .unbind(),
568 ],
569 )
570 .expect("args");
571
572 let config = construct_data_client_config(py, Some(&args), None);
573
574 assert_eq!(
575 config.base_url_reference_prices.as_deref(),
576 Some("wss://ws-live-data.example")
577 );
578 });
579 }
580
581 #[rstest]
582 fn direct_pyo3_data_config_propagates_proxy_without_raw_getter() {
583 const SECRET: &str = "data-python-proxy-secret";
584 Python::initialize();
585 Python::attach(|py| {
586 let proxy_url = format!("http://data-user:{SECRET}@127.0.0.1:18083");
587 let kwargs = PyDict::new(py);
588 kwargs.set_item("proxy_url", &proxy_url).unwrap();
589 let cls = py.get_type::<PolymarketDataClientConfig>();
590 let obj = cls.call((), Some(&kwargs)).expect("construct data config");
591 let has_proxy_url = obj
592 .getattr("has_proxy_url")
593 .expect("has_proxy_url getter")
594 .extract::<bool>()
595 .expect("bool getter");
596 let repr = obj.repr().expect("data config repr").to_string();
597 let config = obj
598 .extract::<PolymarketDataClientConfig>()
599 .expect("extract data config");
600
601 assert_eq!(
602 config.proxy_url.as_ref().map(SecretString::expose_secret),
603 Some(proxy_url.as_str()),
604 );
605 assert!(has_proxy_url);
606 assert!(!obj.hasattr("proxy_url").unwrap());
607 assert!(!repr.contains(SECRET));
608 });
609 }
610
611 #[rstest]
612 fn direct_pyo3_exec_config_propagates_proxy_without_raw_getter() {
613 const SECRET: &str = "exec-python-proxy-secret";
614 Python::initialize();
615 Python::attach(|py| {
616 let proxy_url = format!("https://exec-user:{SECRET}@127.0.0.1:18084");
617 let kwargs = PyDict::new(py);
618 kwargs.set_item("proxy_url", &proxy_url).unwrap();
619 kwargs.set_item("heartbeat_enabled", true).unwrap();
620 let cls = py.get_type::<PolymarketExecutionClientConfig>();
621 let obj = cls
622 .call((), Some(&kwargs))
623 .expect("construct execution config");
624 let has_proxy_url = obj
625 .getattr("has_proxy_url")
626 .expect("has_proxy_url getter")
627 .extract::<bool>()
628 .expect("bool getter");
629 let repr = obj.repr().expect("execution config repr").to_string();
630 let heartbeat_enabled = obj
631 .getattr("heartbeat_enabled")
632 .expect("heartbeat_enabled getter")
633 .extract::<bool>()
634 .expect("bool getter");
635 let config = obj
636 .extract::<PolymarketExecutionClientConfig>()
637 .expect("extract execution config");
638
639 assert_eq!(
640 config.proxy_url.as_ref().map(SecretString::expose_secret),
641 Some(proxy_url.as_str()),
642 );
643 assert!(config.heartbeat_enabled);
644 assert!(has_proxy_url);
645 assert!(heartbeat_enabled);
646 assert!(!obj.hasattr("proxy_url").unwrap());
647 assert!(!repr.contains(SECRET));
648 });
649 }
650
651 #[rstest]
652 fn direct_pyo3_exec_config_wires_instrument_config_load_ids() {
653 Python::initialize();
654 Python::attach(|py| {
655 let scoped = InstrumentId::from("0xabc-123.POLYMARKET");
656 let provider_kwargs = PyDict::new(py);
657 provider_kwargs.set_item("load_ids", vec![scoped]).unwrap();
658 let provider = py
659 .get_type::<PolymarketInstrumentProviderConfig>()
660 .call((), Some(&provider_kwargs))
661 .expect("construct provider config");
662 let kwargs = PyDict::new(py);
663 kwargs.set_item("instrument_config", &provider).unwrap();
664 let obj = py
665 .get_type::<PolymarketExecutionClientConfig>()
666 .call((), Some(&kwargs))
667 .expect("construct execution config");
668 let exposed = obj
669 .getattr("instrument_config")
670 .expect("instrument_config getter")
671 .extract::<PolymarketInstrumentProviderConfig>()
672 .expect("extract provider config");
673 let config = obj
674 .extract::<PolymarketExecutionClientConfig>()
675 .expect("extract execution config");
676
677 assert_eq!(exposed.load_ids.as_deref(), Some([scoped].as_slice()));
678 assert_eq!(config.reconciliation_load_ids(), Some([scoped].as_slice()));
679 });
680 }
681
682 #[rstest]
683 fn direct_pyo3_proxy_validation_error_redacts_credentials() {
684 const SECRET: &str = "invalid-python-proxy-secret";
685 Python::initialize();
686 Python::attach(|py| {
687 let kwargs = PyDict::new(py);
688 kwargs
689 .set_item("proxy_url", format!("http://proxy-user:{SECRET}@[::1"))
690 .unwrap();
691 let error = py
692 .get_type::<PolymarketDataClientConfig>()
693 .call((), Some(&kwargs))
694 .expect_err("malformed proxy URL should fail");
695 let message = error.to_string();
696
697 assert!(message.contains("Invalid Polymarket proxy URL"));
698 assert!(!message.contains(SECRET));
699 });
700 }
701}