rp_python_sdk 1.1.7__tar.gz → 1.2.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 (22) hide show
  1. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/PKG-INFO +31 -2
  2. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/README.md +30 -1
  3. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/pyproject.toml +2 -2
  4. rp_python_sdk-1.2.0/rp_python_sdk/cache/__init__.py +0 -0
  5. rp_python_sdk-1.2.0/rp_python_sdk/cache/http_cache.py +117 -0
  6. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/get_participants.py +43 -15
  7. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py +5 -3
  8. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/retrieve_tokens.py +23 -8
  9. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/user_info.py +4 -3
  10. rp_python_sdk-1.2.0/rp_python_sdk/endpoints/util/version_info.py +2 -0
  11. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/relying_party_client_sdk.py +6 -4
  12. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/sdk_config.py +4 -0
  13. rp_python_sdk-1.1.7/rp_python_sdk/endpoints/util/version_info.py +0 -2
  14. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/__init__.py +0 -0
  15. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/fapi.py +0 -0
  16. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/mapping.py +0 -0
  17. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/system_info.py +0 -0
  18. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/user_agent.py +0 -0
  19. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/filters/participant_filters.py +0 -0
  20. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/model.py +0 -0
  21. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
  22. {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.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.1.7
3
+ Version: 1.2.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
@@ -203,7 +203,12 @@ client = RelyingPartyClientSdk(
203
203
  client_id='https://rp.directory.sandbox.connectid.com.au/openid_relying_party/280518db-9807-4824-b080-324d94b45f6a',
204
204
  signing_kid='1X8udt28NY8NiSS8OJFvabv63K5igyFx5pM5ajKlMx8',
205
205
  custom_config=CustomConfig(
206
- enable_auto_compliance_verification=False
206
+ enable_auto_compliance_verification=False,
207
+ # HTTP cache configuration (optional)
208
+ cache_enabled=True, # Enable/disable caching (default: True)
209
+ cache_ttl_minutes=10, # Cache TTL in minutes (default: 10)
210
+ cache_max_entries=100, # Max cached entries (default: 100)
211
+ cache_max_element_size_bytes=5242880 # Max size per entry in bytes (default: 5MB)
207
212
  )
208
213
  ),
209
214
  )
@@ -211,6 +216,19 @@ client = RelyingPartyClientSdk(
211
216
  participants = client.get_participants()
212
217
  ```
213
218
 
219
+ ### HTTP Response Caching
220
+
221
+ The SDK includes an in-memory LRU cache that automatically caches HTTP responses for stable endpoints (participants registry,
222
+ OIDC discovery documents, and JWKS endpoints) to improve performance and reduce network calls. The cache is enabled by default
223
+ with sensible defaults, but can be customized or disabled via the `CustomConfig` options shown above.
224
+
225
+ Key cache features:
226
+ - **Thread-safe**: Safe for concurrent use across multiple threads
227
+ - **LRU eviction**: Automatically evicts least recently used entries when the cache is full
228
+ - **TTL expiration**: Entries expire after the configured TTL (default: 10 minutes)
229
+ - **Size limits**: Prevents caching of responses that exceed the configured size limit
230
+ - **Transparent**: Caching is automatic and transparent to SDK users
231
+
214
232
  The response will contain List of Organisations and their Authorisation Server, with an object structure similar to below.
215
233
 
216
234
  They key fields of interest are:
@@ -433,6 +451,17 @@ poetry publish
433
451
 
434
452
  ## Release Notes
435
453
 
454
+ ### 1.2.0 (Feb 11, 2026)
455
+ - **New Feature: HTTP Response Cache** - Added in-memory LRU cache for HTTP responses to improve SDK performance
456
+ - Caches participants registry, OIDC discovery documents, and JWKS endpoints
457
+ - Configurable TTL (default: 10 minutes), max entries (default: 100), and size limits (default: 5MB per entry)
458
+ - Thread-safe with automatic LRU eviction
459
+ - Cache can be disabled or customized via `CustomConfig`:
460
+ - `cache_enabled` (bool, default: `True`)
461
+ - `cache_ttl_minutes` (int, default: `10`)
462
+ - `cache_max_entries` (int, default: `100`)
463
+ - `cache_max_element_size_bytes` (int, default: `5242880`)
464
+
436
465
  ### 1.1.7 (Jan 9, 2026)
437
466
  - Security fix: Updated urllib3 to 2.6.3 to address high severity vulnerability (CVE-2026-21441)
438
467
 
@@ -182,7 +182,12 @@ client = RelyingPartyClientSdk(
182
182
  client_id='https://rp.directory.sandbox.connectid.com.au/openid_relying_party/280518db-9807-4824-b080-324d94b45f6a',
183
183
  signing_kid='1X8udt28NY8NiSS8OJFvabv63K5igyFx5pM5ajKlMx8',
184
184
  custom_config=CustomConfig(
185
- enable_auto_compliance_verification=False
185
+ enable_auto_compliance_verification=False,
186
+ # HTTP cache configuration (optional)
187
+ cache_enabled=True, # Enable/disable caching (default: True)
188
+ cache_ttl_minutes=10, # Cache TTL in minutes (default: 10)
189
+ cache_max_entries=100, # Max cached entries (default: 100)
190
+ cache_max_element_size_bytes=5242880 # Max size per entry in bytes (default: 5MB)
186
191
  )
187
192
  ),
188
193
  )
@@ -190,6 +195,19 @@ client = RelyingPartyClientSdk(
190
195
  participants = client.get_participants()
191
196
  ```
192
197
 
198
+ ### HTTP Response Caching
199
+
200
+ The SDK includes an in-memory LRU cache that automatically caches HTTP responses for stable endpoints (participants registry,
201
+ OIDC discovery documents, and JWKS endpoints) to improve performance and reduce network calls. The cache is enabled by default
202
+ with sensible defaults, but can be customized or disabled via the `CustomConfig` options shown above.
203
+
204
+ Key cache features:
205
+ - **Thread-safe**: Safe for concurrent use across multiple threads
206
+ - **LRU eviction**: Automatically evicts least recently used entries when the cache is full
207
+ - **TTL expiration**: Entries expire after the configured TTL (default: 10 minutes)
208
+ - **Size limits**: Prevents caching of responses that exceed the configured size limit
209
+ - **Transparent**: Caching is automatic and transparent to SDK users
210
+
193
211
  The response will contain List of Organisations and their Authorisation Server, with an object structure similar to below.
194
212
 
195
213
  They key fields of interest are:
@@ -412,6 +430,17 @@ poetry publish
412
430
 
413
431
  ## Release Notes
414
432
 
433
+ ### 1.2.0 (Feb 11, 2026)
434
+ - **New Feature: HTTP Response Cache** - Added in-memory LRU cache for HTTP responses to improve SDK performance
435
+ - Caches participants registry, OIDC discovery documents, and JWKS endpoints
436
+ - Configurable TTL (default: 10 minutes), max entries (default: 100), and size limits (default: 5MB per entry)
437
+ - Thread-safe with automatic LRU eviction
438
+ - Cache can be disabled or customized via `CustomConfig`:
439
+ - `cache_enabled` (bool, default: `True`)
440
+ - `cache_ttl_minutes` (int, default: `10`)
441
+ - `cache_max_entries` (int, default: `100`)
442
+ - `cache_max_element_size_bytes` (int, default: `5242880`)
443
+
415
444
  ### 1.1.7 (Jan 9, 2026)
416
445
  - Security fix: Updated urllib3 to 2.6.3 to address high severity vulnerability (CVE-2026-21441)
417
446
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rp_python_sdk"
3
- version = "1.1.7"
3
+ version = "1.2.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"
@@ -21,7 +21,7 @@ httpx = "^0.28.1"
21
21
  toml = "^0.10.2"
22
22
 
23
23
  [tool.poetry.group.dev.dependencies]
24
- black = "^25.0.0"
24
+ black = "^26.0.0"
25
25
  pre-commit = "^4.5.1"
26
26
  mypy = "^1.19.1"
27
27
  requests-mock = "^1.12.1"
File without changes
@@ -0,0 +1,117 @@
1
+ from collections import OrderedDict
2
+ from dataclasses import dataclass
3
+ from datetime import datetime, timedelta, timezone
4
+ from threading import Lock
5
+ from typing import Optional
6
+ from rp_python_sdk.sdk_config import CustomConfig
7
+
8
+ from rp_python_sdk.setup_logger import logger
9
+
10
+ @dataclass
11
+ class CacheEntry:
12
+ content: str
13
+ fetched_at: datetime
14
+ size_bytes: int
15
+
16
+
17
+ class HttpCache:
18
+ def __init__(self, config: CustomConfig) -> None:
19
+ self._enabled = config.cache_enabled
20
+ self._ttl_minutes = config.cache_ttl_minutes
21
+ self._max_entries = config.cache_max_entries
22
+ self._max_element_size_bytes = config.cache_max_element_size_bytes
23
+
24
+ self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
25
+ self._lock = Lock()
26
+
27
+ logger.debug(
28
+ f"HTTP cache initialized: enabled={self._enabled}, ttlMinutes={self._ttl_minutes}, "
29
+ f"maxEntries={self._max_entries}, maxElementSize={self._max_element_size_bytes}"
30
+ )
31
+
32
+ def get(self, url: str) -> Optional[str]:
33
+ if not self._enabled:
34
+ return None
35
+
36
+ with self._lock:
37
+ if url not in self._cache:
38
+ return None
39
+
40
+ entry = self._cache[url]
41
+
42
+ if self._is_stale_entry(entry):
43
+ logger.debug(f"Cache entry stale for URL: {url}")
44
+ return None
45
+
46
+ self._cache.move_to_end(url)
47
+ logger.debug(f"Cache hit for URL: {url}")
48
+ return entry.content
49
+
50
+ def put(self, url: str, content: str) -> None:
51
+ if not self._enabled:
52
+ return
53
+
54
+ size_bytes = len(content.encode('utf-8'))
55
+
56
+ if size_bytes > self._max_element_size_bytes:
57
+ logger.warning(
58
+ f"Response too large to cache: {size_bytes} bytes (max: {self._max_element_size_bytes}), URL: {url}"
59
+ )
60
+ return
61
+
62
+ with self._lock:
63
+ entry = CacheEntry(
64
+ content=content,
65
+ fetched_at=datetime.now(timezone.utc),
66
+ size_bytes=size_bytes
67
+ )
68
+
69
+ self._cache[url] = entry
70
+ self._cache.move_to_end(url)
71
+
72
+ logger.debug(f"Cached response for URL: {url}, size: {size_bytes} bytes")
73
+
74
+ if len(self._cache) > self._max_entries:
75
+ evicted_url, _ = self._cache.popitem(last=False)
76
+ logger.debug(f"Evicted cache entry for URL: {evicted_url}")
77
+
78
+ def is_stale(self, url: str) -> bool:
79
+ if not self._enabled:
80
+ return False
81
+
82
+ with self._lock:
83
+ if url not in self._cache:
84
+ return False
85
+
86
+ entry = self._cache[url]
87
+ return self._is_stale_entry(entry)
88
+
89
+ def evict(self, url: str) -> None:
90
+ if not self._enabled:
91
+ return
92
+
93
+ with self._lock:
94
+ if url in self._cache:
95
+ del self._cache[url]
96
+ logger.debug(f"Evicted cache entry for URL: {url}")
97
+
98
+ def clear(self) -> None:
99
+ if not self._enabled:
100
+ return
101
+
102
+ with self._lock:
103
+ self._cache.clear()
104
+ logger.debug("Cache cleared")
105
+
106
+ def size(self) -> int:
107
+ with self._lock:
108
+ return len(self._cache)
109
+
110
+ def is_enabled(self) -> bool:
111
+ return self._enabled
112
+
113
+ def _is_stale_entry(self, entry: CacheEntry) -> bool:
114
+ expiry_time: datetime = entry.fetched_at + timedelta(minutes=self._ttl_minutes)
115
+ current_time: datetime = datetime.now(timezone.utc)
116
+ is_stale: bool = current_time >= expiry_time
117
+ return is_stale
@@ -14,10 +14,11 @@ from rp_python_sdk.model import Participant
14
14
  from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
15
15
  from rp_python_sdk.sdk_config import SdkConfig
16
16
  from rp_python_sdk.setup_logger import logger
17
+ from rp_python_sdk.cache.http_cache import HttpCache
17
18
 
18
19
 
19
- def get_participants(config: SdkConfig) -> list[Participant]:
20
- participants = _retrieve_full_participants_list(config)
20
+ def get_participants(config: SdkConfig, cache: HttpCache) -> list[Participant]:
21
+ participants = _retrieve_full_participants_list(config, cache)
21
22
 
22
23
  filtered_participants = remove_fallback_identity_service_provider(participants)
23
24
 
@@ -47,8 +48,8 @@ def get_participants(config: SdkConfig) -> list[Participant]:
47
48
  return filtered_participants
48
49
 
49
50
 
50
- def retrieve_fallback_provider_participants(config: SdkConfig) -> list[Participant]:
51
- participants = _retrieve_full_participants_list(config)
51
+ def retrieve_fallback_provider_participants(config: SdkConfig, cache: HttpCache) -> list[Participant]:
52
+ participants = _retrieve_full_participants_list(config, cache)
52
53
 
53
54
  filtered_participants = remove_out_of_date_certifications(participants, date.today())
54
55
  filtered_participants = remove_unofficial_certifications(filtered_participants)
@@ -59,10 +60,21 @@ def retrieve_fallback_provider_participants(config: SdkConfig) -> list[Participa
59
60
  return filtered_participants
60
61
 
61
62
 
62
- def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
63
- logger.info(f"Retrieving all identity providers from {config.registry_participants_uri}")
63
+ def _retrieve_full_participants_list(config: SdkConfig, cache: HttpCache) -> list[Participant]:
64
+ url = config.registry_participants_uri
64
65
 
65
- response = requests.get(config.registry_participants_uri,
66
+ # Try cache first
67
+ cached = cache.get(url)
68
+ if cached:
69
+ logger.debug(f"Cache hit for URL: {url}")
70
+ participant_list: list[Participant] = from_dict(Participant, json.loads(cached), pascal_case=True)
71
+ logger.info(f"Retrieved identity providers from cache, num orgs found: {len(participant_list)}")
72
+ return participant_list
73
+
74
+ logger.debug(f"Cache miss for URL: {url}")
75
+ logger.info(f"Retrieving all identity providers from {url}")
76
+
77
+ response = requests.get(url,
66
78
  timeout=config.custom_config.timeout_in_seconds,
67
79
  headers={
68
80
  'User-Agent': build_user_agent(config.client_id)
@@ -71,6 +83,10 @@ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
71
83
  raise RelyingPartyClientSdkException(
72
84
  f"Call to identity provider endpoint failed with code: {response.status_code}")
73
85
 
86
+ # Cache the successful response
87
+ cache.put(url, response.text)
88
+ logger.debug(f"Cached response for URL: {url}, size: {len(response.text)} bytes")
89
+
74
90
  # participant_list: list[Participant] = Participant.from_json(response.text)
75
91
  participant_list: list[Participant] = from_dict(Participant, json.loads(response.text), pascal_case=True)
76
92
 
@@ -78,7 +94,7 @@ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
78
94
  return participant_list
79
95
 
80
96
 
81
- def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) -> AuthorisationServer:
97
+ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str, cache: HttpCache) -> AuthorisationServer:
82
98
  def find_auth_server_by_id(participants: list[Participant], target_id: str) -> AuthorisationServer | None:
83
99
  for participant in participants:
84
100
  for server in participant.authorisation_servers:
@@ -87,11 +103,11 @@ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) ->
87
103
  return None
88
104
 
89
105
  # Search in main participants
90
- result = find_auth_server_by_id(get_participants(config), authorisation_server_id)
106
+ result = find_auth_server_by_id(get_participants(config, cache), authorisation_server_id)
91
107
 
92
108
  # If not found, check fallback provider participants
93
109
  if result is None:
94
- fallback_participants = retrieve_fallback_provider_participants(config)
110
+ fallback_participants = retrieve_fallback_provider_participants(config, cache)
95
111
  result = find_auth_server_by_id(fallback_participants, authorisation_server_id)
96
112
 
97
113
  if result is None:
@@ -101,14 +117,26 @@ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) ->
101
117
  return result
102
118
 
103
119
 
104
- def get_issuer_metadata(authorisation_server: AuthorisationServer) -> IssuerMetadata:
105
- auth_server_issuer = authorisation_server.open_i_d_discovery_document
120
+ def get_issuer_metadata(authorisation_server: AuthorisationServer, cache: HttpCache) -> IssuerMetadata:
121
+ url = authorisation_server.open_i_d_discovery_document
122
+
123
+ # Try cache first
124
+ cached = cache.get(url)
125
+ if cached:
126
+ logger.debug(f"Cache hit for URL: {url}")
127
+ return from_dict(IssuerMetadata, json.loads(cached))
128
+
129
+ logger.debug(f"Cache miss for URL: {url}")
106
130
 
107
131
  try:
108
- logger.info(f"Getting discovery document from {auth_server_issuer}")
109
- response = requests.get(auth_server_issuer)
132
+ logger.info(f"Getting discovery document from {url}")
133
+ response = requests.get(url)
110
134
  response.raise_for_status() # Raises an exception for 4XX or 5XX errors
111
135
 
136
+ # Cache the successful response
137
+ cache.put(url, response.text)
138
+ logger.debug(f"Cached response for URL: {url}, size: {len(response.text)} bytes")
139
+
112
140
  return from_dict(IssuerMetadata, json.loads(response.text))
113
141
 
114
142
  except requests.exceptions.HTTPError as http_err:
@@ -116,4 +144,4 @@ def get_issuer_metadata(authorisation_server: AuthorisationServer) -> IssuerMeta
116
144
  f"Call to identity provider endpoint failed with code: {response.status_code}") from http_err
117
145
  except requests.exceptions.RequestException as req_err:
118
146
  raise RelyingPartyClientSdkException(
119
- f"Failed to connect to authorisation service {auth_server_issuer}") from req_err
147
+ f"Failed to connect to authorisation service {url}") from req_err
@@ -16,6 +16,7 @@ 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
+ from rp_python_sdk.cache.http_cache import HttpCache
19
20
 
20
21
  extended_claims = {"over16", "over18", "over21", "over25", "over65", "beneficiary_account_au",
21
22
  "beneficiary_account_au_payid", "beneficiary_account_international"}
@@ -25,14 +26,15 @@ def send_pushed_authorisation_request(config: SdkConfig,
25
26
  authorisation_server_id: str,
26
27
  essential_claims: set[str],
27
28
  voluntary_claims: set[str],
28
- purpose: str) -> PARResponse:
29
+ purpose: str,
30
+ cache: HttpCache) -> PARResponse:
29
31
  _check_valid_purpose(purpose)
30
32
 
31
33
  checked_essential_claims = _ensure_mandatory_claims_present(essential_claims, voluntary_claims)
32
34
  claim_request = _generate_claims_request(checked_essential_claims, voluntary_claims)
33
35
 
34
- authorisation_server = get_auth_server_details(config, authorisation_server_id)
35
- issuer_metadata = get_issuer_metadata(authorisation_server)
36
+ authorisation_server = get_auth_server_details(config, authorisation_server_id, cache)
37
+ issuer_metadata = get_issuer_metadata(authorisation_server, cache)
36
38
 
37
39
  auth_server_url = issuer_metadata.issuer
38
40
  x_fapi_interaction_id = create_x_fapi_interaction_id()
@@ -22,12 +22,13 @@ from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientS
22
22
  from rp_python_sdk.sdk_config import SdkConfig
23
23
  from rp_python_sdk.setup_logger import logger
24
24
 
25
+ from rp_python_sdk.cache.http_cache import HttpCache
25
26
 
26
27
  def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_body: CallbackBody,
27
28
  original_code_verifier: str,
28
- original_state: str, nonce: str) -> TokenSet:
29
- authorisation_server = get_auth_server_details(config, authorisation_server_id)
30
- issuer_metadata = get_issuer_metadata(authorisation_server)
29
+ original_state: str, nonce: str, cache: HttpCache) -> TokenSet:
30
+ authorisation_server = get_auth_server_details(config, authorisation_server_id, cache)
31
+ issuer_metadata = get_issuer_metadata(authorisation_server, cache)
31
32
 
32
33
  if issuer_metadata.issuer != callback_body.iss:
33
34
  raise RelyingPartyClientSdkException(
@@ -104,7 +105,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
104
105
 
105
106
  token_input = from_dict(TokenInput, json.loads(response.text))
106
107
 
107
- jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id)
108
+ jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id, cache)
108
109
  allowed_algs = issuer_metadata.id_token_signing_alg_values_supported
109
110
 
110
111
  header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body,
@@ -113,7 +114,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
113
114
  token_set = TokenSet(token_input, payload, x_fapi_interaction_id)
114
115
 
115
116
  if config.custom_config.enable_auto_compliance_verification:
116
- call_user_info(config, authorisation_server_id, token_set.token_input.access_token)
117
+ call_user_info(config, authorisation_server_id, token_set.token_input.access_token, cache)
117
118
 
118
119
  logger.info(
119
120
  f"Token response successfully retrieved from authorisation server {authorisation_server_id}, "
@@ -275,17 +276,31 @@ def _matching_alg(token_alg: str, key_element: dict) -> bool:
275
276
  return key_element.get("alg") is None or key_element.get("alg") == token_alg
276
277
 
277
278
 
278
- def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str) -> Any:
279
+ def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str, cache: HttpCache) -> Any:
280
+ url = issuer_metadata.jwks_uri
281
+
282
+ # Try cache first
283
+ cached = cache.get(url)
284
+ if cached:
285
+ logger.debug(f"Cache hit for URL: {url}")
286
+ return json.loads(cached)
287
+
288
+ logger.debug(f"Cache miss for URL: {url}")
289
+
279
290
  headers = {
280
291
  "Accept": "application/json",
281
292
  "x-fapi-interaction-id": x_fapi_interaction_id
282
293
  }
283
- response = requests.get(issuer_metadata.jwks_uri, headers=headers)
294
+ response = requests.get(url, headers=headers)
284
295
  response.raise_for_status()
285
296
 
286
297
  if not response.ok:
287
298
  raise RelyingPartyClientSdkException(
288
- f"Response code for JWKS to {issuer_metadata.jwks_uri} not successful, was: {response.status_code}, "
299
+ f"Response code for JWKS to {url} not successful, was: {response.status_code}, "
289
300
  f"x-fapi-interaction-id: {x_fapi_interaction_id}, response body: {response.text}")
290
301
 
302
+ # Cache the successful response
303
+ cache.put(url, response.text)
304
+ logger.debug(f"Cached response for URL: {url}, size: {len(response.text)} bytes")
305
+
291
306
  return response.json()
@@ -2,6 +2,7 @@ from typing import Any
2
2
 
3
3
  import requests
4
4
 
5
+ from rp_python_sdk.cache.http_cache import HttpCache
5
6
  from rp_python_sdk.endpoints.get_participants import get_issuer_metadata, \
6
7
  get_auth_server_details
7
8
  from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id
@@ -10,12 +11,12 @@ from rp_python_sdk.sdk_config import SdkConfig
10
11
  from rp_python_sdk.setup_logger import logger
11
12
 
12
13
 
13
- def call_user_info(config: SdkConfig, authorisation_server_id: str, access_token: str) -> dict[str, Any]:
14
+ def call_user_info(config: SdkConfig, authorisation_server_id: str, access_token: str, cache: HttpCache) -> dict[str, Any]:
14
15
  x_fapi_interaction_id = create_x_fapi_interaction_id()
15
16
  logger.info(f"Making call to user info endpoint, x-fapi-interaction-id: {x_fapi_interaction_id}")
16
17
 
17
- authorisation_server = get_auth_server_details(config, authorisation_server_id)
18
- issuer_metadata = get_issuer_metadata(authorisation_server)
18
+ authorisation_server = get_auth_server_details(config, authorisation_server_id, cache)
19
+ issuer_metadata = get_issuer_metadata(authorisation_server, cache)
19
20
 
20
21
  user_info_endpoint = issuer_metadata.get_preferred_userinfo_endpoint()
21
22
 
@@ -0,0 +1,2 @@
1
+ def get_version_info() -> str:
2
+ return "1.2.0" # Version number
@@ -1,3 +1,4 @@
1
+ from rp_python_sdk.cache.http_cache import HttpCache
1
2
  from rp_python_sdk.endpoints.get_participants import get_participants, \
2
3
  retrieve_fallback_provider_participants
3
4
  from rp_python_sdk.endpoints.pushed_authorisation_request import send_pushed_authorisation_request
@@ -12,6 +13,7 @@ class RelyingPartyClientSdk:
12
13
 
13
14
  def __init__(self, config: SdkConfig):
14
15
  self.config = config
16
+ self.cache = HttpCache(config.custom_config)
15
17
 
16
18
  if config.custom_config.enable_auto_compliance_verification:
17
19
  logger.info(f"Auto Compliance Verification mode enabled, printing config information: "
@@ -21,10 +23,10 @@ class RelyingPartyClientSdk:
21
23
  f"registry_participants_uri={self.config.registry_participants_uri}")
22
24
 
23
25
  def get_participants(self) -> list[Participant]:
24
- return get_participants(self.config)
26
+ return get_participants(self.config, self.cache)
25
27
 
26
28
  def get_fallback_provider_participants(self) -> list[Participant]:
27
- return retrieve_fallback_provider_participants(self.config)
29
+ return retrieve_fallback_provider_participants(self.config, self.cache)
28
30
 
29
31
  def send_pushed_authorisation_request(self, authorisation_server_id: str,
30
32
  essential_claims: set[str] | None = None,
@@ -36,9 +38,9 @@ class RelyingPartyClientSdk:
36
38
  checked_purpose = purpose if purpose else self.config.custom_config.purpose
37
39
 
38
40
  return send_pushed_authorisation_request(self.config, authorisation_server_id, checked_essential_claims,
39
- checked_voluntary_claims, checked_purpose)
41
+ checked_voluntary_claims, checked_purpose, self.cache)
40
42
 
41
43
  def retrieve_tokens(self, authorisation_server_id: str, callback_body: CallbackBody, original_code_verifier: str,
42
44
  original_state: str, nonce: str) -> TokenSet:
43
45
  return retrieve_tokens(self.config, authorisation_server_id, callback_body, original_code_verifier,
44
- original_state, nonce)
46
+ original_state, nonce, self.cache)
@@ -23,6 +23,10 @@ class CustomConfig:
23
23
  required_claims: List[str] = field(default_factory=list)
24
24
  required_participant_certifications: List['CertificationFilter'] = field(default_factory=list)
25
25
  timeout_in_seconds: int = 10
26
+ cache_enabled: bool = True
27
+ cache_ttl_minutes: int = 10
28
+ cache_max_entries: int = 100
29
+ cache_max_element_size_bytes: int = 5242880
26
30
 
27
31
  def __post_init__(self) -> None:
28
32
  if self.include_uncertified_participants:
@@ -1,2 +0,0 @@
1
- def get_version_info() -> str:
2
- return "1.1.7" # Version number