rp_python_sdk 1.4.1__tar.gz → 1.5.0__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 (36) hide show
  1. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/PKG-INFO +175 -1
  2. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/README.md +172 -0
  3. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/pyproject.toml +5 -1
  4. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py +41 -31
  5. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/retrieve_tokens.py +46 -46
  6. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/user_info.py +22 -6
  7. rp_python_sdk-1.5.0/rp_python_sdk/endpoints/util/client_assertion.py +28 -0
  8. rp_python_sdk-1.5.0/rp_python_sdk/endpoints/util/version_info.py +2 -0
  9. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/relying_party_client_sdk.py +58 -2
  10. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/sdk_config.py +2 -0
  11. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/__init__.py +21 -0
  12. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/async_telemetry_sender.py +36 -0
  13. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/sync_telemetry_sender.py +21 -0
  14. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_config.py +27 -0
  15. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_context.py +69 -0
  16. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_dispatcher.py +120 -0
  17. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_event.py +45 -0
  18. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_event_factory.py +107 -0
  19. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_sender.py +24 -0
  20. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_session.py +55 -0
  21. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_token_provider.py +92 -0
  22. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_transport.py +79 -0
  23. rp_python_sdk-1.5.0/rp_python_sdk/telemetry/telemetry_url_resolver.py +64 -0
  24. rp_python_sdk-1.4.1/rp_python_sdk/endpoints/util/version_info.py +0 -2
  25. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/__init__.py +0 -0
  26. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/cache/__init__.py +0 -0
  27. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/cache/http_cache.py +0 -0
  28. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/get_participants.py +0 -0
  29. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/util/fapi.py +0 -0
  30. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/util/mapping.py +0 -0
  31. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/util/system_info.py +0 -0
  32. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/endpoints/util/user_agent.py +0 -0
  33. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/filters/participant_filters.py +0 -0
  34. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/model.py +0 -0
  35. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
  36. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.0}/rp_python_sdk/setup_logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rp_python_sdk
3
- Version: 1.4.1
3
+ Version: 1.5.0
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
@@ -11,7 +11,9 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Programming Language :: Python :: 3.15
14
15
  Requires-Dist: authlib (>=1.7.2,<2.0.0)
16
+ Requires-Dist: certifi (>=2026.4.22,<2027.0.0)
15
17
  Requires-Dist: cffi (>=2.0.0,<3.0.0)
16
18
  Requires-Dist: cryptography (>=50.0.0)
17
19
  Requires-Dist: joserfc (>=1.6.8,<2.0.0)
@@ -414,6 +416,173 @@ The required function parameters are:
414
416
  The method will return a `Tokenset` that contains the access_token and id_token with the requested claims along
415
417
  with a unique `x-fapi-interaction-id` that was used for this request to the authorisation server.
416
418
 
419
+ ## Telemetry
420
+
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`.
424
+
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
+ ```
443
+
444
+ When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
445
+ threads, so there is no overhead.
446
+
447
+ ### Delivery mode and budget
448
+
449
+ The `delivery_mode` field selects how events are delivered:
450
+
451
+ - `TelemetryDeliveryMode.ASYNC` (default): fire-and-forget. Events are placed on a bounded in-memory queue
452
+ that is drained by daemon worker threads, so the SDK operation returns without waiting for any network I/O.
453
+ - `TelemetryDeliveryMode.SYNC`: events are delivered inline, before the SDK operation returns. Use this for
454
+ hosts that may freeze or terminate before background work completes (for example AWS Lambda), where a
455
+ fire-and-forget event could be lost. Inline delivery is bounded by `sync_send_budget_seconds` (default `1.0`).
456
+
457
+ ```python
458
+ from rp_python_sdk.telemetry.telemetry_config import TelemetryConfig, TelemetryDeliveryMode
459
+
460
+ CustomConfig(
461
+ telemetry_config=TelemetryConfig(
462
+ delivery_mode=TelemetryDeliveryMode.SYNC,
463
+ sync_send_budget_seconds=1.0,
464
+ )
465
+ )
466
+ ```
467
+
468
+ Note on the SYNC budget: `sync_send_budget_seconds` bounds the **per-phase inactivity** (the connect and read
469
+ timeouts) of the underlying HTTP calls, not the total wall-clock latency of delivery. A degraded telemetry
470
+ endpoint that stays marginally responsive can therefore add more than the configured budget to your SDK call.
471
+ The budget is applied as a remaining-time budget across the token refresh and the event POST, so no single
472
+ phase is given the full budget afresh.
473
+
474
+ `TelemetryConfig` fields:
475
+
476
+ | Field | Default | Description |
477
+ | --- | --- | --- |
478
+ | `enabled` | `True` | Master on/off switch for telemetry. |
479
+ | `endpoint_url` | `None` | Override for the telemetry ingest URL. When `None`, the URL is derived from the SDK environment. |
480
+ | `registry_discovery_document_url` | `None` | Override for the Registry OIDC discovery document URL used to obtain a telemetry access token. When `None`, derived from the environment. |
481
+ | `delivery_mode` | `TelemetryDeliveryMode.ASYNC` | `ASYNC` (background dispatch) or `SYNC` (inline delivery). |
482
+ | `sync_send_budget_seconds` | `1.0` | SYNC-only per-phase inactivity budget in seconds. Must be greater than 0. |
483
+ | `queue_capacity` | `200` | ASYNC-only bound on the in-memory event queue. Events are dropped when the queue is full. Must be greater than 0. |
484
+ | `max_concurrent_sends` | `3` | ASYNC-only number of daemon worker threads draining the queue. Must be greater than 0. |
485
+
486
+ Under `SYNC` the queue is bypassed entirely, so `queue_capacity` and `max_concurrent_sends` are inert.
487
+
488
+ ### What is sent
489
+
490
+ One event per operation, tagged `par_request`, `token_request` or `userinfo_request`. Every event carries the same
491
+ top-level envelope; the `data` object varies by operation. Optional fields are omitted entirely when absent rather
492
+ than sent as `null`.
493
+
494
+ #### Top-level fields
495
+
496
+ | Field | Type | Description |
497
+ |-------------------------|---------|--------------------------------------------------------------------------------------|
498
+ | `sourceService` | string | The SDK that emitted the event; always `rp-python-sdk` |
499
+ | `userAgent` | string | The SDK user-agent string, including SDK name and version |
500
+ | `sdkVersion` | string | SDK version, for diagnosing version-specific issues |
501
+ | `authorisationServerId` | string | The identity provider the request was sent to |
502
+ | `clientId` | string | Your client id, as configured on the SDK |
503
+ | `timestamp` | string | When the operation completed (RFC 3339 / ISO-8601, UTC, `Z`-suffixed) |
504
+ | `xFapiInteractionId` | string | The same interaction id returned to you, for correlation |
505
+ | `durationMs` | number | How long the operation took, in milliseconds |
506
+ | `success` | boolean | Whether the operation succeeded |
507
+ | `type` | string | The operation: `par_request`, `token_request` or `userinfo_request` |
508
+ | `schemaVersion` | string | Telemetry schema version (currently `1.0`) |
509
+ | `data` | object | Per-operation detail (see below) |
510
+
511
+ #### `data` by operation
512
+
513
+ `errorDetails` is present only on failure; `expires_in` / `request_uri` and `transaction_id` are present only on
514
+ success.
515
+
516
+ **`par_request`**
517
+
518
+ | Field | Type | Description |
519
+ |------------------------------|---------|-------------------------------------------------------------------|
520
+ | `state` | string | The OAuth `state` sent on the request |
521
+ | `request.purpose` | string | The requested purpose |
522
+ | `request.body.claims` | object | The requested claims structure (claim *names* only, never values) |
523
+ | `response.statusCode` | number | HTTP status code of the PAR response |
524
+ | `response.errorDetails` | string | Error detail from the response, on failure |
525
+ | `response.body.expires_in` | number | `expires_in` from the PAR response, on success |
526
+ | `response.body.request_uri` | string | `request_uri` from the PAR response, on success |
527
+
528
+ **`token_request`**
529
+
530
+ | Field | Type | Description |
531
+ |---------------------------|---------|-----------------------------------------------------------------|
532
+ | `state` | string | The OAuth `state` associated with the token exchange |
533
+ | `request.grant_type` | string | The grant type used (e.g. `authorization_code`) |
534
+ | `response.statusCode` | number | HTTP status code of the token response |
535
+ | `response.errorDetails` | string | Error detail from the response, on failure |
536
+ | `response.transaction_id` | string | The `txn` transaction identifier from the token response, on success |
537
+
538
+ **`userinfo_request`**
539
+
540
+ | Field | Type | Description |
541
+ |---------------------------|---------|---------------------------------------------|
542
+ | `response.statusCode` | number | HTTP status code of the userinfo response |
543
+ | `response.errorDetails` | string | Error detail from the response, on failure |
544
+
545
+ UserInfo events carry no `state` and no `request` object.
546
+
547
+ The following are **never** collected:
548
+
549
+ - no access, refresh or ID tokens
550
+ - no authorization codes
551
+ - no client credentials or client assertions
552
+ - no nonces
553
+ - no code verifiers or code challenges
554
+ - no request-object JWTs
555
+ - no full request or response bodies
556
+ - no claim values (beyond the transaction id)
557
+
558
+ Events are authenticated with the SDK's existing transport certificate (mutual TLS) and a signed client assertion —
559
+ the same credentials the SDK already uses for the ConnectID Registry — so no extra configuration or credentials are
560
+ needed.
561
+
562
+ ### Releasing telemetry resources (`close()` and context-manager use)
563
+
564
+ `RelyingPartyClientSdk` exposes a `close()` method and supports the context-manager protocol. Closing signals
565
+ the background dispatcher to stop, discards any queued events, and joins the worker threads within a bounded
566
+ grace period, releasing the transport session. `close()` is idempotent and never raises.
567
+
568
+ Prefer the context manager so resources are released automatically:
569
+
570
+ ```python
571
+ with RelyingPartyClientSdk(config) as sdk:
572
+ participants = sdk.get_participants()
573
+ # telemetry resources are released on exit
574
+ ```
575
+
576
+ Or call `close()` explicitly when you manage the lifecycle yourself:
577
+
578
+ ```python
579
+ sdk = RelyingPartyClientSdk(config)
580
+ try:
581
+ participants = sdk.get_participants()
582
+ finally:
583
+ sdk.close()
584
+ ```
585
+
417
586
  ## Conformance test
418
587
 
419
588
  An automated conformance test has been added. To execute the conformance test, a token is required,
@@ -451,6 +620,11 @@ poetry publish
451
620
 
452
621
  ## Release Notes
453
622
 
623
+ ### 1.5.0 (Sep 22, 2026)
624
+ - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
625
+ - Supports ASYNC (default) and SYNC delivery modes
626
+ - Configurable via `TelemetryConfig` on `CustomConfig` — disable with `TelemetryConfig(enabled=False)`
627
+
454
628
  ### 1.4.1 (Aug 4, 2026)
455
629
  - Security: require cryptography 50.0.0 or newer to address CVE-2026-69247 and CVE-2026-69249
456
630
  - Removed the cryptography major-version ceiling so consumers can adopt future security releases without waiting for an SDK release
@@ -393,6 +393,173 @@ The required function parameters are:
393
393
  The method will return a `Tokenset` that contains the access_token and id_token with the requested claims along
394
394
  with a unique `x-fapi-interaction-id` that was used for this request to the authorisation server.
395
395
 
396
+ ## Telemetry
397
+
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`.
401
+
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
+ ```
420
+
421
+ When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
422
+ threads, so there is no overhead.
423
+
424
+ ### Delivery mode and budget
425
+
426
+ The `delivery_mode` field selects how events are delivered:
427
+
428
+ - `TelemetryDeliveryMode.ASYNC` (default): fire-and-forget. Events are placed on a bounded in-memory queue
429
+ that is drained by daemon worker threads, so the SDK operation returns without waiting for any network I/O.
430
+ - `TelemetryDeliveryMode.SYNC`: events are delivered inline, before the SDK operation returns. Use this for
431
+ hosts that may freeze or terminate before background work completes (for example AWS Lambda), where a
432
+ fire-and-forget event could be lost. Inline delivery is bounded by `sync_send_budget_seconds` (default `1.0`).
433
+
434
+ ```python
435
+ from rp_python_sdk.telemetry.telemetry_config import TelemetryConfig, TelemetryDeliveryMode
436
+
437
+ CustomConfig(
438
+ telemetry_config=TelemetryConfig(
439
+ delivery_mode=TelemetryDeliveryMode.SYNC,
440
+ sync_send_budget_seconds=1.0,
441
+ )
442
+ )
443
+ ```
444
+
445
+ Note on the SYNC budget: `sync_send_budget_seconds` bounds the **per-phase inactivity** (the connect and read
446
+ timeouts) of the underlying HTTP calls, not the total wall-clock latency of delivery. A degraded telemetry
447
+ endpoint that stays marginally responsive can therefore add more than the configured budget to your SDK call.
448
+ The budget is applied as a remaining-time budget across the token refresh and the event POST, so no single
449
+ phase is given the full budget afresh.
450
+
451
+ `TelemetryConfig` fields:
452
+
453
+ | Field | Default | Description |
454
+ | --- | --- | --- |
455
+ | `enabled` | `True` | Master on/off switch for telemetry. |
456
+ | `endpoint_url` | `None` | Override for the telemetry ingest URL. When `None`, the URL is derived from the SDK environment. |
457
+ | `registry_discovery_document_url` | `None` | Override for the Registry OIDC discovery document URL used to obtain a telemetry access token. When `None`, derived from the environment. |
458
+ | `delivery_mode` | `TelemetryDeliveryMode.ASYNC` | `ASYNC` (background dispatch) or `SYNC` (inline delivery). |
459
+ | `sync_send_budget_seconds` | `1.0` | SYNC-only per-phase inactivity budget in seconds. Must be greater than 0. |
460
+ | `queue_capacity` | `200` | ASYNC-only bound on the in-memory event queue. Events are dropped when the queue is full. Must be greater than 0. |
461
+ | `max_concurrent_sends` | `3` | ASYNC-only number of daemon worker threads draining the queue. Must be greater than 0. |
462
+
463
+ Under `SYNC` the queue is bypassed entirely, so `queue_capacity` and `max_concurrent_sends` are inert.
464
+
465
+ ### What is sent
466
+
467
+ One event per operation, tagged `par_request`, `token_request` or `userinfo_request`. Every event carries the same
468
+ top-level envelope; the `data` object varies by operation. Optional fields are omitted entirely when absent rather
469
+ than sent as `null`.
470
+
471
+ #### Top-level fields
472
+
473
+ | Field | Type | Description |
474
+ |-------------------------|---------|--------------------------------------------------------------------------------------|
475
+ | `sourceService` | string | The SDK that emitted the event; always `rp-python-sdk` |
476
+ | `userAgent` | string | The SDK user-agent string, including SDK name and version |
477
+ | `sdkVersion` | string | SDK version, for diagnosing version-specific issues |
478
+ | `authorisationServerId` | string | The identity provider the request was sent to |
479
+ | `clientId` | string | Your client id, as configured on the SDK |
480
+ | `timestamp` | string | When the operation completed (RFC 3339 / ISO-8601, UTC, `Z`-suffixed) |
481
+ | `xFapiInteractionId` | string | The same interaction id returned to you, for correlation |
482
+ | `durationMs` | number | How long the operation took, in milliseconds |
483
+ | `success` | boolean | Whether the operation succeeded |
484
+ | `type` | string | The operation: `par_request`, `token_request` or `userinfo_request` |
485
+ | `schemaVersion` | string | Telemetry schema version (currently `1.0`) |
486
+ | `data` | object | Per-operation detail (see below) |
487
+
488
+ #### `data` by operation
489
+
490
+ `errorDetails` is present only on failure; `expires_in` / `request_uri` and `transaction_id` are present only on
491
+ success.
492
+
493
+ **`par_request`**
494
+
495
+ | Field | Type | Description |
496
+ |------------------------------|---------|-------------------------------------------------------------------|
497
+ | `state` | string | The OAuth `state` sent on the request |
498
+ | `request.purpose` | string | The requested purpose |
499
+ | `request.body.claims` | object | The requested claims structure (claim *names* only, never values) |
500
+ | `response.statusCode` | number | HTTP status code of the PAR response |
501
+ | `response.errorDetails` | string | Error detail from the response, on failure |
502
+ | `response.body.expires_in` | number | `expires_in` from the PAR response, on success |
503
+ | `response.body.request_uri` | string | `request_uri` from the PAR response, on success |
504
+
505
+ **`token_request`**
506
+
507
+ | Field | Type | Description |
508
+ |---------------------------|---------|-----------------------------------------------------------------|
509
+ | `state` | string | The OAuth `state` associated with the token exchange |
510
+ | `request.grant_type` | string | The grant type used (e.g. `authorization_code`) |
511
+ | `response.statusCode` | number | HTTP status code of the token response |
512
+ | `response.errorDetails` | string | Error detail from the response, on failure |
513
+ | `response.transaction_id` | string | The `txn` transaction identifier from the token response, on success |
514
+
515
+ **`userinfo_request`**
516
+
517
+ | Field | Type | Description |
518
+ |---------------------------|---------|---------------------------------------------|
519
+ | `response.statusCode` | number | HTTP status code of the userinfo response |
520
+ | `response.errorDetails` | string | Error detail from the response, on failure |
521
+
522
+ UserInfo events carry no `state` and no `request` object.
523
+
524
+ The following are **never** collected:
525
+
526
+ - no access, refresh or ID tokens
527
+ - no authorization codes
528
+ - no client credentials or client assertions
529
+ - no nonces
530
+ - no code verifiers or code challenges
531
+ - no request-object JWTs
532
+ - no full request or response bodies
533
+ - no claim values (beyond the transaction id)
534
+
535
+ Events are authenticated with the SDK's existing transport certificate (mutual TLS) and a signed client assertion —
536
+ the same credentials the SDK already uses for the ConnectID Registry — so no extra configuration or credentials are
537
+ needed.
538
+
539
+ ### Releasing telemetry resources (`close()` and context-manager use)
540
+
541
+ `RelyingPartyClientSdk` exposes a `close()` method and supports the context-manager protocol. Closing signals
542
+ the background dispatcher to stop, discards any queued events, and joins the worker threads within a bounded
543
+ grace period, releasing the transport session. `close()` is idempotent and never raises.
544
+
545
+ Prefer the context manager so resources are released automatically:
546
+
547
+ ```python
548
+ with RelyingPartyClientSdk(config) as sdk:
549
+ participants = sdk.get_participants()
550
+ # telemetry resources are released on exit
551
+ ```
552
+
553
+ Or call `close()` explicitly when you manage the lifecycle yourself:
554
+
555
+ ```python
556
+ sdk = RelyingPartyClientSdk(config)
557
+ try:
558
+ participants = sdk.get_participants()
559
+ finally:
560
+ sdk.close()
561
+ ```
562
+
396
563
  ## Conformance test
397
564
 
398
565
  An automated conformance test has been added. To execute the conformance test, a token is required,
@@ -430,6 +597,11 @@ poetry publish
430
597
 
431
598
  ## Release Notes
432
599
 
600
+ ### 1.5.0 (Sep 22, 2026)
601
+ - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
602
+ - Supports ASYNC (default) and SYNC delivery modes
603
+ - Configurable via `TelemetryConfig` on `CustomConfig` — disable with `TelemetryConfig(enabled=False)`
604
+
433
605
  ### 1.4.1 (Aug 4, 2026)
434
606
  - Security: require cryptography 50.0.0 or newer to address CVE-2026-69247 and CVE-2026-69249
435
607
  - Removed the cryptography major-version ceiling so consumers can adopt future security releases without waiting for an SDK release
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rp_python_sdk"
3
- version = "1.4.1"
3
+ version = "1.5.0"
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"
@@ -14,11 +14,15 @@ cryptography = ">=50.0.0"
14
14
  joserfc = "^1.6.8"
15
15
  cffi = "^2.0.0"
16
16
  urllib3 = "^2.7.0"
17
+ # Already present transitively via requests; declared explicitly because the telemetry TLS
18
+ # context imports it directly to trust the public roots that sign the Registry endpoints.
19
+ certifi = "^2026.4.22"
17
20
 
18
21
  [tool.poetry.group.test.dependencies]
19
22
  pytest = "^9.0.0"
20
23
  httpx = "^0.28.1"
21
24
  toml = "^0.10.2"
25
+ jsonschema = { extras = ["format-nongpl"], version = "^4.25.1" }
22
26
 
23
27
  [tool.poetry.group.dev.dependencies]
24
28
  black = "^26.5.1"
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import time
3
- import uuid
4
3
  from typing import Any
5
4
  from urllib.parse import quote
6
5
 
@@ -11,12 +10,16 @@ from authlib.oauth2.rfc7636 import create_s256_code_challenge
11
10
 
12
11
  from rp_python_sdk.endpoints.get_participants import get_auth_server_details, \
13
12
  get_issuer_metadata
13
+ from rp_python_sdk.endpoints.util.client_assertion import create_client_assertion
14
14
  from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id, log_x_fapi_interaction_id_matches
15
15
  from rp_python_sdk.model import PARResponse
16
16
  from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
17
17
  from rp_python_sdk.sdk_config import SdkConfig
18
18
  from rp_python_sdk.setup_logger import logger
19
19
  from rp_python_sdk.cache.http_cache import HttpCache
20
+ from rp_python_sdk.telemetry.telemetry_context import ParTelemetryContext
21
+ from rp_python_sdk.telemetry.telemetry_event_factory import create_par_request_event
22
+ from rp_python_sdk.telemetry.telemetry_sender import NoopTelemetrySender, TelemetrySender
20
23
 
21
24
  extended_claims = {"over16", "over18", "over21", "over25", "over65", "beneficiary_account_au",
22
25
  "beneficiary_account_au_payid", "beneficiary_account_international"}
@@ -27,7 +30,8 @@ def send_pushed_authorisation_request(config: SdkConfig,
27
30
  essential_claims: set[str],
28
31
  voluntary_claims: set[str],
29
32
  purpose: str,
30
- cache: HttpCache) -> PARResponse:
33
+ cache: HttpCache,
34
+ telemetry_sender: TelemetrySender = NoopTelemetrySender.instance()) -> PARResponse:
31
35
  _check_valid_purpose(purpose)
32
36
 
33
37
  checked_essential_claims = _ensure_mandatory_claims_present(essential_claims, voluntary_claims)
@@ -39,7 +43,7 @@ def send_pushed_authorisation_request(config: SdkConfig,
39
43
  auth_server_url = issuer_metadata.issuer
40
44
  x_fapi_interaction_id = create_x_fapi_interaction_id()
41
45
 
42
- logger.info(
46
+ logger.debug(
43
47
  f"Sending PAR to auth server: {authorisation_server_id} - {authorisation_server.customer_friendly_name}, "
44
48
  f"essential claims requested: {checked_essential_claims}, voluntary claims requested: {voluntary_claims}, "
45
49
  f"x-fapi-interaction-id: {x_fapi_interaction_id}, purpose: {purpose}")
@@ -57,7 +61,6 @@ def send_pushed_authorisation_request(config: SdkConfig,
57
61
  state = generate_token()
58
62
  nonce = generate_token()
59
63
  scope = "openid"
60
- jti = str(uuid.uuid4())
61
64
 
62
65
  code_verifier = generate_token(48)
63
66
  code_challenge = create_s256_code_challenge(code_verifier)
@@ -84,22 +87,12 @@ def send_pushed_authorisation_request(config: SdkConfig,
84
87
  jws = JsonWebSignature()
85
88
  request_jwt = jws.serialize_compact(header, json.dumps(request_payload), config.signing_key)
86
89
 
87
- client_assertion_payload = {
88
- 'sub': client_id,
89
- 'aud': auth_server_url,
90
- 'iss': client_id,
91
- 'exp': exp,
92
- 'jti': jti,
93
- 'iat': iat,
94
- }
95
-
96
- client_assertion_jwt = jws.serialize_compact(header, json.dumps(client_assertion_payload),
97
- config.signing_key)
90
+ client_assertion_jwt = create_client_assertion(config.signing_key, config.signing_kid, client_id, auth_server_url)
98
91
 
99
92
  # Data payload
100
93
  data = {
101
94
  "request": request_jwt.decode("utf-8"),
102
- "client_assertion": client_assertion_jwt.decode("utf-8"),
95
+ "client_assertion": client_assertion_jwt,
103
96
  "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
104
97
  }
105
98
 
@@ -109,25 +102,42 @@ def send_pushed_authorisation_request(config: SdkConfig,
109
102
  "x-fapi-interaction-id": x_fapi_interaction_id
110
103
  }
111
104
 
112
- # Making the POST request
113
- response = requests.post(par_endpoint, data=data, headers=headers,
114
- cert=(config.transport_pem, config.transport_key),
115
- verify=config.ca_pem)
116
-
117
- log_x_fapi_interaction_id_matches(authorisation_server, "send_pushed_authorisation_request",
118
- x_fapi_interaction_id, response)
119
-
120
- if not response.ok:
121
- raise RelyingPartyClientSdkException(f"Response code for PAR to {authorisation_server_id} not successful, "
122
- f"was: {response.status_code} x-fapi-interaction-id: {x_fapi_interaction_id}")
123
-
124
- request_uri = response.json()['request_uri']
125
- logger.debug(f"PAR was sent, request_uri='{request_uri}'")
105
+ context = ParTelemetryContext()
106
+ http_status_code = None
107
+ try:
108
+ # Making the POST request
109
+ response = requests.post(par_endpoint, data=data, headers=headers,
110
+ cert=(config.transport_pem, config.transport_key),
111
+ verify=config.ca_pem)
112
+ http_status_code = response.status_code
113
+
114
+ log_x_fapi_interaction_id_matches(authorisation_server, "send_pushed_authorisation_request",
115
+ x_fapi_interaction_id, response)
116
+
117
+ if not response.ok:
118
+ raise RelyingPartyClientSdkException(
119
+ f"Response code for PAR to {authorisation_server_id} not successful, "
120
+ f"was: {response.status_code} x-fapi-interaction-id: {x_fapi_interaction_id}, "
121
+ f"response body: {response.text}")
122
+
123
+ body = response.json()
124
+ raw_expires_in = body.get("expires_in")
125
+ expires_in = raw_expires_in if isinstance(raw_expires_in, int) else None
126
+ request_uri = body['request_uri']
127
+ logger.debug(f"PAR was sent, request_uri='{request_uri}'")
128
+
129
+ context.record_success(http_status_code, expires_in, request_uri)
130
+ except Exception as exception:
131
+ context.record_failure(http_status_code, str(exception))
132
+ raise
133
+ finally:
134
+ event = create_par_request_event(config, x_fapi_interaction_id, authorisation_server_id,
135
+ state, purpose, claim_request, context)
136
+ telemetry_sender.send(event)
126
137
 
127
138
  return PARResponse(_construct_auth_url(auth_endpoint, client_id, request_uri),
128
139
  code_verifier, state, nonce, x_fapi_interaction_id)
129
140
 
130
-
131
141
  def _check_valid_purpose(purpose: str) -> None:
132
142
  if len(purpose) < 3 or len(purpose) > 300:
133
143
  raise RelyingPartyClientSdkException("Purpose must be between 3 and 300 characters")