rp_python_sdk 1.5.0__tar.gz → 1.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/PKG-INFO +11 -21
  2. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/README.md +10 -20
  3. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/pyproject.toml +1 -1
  4. rp_python_sdk-1.5.2/rp_python_sdk/endpoints/util/version_info.py +2 -0
  5. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/relying_party_client_sdk.py +17 -6
  6. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_dispatcher.py +34 -0
  7. rp_python_sdk-1.5.2/rp_python_sdk/telemetry/telemetry_session_pool.py +81 -0
  8. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_token_provider.py +10 -8
  9. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_transport.py +38 -13
  10. rp_python_sdk-1.5.0/rp_python_sdk/endpoints/util/version_info.py +0 -2
  11. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/__init__.py +0 -0
  12. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/cache/__init__.py +0 -0
  13. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/cache/http_cache.py +0 -0
  14. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/get_participants.py +0 -0
  15. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/pushed_authorisation_request.py +0 -0
  16. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/retrieve_tokens.py +0 -0
  17. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/user_info.py +0 -0
  18. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/client_assertion.py +0 -0
  19. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/fapi.py +0 -0
  20. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/mapping.py +0 -0
  21. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/system_info.py +0 -0
  22. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/user_agent.py +0 -0
  23. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/filters/participant_filters.py +0 -0
  24. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/model.py +0 -0
  25. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
  26. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/sdk_config.py +0 -0
  27. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/setup_logger.py +0 -0
  28. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/__init__.py +0 -0
  29. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/async_telemetry_sender.py +0 -0
  30. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/sync_telemetry_sender.py +0 -0
  31. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_config.py +0 -0
  32. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_context.py +0 -0
  33. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_event.py +0 -0
  34. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_event_factory.py +0 -0
  35. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_sender.py +0 -0
  36. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_session.py +0 -0
  37. {rp_python_sdk-1.5.0 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_url_resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rp_python_sdk
3
- Version: 1.5.0
3
+ Version: 1.5.2
4
4
  Summary: Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem
5
5
  License: Proprietary
6
6
  Author: Erik Pragt
@@ -419,27 +419,9 @@ with a unique `x-fapi-interaction-id` that was used for this request to the auth
419
419
  ## Telemetry
420
420
 
421
421
  The SDK emits non-sensitive telemetry about its PAR, Token and UserInfo endpoint interactions to the
422
- centralized ConnectID telemetry service over mTLS. Telemetry is **enabled by default** and requires no
423
- configuration. It is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
422
+ centralized ConnectID telemetry service over mTLS. Telemetry is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
424
423
 
425
- ### Opting out
426
-
427
- To disable telemetry, set `enabled=False` on the `TelemetryConfig`:
428
-
429
- ```python
430
- from rp_python_sdk.relying_party_client_sdk import RelyingPartyClientSdk
431
- from rp_python_sdk.sdk_config import SdkConfig, CustomConfig
432
- from rp_python_sdk.telemetry.telemetry_config import TelemetryConfig
433
-
434
- client = RelyingPartyClientSdk(
435
- config=SdkConfig(
436
- # ... other SdkConfig fields ...
437
- custom_config=CustomConfig(
438
- telemetry_config=TelemetryConfig(enabled=False)
439
- )
440
- ),
441
- )
442
- ```
424
+ > **Note:** Telemetry is **enabled by default**. To opt out, set `TelemetryConfig(enabled=False)` on your `CustomConfig`.
443
425
 
444
426
  When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
445
427
  threads, so there is no overhead.
@@ -620,6 +602,14 @@ poetry publish
620
602
 
621
603
  ## Release Notes
622
604
 
605
+ ### 1.5.2 (Sep 30, 2026)
606
+ - **Telemetry re-enabled by default**: Telemetry is now enabled by default again after the concurrency and shutdown bugfixes below. Opt out with `TelemetryConfig(enabled=False)`.
607
+ - **Concurrent telemetry session isolation**: Fixed a race condition where overlapping SDK operations could share or corrupt telemetry session state.
608
+ - **Daemon thread shutdown with atexit handler**: Fixed an issue where daemon worker threads were not cleaned up when `close()` was not called explicitly, by registering an `atexit` handler for graceful shutdown.
609
+
610
+ ### 1.5.1 (Sep 25, 2026)
611
+ - **Telemetry disabled by default**: Telemetry was temporarily disabled while a bugfix was in progress.
612
+
623
613
  ### 1.5.0 (Sep 22, 2026)
624
614
  - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
625
615
  - Supports ASYNC (default) and SYNC delivery modes
@@ -396,27 +396,9 @@ with a unique `x-fapi-interaction-id` that was used for this request to the auth
396
396
  ## Telemetry
397
397
 
398
398
  The SDK emits non-sensitive telemetry about its PAR, Token and UserInfo endpoint interactions to the
399
- centralized ConnectID telemetry service over mTLS. Telemetry is **enabled by default** and requires no
400
- configuration. It is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
399
+ centralized ConnectID telemetry service over mTLS. Telemetry is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
401
400
 
402
- ### Opting out
403
-
404
- To disable telemetry, set `enabled=False` on the `TelemetryConfig`:
405
-
406
- ```python
407
- from rp_python_sdk.relying_party_client_sdk import RelyingPartyClientSdk
408
- from rp_python_sdk.sdk_config import SdkConfig, CustomConfig
409
- from rp_python_sdk.telemetry.telemetry_config import TelemetryConfig
410
-
411
- client = RelyingPartyClientSdk(
412
- config=SdkConfig(
413
- # ... other SdkConfig fields ...
414
- custom_config=CustomConfig(
415
- telemetry_config=TelemetryConfig(enabled=False)
416
- )
417
- ),
418
- )
419
- ```
401
+ > **Note:** Telemetry is **enabled by default**. To opt out, set `TelemetryConfig(enabled=False)` on your `CustomConfig`.
420
402
 
421
403
  When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
422
404
  threads, so there is no overhead.
@@ -597,6 +579,14 @@ poetry publish
597
579
 
598
580
  ## Release Notes
599
581
 
582
+ ### 1.5.2 (Sep 30, 2026)
583
+ - **Telemetry re-enabled by default**: Telemetry is now enabled by default again after the concurrency and shutdown bugfixes below. Opt out with `TelemetryConfig(enabled=False)`.
584
+ - **Concurrent telemetry session isolation**: Fixed a race condition where overlapping SDK operations could share or corrupt telemetry session state.
585
+ - **Daemon thread shutdown with atexit handler**: Fixed an issue where daemon worker threads were not cleaned up when `close()` was not called explicitly, by registering an `atexit` handler for graceful shutdown.
586
+
587
+ ### 1.5.1 (Sep 25, 2026)
588
+ - **Telemetry disabled by default**: Telemetry was temporarily disabled while a bugfix was in progress.
589
+
600
590
  ### 1.5.0 (Sep 22, 2026)
601
591
  - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
602
592
  - Supports ASYNC (default) and SYNC delivery modes
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rp_python_sdk"
3
- version = "1.5.0"
3
+ version = "1.5.2"
4
4
  description = "Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem"
5
5
  authors = ["Erik Pragt <erik.pragt@connectid.com.au>"]
6
6
  license = "Proprietary"
@@ -0,0 +1,2 @@
1
+ def get_version_info() -> str:
2
+ return "1.5.2" # Version number
@@ -25,13 +25,24 @@ def _create_telemetry_sender(config: SdkConfig) -> TelemetrySender:
25
25
  logger.info("Telemetry is disabled; no telemetry events will be sent.")
26
26
  return NoopTelemetrySender.instance()
27
27
 
28
- logger.info("Telemetry is enabled; telemetry events will be sent to the telemetry service.")
29
- transport = TelemetryTransport.create(config)
30
- if telemetry.delivery_mode is TelemetryDeliveryMode.SYNC:
31
- return SyncTelemetrySender(transport, telemetry.sync_send_budget_seconds)
28
+ transport: Optional[TelemetryTransport] = None
29
+ try:
30
+ transport = TelemetryTransport.create(config)
31
+ if telemetry.delivery_mode is TelemetryDeliveryMode.SYNC:
32
+ sender: TelemetrySender = SyncTelemetrySender(
33
+ transport, telemetry.sync_send_budget_seconds)
34
+ else:
35
+ dispatcher = TelemetryDispatcher(
36
+ telemetry.queue_capacity, telemetry.max_concurrent_sends)
37
+ sender = AsyncTelemetrySender(transport, dispatcher)
38
+ except Exception as exception:
39
+ if transport is not None:
40
+ transport.close()
41
+ logger.warning(f"Failed to initialise telemetry; telemetry is disabled: {exception}")
42
+ return NoopTelemetrySender.instance()
32
43
 
33
- dispatcher = TelemetryDispatcher(telemetry.queue_capacity, telemetry.max_concurrent_sends)
34
- return AsyncTelemetrySender(transport, dispatcher)
44
+ logger.info("Telemetry is enabled; telemetry events will be sent to the telemetry service.")
45
+ return sender
35
46
 
36
47
 
37
48
  def _close_telemetry_sender(sender: TelemetrySender) -> None:
@@ -1,14 +1,41 @@
1
+ import atexit
1
2
  import queue
2
3
  import threading
3
4
  import time
5
+ import weakref
4
6
  from typing import Callable, List
5
7
 
6
8
  from rp_python_sdk.setup_logger import logger
7
9
 
8
10
  DEFAULT_GRACE_PERIOD_SECONDS = 2.0
11
+ DEFAULT_EXIT_GRACE_PERIOD_SECONDS = 12.0
9
12
  DEFAULT_IDLE_TIMEOUT_SECONDS = 30.0
10
13
  POLL_INTERVAL_SECONDS = 0.1
11
14
 
15
+ # Weak references to all live dispatchers. The atexit handler iterates these and
16
+ # calls close() on any that are still alive, ensuring daemon worker threads are
17
+ # joined before the interpreter tears down — preventing the
18
+ # 'Fatal Python error: Aborted' on Python 3.12/Linux when threads are in C-level SSL code.
19
+ _live_dispatchers: weakref.WeakSet["TelemetryDispatcher"] = weakref.WeakSet()
20
+ _atexit_registered = False
21
+ _atexit_lock = threading.Lock()
22
+
23
+
24
+ def _shutdown_all_dispatchers() -> None:
25
+ for dispatcher in list(_live_dispatchers):
26
+ try:
27
+ dispatcher.close(grace_period_seconds=DEFAULT_EXIT_GRACE_PERIOD_SECONDS)
28
+ except Exception as exception:
29
+ logger.debug(f"Ignoring error while closing telemetry dispatcher at exit: {exception}")
30
+
31
+
32
+ def _register_atexit_hook() -> None:
33
+ global _atexit_registered
34
+ with _atexit_lock:
35
+ if not _atexit_registered:
36
+ atexit.register(_shutdown_all_dispatchers)
37
+ _atexit_registered = True
38
+
12
39
 
13
40
  class TelemetryDispatcher:
14
41
 
@@ -32,6 +59,9 @@ class TelemetryDispatcher:
32
59
  # Monotonic so a replacement worker never reuses a retired worker's thread name.
33
60
  self._next_worker_number = 0
34
61
 
62
+ _live_dispatchers.add(self)
63
+ _register_atexit_hook()
64
+
35
65
  def enqueue(self, task: Callable[[], None]) -> bool:
36
66
  """Queues a task for background execution, returning False when it was dropped.
37
67
 
@@ -54,11 +84,15 @@ class TelemetryDispatcher:
54
84
 
55
85
  def close(self, grace_period_seconds: float = DEFAULT_GRACE_PERIOD_SECONDS) -> None:
56
86
  with self._lock:
87
+ if self._shutdown.is_set():
88
+ return
57
89
  self._shutdown.set()
58
90
  self._discard_pending()
59
91
  workers = list(self._workers)
60
92
  self._workers.clear()
61
93
 
94
+ _live_dispatchers.discard(self)
95
+
62
96
  deadline = time.monotonic() + grace_period_seconds
63
97
  for worker in workers:
64
98
  remaining = max(0.0, deadline - time.monotonic())
@@ -0,0 +1,81 @@
1
+ import queue
2
+ import threading
3
+ from typing import Iterable, Optional
4
+
5
+ import requests
6
+
7
+ from rp_python_sdk.setup_logger import logger
8
+
9
+
10
+ class TelemetrySessionPool:
11
+ """Hands each telemetry send exclusive ownership of one HTTP session.
12
+
13
+ ``requests.Session`` contains mutable adapter and connection-pool state. A session therefore
14
+ stays leased to one worker for the complete token-and-event exchange. Shutdown closes idle
15
+ sessions immediately, while a leased session is closed by the worker that returns it. This
16
+ keeps shutdown bounded without closing TLS resources underneath an in-flight request.
17
+ """
18
+
19
+ def __init__(self, sessions: Iterable[requests.Session]) -> None:
20
+ available_sessions = list(sessions)
21
+ if not available_sessions:
22
+ raise ValueError("At least one telemetry session is required")
23
+
24
+ self._available: queue.LifoQueue[requests.Session] = queue.LifoQueue(
25
+ maxsize=len(available_sessions)
26
+ )
27
+ for session in available_sessions:
28
+ self._available.put_nowait(session)
29
+
30
+ self._lock = threading.Lock()
31
+ self._closed = False
32
+
33
+ def acquire(self) -> Optional[requests.Session]:
34
+ """Returns an exclusively leased session, or None without blocking."""
35
+ with self._lock:
36
+ if self._closed:
37
+ return None
38
+ try:
39
+ return self._available.get_nowait()
40
+ except queue.Empty:
41
+ return None
42
+
43
+ def release(self, session: requests.Session) -> None:
44
+ """Returns a lease, closing it instead when pool shutdown has begun."""
45
+ close_session = False
46
+ with self._lock:
47
+ if self._closed:
48
+ close_session = True
49
+ else:
50
+ try:
51
+ self._available.put_nowait(session)
52
+ except queue.Full:
53
+ # A duplicate release is a lifecycle bug, but telemetry cleanup must not
54
+ # surface it to application code. Close the redundant resource instead.
55
+ close_session = True
56
+
57
+ if close_session:
58
+ self._close_session(session)
59
+
60
+ def close(self) -> None:
61
+ """Prevents new leases and closes idle resources without waiting for active sends."""
62
+ idle_sessions: list[requests.Session] = []
63
+ with self._lock:
64
+ if self._closed:
65
+ return
66
+ self._closed = True
67
+ while True:
68
+ try:
69
+ idle_sessions.append(self._available.get_nowait())
70
+ except queue.Empty:
71
+ break
72
+
73
+ for session in idle_sessions:
74
+ self._close_session(session)
75
+
76
+ @staticmethod
77
+ def _close_session(session: requests.Session) -> None:
78
+ try:
79
+ session.close()
80
+ except Exception as exception:
81
+ logger.debug(f"Ignoring error while closing telemetry HTTP session: {exception}")
@@ -27,22 +27,24 @@ class TelemetryTokenProvider:
27
27
  self._expires_at: float = 0.0
28
28
  self._discovery_document: Optional[dict[str, Any]] = None
29
29
 
30
- def get_access_token(self, timeout: RequestsTimeout) -> Optional[str]:
30
+ def get_access_token(self, timeout: RequestsTimeout,
31
+ session: Optional[requests.Session] = None) -> Optional[str]:
31
32
  if self._is_token_valid():
32
33
  return self._access_token
33
34
 
34
35
  with self._lock:
35
36
  if self._is_token_valid():
36
37
  return self._access_token
37
- return self._refresh_token(timeout)
38
+ selected_session = session if session is not None else self._session
39
+ return self._refresh_token(timeout, selected_session)
38
40
 
39
41
  def _is_token_valid(self) -> bool:
40
42
  return (self._access_token is not None
41
43
  and time.time() + self.TOKEN_REFRESH_BUFFER_SECONDS < self._expires_at)
42
44
 
43
- def _refresh_token(self, timeout: RequestsTimeout) -> Optional[str]:
45
+ def _refresh_token(self, timeout: RequestsTimeout, session: requests.Session) -> Optional[str]:
44
46
  try:
45
- discovery_document = self._get_discovery_document(timeout)
47
+ discovery_document = self._get_discovery_document(timeout, session)
46
48
  token_endpoint = self._resolve_token_endpoint(discovery_document)
47
49
  audience = discovery_document.get("issuer") or token_endpoint
48
50
 
@@ -57,7 +59,7 @@ class TelemetryTokenProvider:
57
59
  "client_assertion_type": _CLIENT_ASSERTION_TYPE,
58
60
  }
59
61
 
60
- response = self._session.post(token_endpoint, data=data, timeout=timeout)
62
+ response = session.post(token_endpoint, data=data, timeout=timeout)
61
63
  response.raise_for_status()
62
64
  token_response = response.json()
63
65
 
@@ -73,11 +75,12 @@ class TelemetryTokenProvider:
73
75
  self._expires_at = 0.0
74
76
  return None
75
77
 
76
- def _get_discovery_document(self, timeout: RequestsTimeout) -> dict[str, Any]:
78
+ def _get_discovery_document(self, timeout: RequestsTimeout,
79
+ session: requests.Session) -> dict[str, Any]:
77
80
  if self._discovery_document is not None:
78
81
  return self._discovery_document
79
82
 
80
- response = self._session.get(self._registry_discovery_url, timeout=timeout)
83
+ response = session.get(self._registry_discovery_url, timeout=timeout)
81
84
  response.raise_for_status()
82
85
  discovery_document: dict[str, Any] = response.json()
83
86
 
@@ -89,4 +92,3 @@ class TelemetryTokenProvider:
89
92
  mtls_aliases = discovery_document.get("mtls_endpoint_aliases") or {}
90
93
  token_endpoint = mtls_aliases.get("token_endpoint") or discovery_document["token_endpoint"]
91
94
  return str(token_endpoint)
92
-
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, Optional
2
+ from typing import Any, Optional, Sequence
3
3
 
4
4
  import requests
5
5
 
@@ -7,6 +7,7 @@ from rp_python_sdk.sdk_config import SdkConfig
7
7
  from rp_python_sdk.setup_logger import logger
8
8
  from rp_python_sdk.telemetry.telemetry_event import TelemetryEvent
9
9
  from rp_python_sdk.telemetry.telemetry_session import create_telemetry_session
10
+ from rp_python_sdk.telemetry.telemetry_session_pool import TelemetrySessionPool
10
11
  from rp_python_sdk.telemetry.telemetry_token_provider import (
11
12
  TelemetryTokenProvider,
12
13
  RequestsTimeout,
@@ -19,19 +20,34 @@ _JSON_MEDIA_TYPE = "application/json"
19
20
 
20
21
  class TelemetryTransport:
21
22
  def __init__(self, session: requests.Session, token_provider: TelemetryTokenProvider,
22
- telemetry_endpoint_url: str) -> None:
23
- self._session = session
23
+ telemetry_endpoint_url: str,
24
+ additional_sessions: Sequence[requests.Session] = ()) -> None:
25
+ self._sessions = TelemetrySessionPool([session, *additional_sessions])
24
26
  self._token_provider = token_provider
25
27
  self._telemetry_endpoint_url = telemetry_endpoint_url
26
28
 
27
29
  @classmethod
28
30
  def create(cls, config: SdkConfig) -> "TelemetryTransport":
29
31
  telemetry_urls = resolve_telemetry_urls(config)
30
- session = create_telemetry_session(config, config.custom_config.telemetry_config.max_concurrent_sends)
31
- token_provider = TelemetryTokenProvider(session, config,
32
- telemetry_urls.registry_discovery_document_url)
33
-
34
- return cls(session, token_provider, telemetry_urls.telemetry_endpoint_url)
32
+ session_count = config.custom_config.telemetry_config.max_concurrent_sends
33
+ sessions: list[requests.Session] = []
34
+ try:
35
+ # A session is never shared between workers. Each session needs only one connection
36
+ # slot because the dispatcher already bounds the number of concurrent sends.
37
+ for _ in range(session_count):
38
+ sessions.append(create_telemetry_session(config, pool_size=1))
39
+ token_provider = TelemetryTokenProvider(
40
+ sessions[0], config, telemetry_urls.registry_discovery_document_url)
41
+ return cls(sessions[0], token_provider, telemetry_urls.telemetry_endpoint_url,
42
+ additional_sessions=sessions[1:])
43
+ except Exception:
44
+ for session in sessions:
45
+ try:
46
+ session.close()
47
+ except Exception as close_error:
48
+ logger.debug(f"Ignoring error while cleaning up telemetry HTTP session: "
49
+ f"{close_error}")
50
+ raise
35
51
 
36
52
  @property
37
53
  def token_provider(self) -> TelemetryTokenProvider:
@@ -39,13 +55,20 @@ class TelemetryTransport:
39
55
 
40
56
  def send(self, event: TelemetryEvent, timeout: RequestsTimeout,
41
57
  token: Optional[str] = None) -> None:
58
+ session = self._sessions.acquire()
59
+ if session is None:
60
+ logger.debug(f"Skipping telemetry event type {event.type} "
61
+ f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}): "
62
+ f"no exclusive HTTP session is available.")
63
+ return
64
+
42
65
  try:
43
66
  logger.info(f"Dispatching telemetry event type {event.type} "
44
67
  f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}) "
45
68
  f"to {self._telemetry_endpoint_url}")
46
69
 
47
70
  if token is None:
48
- token = self._token_provider.get_access_token(timeout)
71
+ token = self._token_provider.get_access_token(timeout, session)
49
72
  if token is None:
50
73
  logger.warning(f"Skipping telemetry event type {event.type} "
51
74
  f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}): "
@@ -59,9 +82,9 @@ class TelemetryTransport:
59
82
  _FAPI_INTERACTION_ID_HEADER: event.x_fapi_interaction_id,
60
83
  }
61
84
 
62
- response = self._session.post(self._telemetry_endpoint_url, data=body,
63
- headers=headers,
64
- timeout=timeout)
85
+ response = session.post(self._telemetry_endpoint_url, data=body,
86
+ headers=headers,
87
+ timeout=timeout)
65
88
 
66
89
  if not response.ok:
67
90
  logger.warning(f"Telemetry send for event type {event.type} "
@@ -74,6 +97,8 @@ class TelemetryTransport:
74
97
  except Exception as exception:
75
98
  logger.warning(f"Failed to send telemetry event type {event.type} "
76
99
  f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}): {exception}")
100
+ finally:
101
+ self._sessions.release(session)
77
102
 
78
103
  def close(self) -> None:
79
- self._session.close()
104
+ self._sessions.close()
@@ -1,2 +0,0 @@
1
- def get_version_info() -> str:
2
- return "1.5.0" # Version number