rp_python_sdk 1.1.0__tar.gz → 1.1.1__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 (20) hide show
  1. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/PKG-INFO +13 -4
  2. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/README.md +9 -0
  3. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/pyproject.toml +10 -10
  4. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/get_participants.py +21 -9
  5. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/pushed_authorisation_request.py +5 -4
  6. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/retrieve_tokens.py +17 -8
  7. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/user_info.py +1 -1
  8. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/util/fapi.py +7 -8
  9. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/util/mapping.py +1 -1
  10. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/util/system_info.py +5 -4
  11. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/endpoints/util/user_agent.py +2 -3
  12. rp_python_sdk-1.1.1/rp_python_sdk/endpoints/util/version_info.py +2 -0
  13. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/filters/participant_filters.py +4 -3
  14. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/model.py +4 -0
  15. rp_python_sdk-1.1.0/rp_python_sdk/endpoints/util/version_info.py +0 -2
  16. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/__init__.py +0 -0
  17. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/relying_party_client_sdk.py +0 -0
  18. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
  19. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/sdk_config.py +0 -0
  20. {rp_python_sdk-1.1.0 → rp_python_sdk-1.1.1}/rp_python_sdk/setup_logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: rp_python_sdk
3
- Version: 1.1.0
3
+ Version: 1.1.1
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
@@ -10,10 +10,10 @@ Classifier: License :: Other/Proprietary License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
- Requires-Dist: authlib (>=1.3.2,<2.0.0)
13
+ Requires-Dist: authlib (>=1.6.0,<2.0.0)
14
14
  Requires-Dist: cffi (>=1.17.1,<2.0.0)
15
- Requires-Dist: cryptography (>=44.0.0,<45.0.0)
16
- Requires-Dist: joserfc (>=1.0.0,<2.0.0)
15
+ Requires-Dist: cryptography (>=45.0.0,<46.0.0)
16
+ Requires-Dist: joserfc (>=1.1.0,<2.0.0)
17
17
  Requires-Dist: requests (>=2.32.3,<3.0.0)
18
18
  Description-Content-Type: text/markdown
19
19
 
@@ -85,6 +85,11 @@ eval "$(pyenv virtualenv-init -)"
85
85
 
86
86
  ## running tests
87
87
 
88
+ Set RP_SAMPLE_APP_TOKEN environment variable. The value can be obtained from the [ConnectID Docs Portal](https://docs.connectid.com.au/docs/relying-parties/getting-started/).
89
+ ```shell
90
+ export RP_SAMPLE_APP_TOKEN=<your token>
91
+ ```
92
+
88
93
  To run all the test, you can execute the following:
89
94
 
90
95
  ```bash
@@ -426,6 +431,10 @@ poetry publish
426
431
 
427
432
  ## Release Notes
428
433
 
434
+ ### 1.1.1 (Jul 7, 2025)
435
+ - Bugfix release to support manual providers
436
+ - Updated dependencies
437
+
429
438
  ### 1.1.0 (Mar 14, 2025)
430
439
  - Implement user agent logging.
431
440
 
@@ -66,6 +66,11 @@ eval "$(pyenv virtualenv-init -)"
66
66
 
67
67
  ## running tests
68
68
 
69
+ Set RP_SAMPLE_APP_TOKEN environment variable. The value can be obtained from the [ConnectID Docs Portal](https://docs.connectid.com.au/docs/relying-parties/getting-started/).
70
+ ```shell
71
+ export RP_SAMPLE_APP_TOKEN=<your token>
72
+ ```
73
+
69
74
  To run all the test, you can execute the following:
70
75
 
71
76
  ```bash
@@ -407,6 +412,10 @@ poetry publish
407
412
 
408
413
  ## Release Notes
409
414
 
415
+ ### 1.1.1 (Jul 7, 2025)
416
+ - Bugfix release to support manual providers
417
+ - Updated dependencies
418
+
410
419
  ### 1.1.0 (Mar 14, 2025)
411
420
  - Implement user agent logging.
412
421
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rp_python_sdk"
3
- version = "1.1.0"
3
+ version = "1.1.1"
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"
@@ -9,23 +9,23 @@ readme = "README.md"
9
9
  [tool.poetry.dependencies]
10
10
  python = "^3.12"
11
11
  requests = "^2.32.3"
12
- authlib = "^1.3.2"
13
- cryptography = "^44.0.0"
14
- joserfc = "^1.0.0"
12
+ authlib = "^1.6.0"
13
+ cryptography = "^45.0.0"
14
+ joserfc = "^1.1.0"
15
15
  cffi = "^1.17.1"
16
16
 
17
17
  [tool.poetry.group.test.dependencies]
18
- pytest = "^8.3.3"
19
- httpx = "^0.28.0"
18
+ pytest = "^8.4.1"
19
+ httpx = "^0.28.1"
20
20
  toml = "^0.10.2"
21
21
 
22
22
  [tool.poetry.group.dev.dependencies]
23
- black = "^24.10.0"
24
- pre-commit = "^4.0.0"
25
- mypy = "^1.13.0"
23
+ black = "^25.0.0"
24
+ pre-commit = "^4.2.0"
25
+ mypy = "^1.16.1"
26
26
  requests-mock = "^1.12.1"
27
27
  freezegun = "^1.4.0"
28
- flake8 = "^7.1.1"
28
+ flake8 = "^7.3.0"
29
29
 
30
30
  [build-system]
31
31
  requires = ["poetry-core"]
@@ -1,7 +1,7 @@
1
+ import json
1
2
  from datetime import date
2
3
 
3
4
  import requests
4
- import json
5
5
 
6
6
  from rp_python_sdk.endpoints.util.mapping import from_dict
7
7
  from rp_python_sdk.endpoints.util.user_agent import build_user_agent
@@ -65,7 +65,7 @@ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
65
65
  response = requests.get(config.registry_participants_uri,
66
66
  timeout=config.custom_config.timeout_in_seconds,
67
67
  headers={
68
- 'User-Agent': build_user_agent(config)
68
+ 'User-Agent': build_user_agent(config.client_id)
69
69
  })
70
70
  if response.status_code != 200:
71
71
  raise RelyingPartyClientSdkException(
@@ -79,17 +79,29 @@ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
79
79
 
80
80
 
81
81
  def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) -> AuthorisationServer:
82
- result = None
83
- for participant in get_participants(config):
84
- for server in participant.authorisation_servers:
85
- if str(server.authorisation_server_id) == authorisation_server_id:
86
- result = server
87
- break
82
+ def find_auth_server_by_id(participants: list[Participant], target_id: str) -> AuthorisationServer | None:
83
+ for participant in participants:
84
+ for server in participant.authorisation_servers:
85
+ if str(server.authorisation_server_id) == target_id:
86
+ return server
87
+ return None
88
+
89
+ # Search in main participants
90
+ result = find_auth_server_by_id(get_participants(config), authorisation_server_id)
91
+
92
+ # If not found, check fallback provider participants
93
+ if result is None:
94
+ fallback_participants = retrieve_fallback_provider_participants(config)
95
+ result = find_auth_server_by_id(fallback_participants, authorisation_server_id)
96
+
97
+ if result is None:
98
+ raise RelyingPartyClientSdkException(
99
+ f"No authorisation server with id: {authorisation_server_id} was found")
88
100
 
89
101
  return result
90
102
 
91
103
 
92
- def get_issuer_metadata(authorisation_server: AuthorisationServer):
104
+ def get_issuer_metadata(authorisation_server: AuthorisationServer) -> IssuerMetadata:
93
105
  auth_server_issuer = authorisation_server.open_i_d_discovery_document
94
106
 
95
107
  try:
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import time
3
3
  import uuid
4
+ from typing import Any
4
5
  from urllib.parse import quote
5
6
 
6
7
  import requests
@@ -125,7 +126,7 @@ def send_pushed_authorisation_request(config: SdkConfig,
125
126
  code_verifier, state, nonce, x_fapi_interaction_id)
126
127
 
127
128
 
128
- def _check_valid_purpose(purpose) -> None:
129
+ def _check_valid_purpose(purpose: str) -> None:
129
130
  if len(purpose) < 3 or len(purpose) > 300:
130
131
  raise RelyingPartyClientSdkException("Purpose must be between 3 and 300 characters")
131
132
 
@@ -149,7 +150,7 @@ def _ensure_mandatory_claims_present(essential_claims: set[str], voluntary_claim
149
150
  return result_claims
150
151
 
151
152
 
152
- def _generate_claims_request(essential_claims: set[str], voluntary_claims: set[str]):
153
+ def _generate_claims_request(essential_claims: set[str], voluntary_claims: set[str]) -> dict[str, Any]:
153
154
  # Filter out any claims that are in both essential and voluntary sets so essential takes precedence
154
155
  deduplicated_voluntary_claims = voluntary_claims - essential_claims
155
156
 
@@ -179,7 +180,7 @@ def _generate_claims_request(essential_claims: set[str], voluntary_claims: set[s
179
180
  return {"id_token": id_token_claims}
180
181
 
181
182
 
182
- def _partition_claims(claims: set[str]):
183
+ def _partition_claims(claims: set[str]) -> dict[bool, set[str]]:
183
184
  """
184
185
  Partition claims into basic and extended types based on a criteria (here, whether they are in extended_claims).
185
186
  """
@@ -189,7 +190,7 @@ def _partition_claims(claims: set[str]):
189
190
  return partitioned
190
191
 
191
192
 
192
- def _map_claims(claims, claim_map):
193
+ def _map_claims(claims: set[str], claim_map: dict[str, Any]) -> dict[str, Any]:
193
194
  """
194
195
  Map each claim to the provided claim_map.
195
196
  """
@@ -5,12 +5,12 @@ import uuid
5
5
  from typing import Tuple, Any
6
6
 
7
7
  import requests
8
- from joserfc import jws
9
- from joserfc.jwk import RSAKey
10
- from joserfc.jws import JWSRegistry
11
8
  from authlib.jose import JsonWebToken
12
9
  from authlib.jose.errors import BadSignatureError, DecodeError
13
10
  from authlib.jose.util import extract_header
11
+ from joserfc import jws
12
+ from joserfc.jwk import RSAKey
13
+ from joserfc.jws import JWSRegistry
14
14
 
15
15
  from rp_python_sdk.endpoints.get_participants import get_issuer_metadata, \
16
16
  get_auth_server_details
@@ -72,7 +72,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
72
72
 
73
73
  key = RSAKey.import_key(config.signing_key)
74
74
  header = {'alg': 'PS256', 'kid': config.signing_kid}
75
- client_assertion_jwt = jws.serialize_compact(header, json.dumps(client_assertion_payload), key, registry = registry)
75
+ client_assertion_jwt = jws.serialize_compact(header, json.dumps(client_assertion_payload), key, registry=registry)
76
76
 
77
77
  # Data payload
78
78
  data = {
@@ -107,7 +107,8 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
107
107
  jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id)
108
108
  allowed_algs = issuer_metadata.id_token_signing_alg_values_supported
109
109
 
110
- header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body, issuer_metadata.issuer)
110
+ header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body,
111
+ issuer_metadata.issuer)
111
112
 
112
113
  token_set = TokenSet(token_input, payload, x_fapi_interaction_id)
113
114
 
@@ -197,9 +198,11 @@ def _validate_payload(json_payload: dict[str, Any], nonce: str, callback_body: C
197
198
  if json_payload['aud'] != client_id:
198
199
  raise RelyingPartyClientSdkException('aud claim in token response does not match client ID')
199
200
 
200
- def _get_unverified_header(id_token:str) -> Any:
201
+
202
+ def _get_unverified_header(id_token: str) -> Any:
201
203
  return extract_header(id_token.split('.')[0].encode(), DecodeError)
202
204
 
205
+
203
206
  def _decode_base64(base64_str: str) -> str:
204
207
  """
205
208
  Decode a Base64 encoded string.
@@ -223,12 +226,14 @@ def _validate_header(json_header: dict, allowed_algs: list) -> None:
223
226
  f"alg value in id token header is not one of the supported algorithms listed on the well-known. "
224
227
  f"Provided alg {json_header['alg']}")
225
228
 
229
+
226
230
  def _find_key_in_jwks(kid: str, token_alg: str, jwks: dict) -> Any:
227
231
  for key_element in jwks.get('keys', []):
228
232
  if _matching_kid(kid, key_element) and _matching_alg(token_alg, key_element) and _matching_use(key_element):
229
233
  return key_element
230
234
  return None
231
235
 
236
+
232
237
  def _validate_signature(issuer: str, audience: str, allowed_alg: list, id_token: str, jwks: dict) -> None:
233
238
  try:
234
239
  # Pub key extraction
@@ -257,14 +262,18 @@ def _validate_signature(issuer: str, audience: str, allowed_alg: list, id_token:
257
262
  logger.debug(f"Invalid JWT token: {id_token}")
258
263
  raise RelyingPartyClientSdkException(f"Invalid JWT signature: {e}")
259
264
 
265
+
260
266
  def _matching_use(key_element: dict) -> bool:
261
267
  return key_element.get('use') == "sig"
262
268
 
269
+
263
270
  def _matching_kid(kid: str, key_element: dict) -> bool:
264
271
  return key_element.get('kid') == kid
265
272
 
273
+
266
274
  def _matching_alg(token_alg: str, key_element: dict) -> bool:
267
- return key_element.get("alg") is None or key_element.get("alg") == token_alg
275
+ return key_element.get("alg") is None or key_element.get("alg") == token_alg
276
+
268
277
 
269
278
  def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str) -> Any:
270
279
  headers = {
@@ -279,4 +288,4 @@ def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str)
279
288
  f"Response code for JWKS to {issuer_metadata.jwks_uri} not successful, was: {response.status_code}, "
280
289
  f"x-fapi-interaction-id: {x_fapi_interaction_id}, response body: {response.text}")
281
290
 
282
- return response.json()
291
+ return response.json()
@@ -40,5 +40,5 @@ def call_user_info(config: SdkConfig, authorisation_server_id: str, access_token
40
40
  f"Failed to read body from response while connecting to {user_info_endpoint}") from e
41
41
 
42
42
 
43
- def _is_success(response: requests.Response):
43
+ def _is_success(response: requests.Response) -> bool:
44
44
  return 200 <= response.status_code < 300
@@ -6,25 +6,24 @@ from rp_python_sdk.model import AuthorisationServer
6
6
  from rp_python_sdk.setup_logger import logger
7
7
 
8
8
 
9
- def create_x_fapi_interaction_id():
9
+ def create_x_fapi_interaction_id() -> str:
10
10
  return str(uuid.uuid4())
11
11
 
12
12
 
13
13
  def log_x_fapi_interaction_id_matches(authorisation_server: AuthorisationServer, request_name: str,
14
- x_fapi_interaction_id: str, response: Response):
14
+ x_fapi_interaction_id: str, response: Response) -> None:
15
15
  fapi_header = response.headers.get('x-fapi-interaction-id')
16
16
  if not fapi_header:
17
17
  logger.warning(
18
18
  f"No x-fapi-interaction-id header returned from auth server {authorisation_server.authorisation_server_id} "
19
19
  f"on request {request_name}, expected {x_fapi_interaction_id}")
20
20
  return
21
-
22
- if x_fapi_interaction_id == fapi_header:
21
+ elif x_fapi_interaction_id == fapi_header:
23
22
  logger.debug(
24
23
  f"x-fapi-interaction-id correctly returned from auth server {authorisation_server.authorisation_server_id} "
25
24
  f"on {request_name} matched expected {x_fapi_interaction_id}")
26
25
  return
27
-
28
- logger.warning(
29
- f"x-fapi-interaction-id returned from auth server {authorisation_server.authorisation_server_id} "
30
- f"on {request_name} did not match. sent {x_fapi_interaction_id}, returned {fapi_header}")
26
+ else:
27
+ logger.warning(
28
+ f"x-fapi-interaction-id returned from auth server {authorisation_server.authorisation_server_id} "
29
+ f"on {request_name} did not match. sent {x_fapi_interaction_id}, returned {fapi_header}")
@@ -11,7 +11,7 @@ def snake_to_pascal(name: str) -> str:
11
11
  return ''.join(word.capitalize() for word in name.split('_'))
12
12
 
13
13
 
14
- def from_dict(data_class: Type[T], data: Union[dict, list], pascal_case=False) -> Union[T, List[T]]:
14
+ def from_dict(data_class: Type[T], data: Union[dict, list], pascal_case: bool = False) -> Union[T, List[T]]:
15
15
  """
16
16
  Recursively maps a dictionary or a list of dictionaries to a dataclass or a list of dataclasses.
17
17
  Handles key transformations from snake_case to PascalCase, and resolves forward references.
@@ -1,18 +1,19 @@
1
1
  import platform
2
2
 
3
3
 
4
- def get_system_info():
4
+ def get_system_info() -> str:
5
5
  os_name = platform.system() # e.g., "Darwin", "Windows", "Linux"
6
6
  os_version = platform.version() # e.g., "19.6.0", "10.0.19043", "5.15.0-84-generic"
7
7
  python_version = platform.python_version() # Python version
8
8
 
9
9
  return f"{detect_operating_system(os_name)}; {os_name} {os_version}; Python{python_version}"
10
10
 
11
- def detect_operating_system(input):
12
- if input is None:
11
+
12
+ def detect_operating_system(raw_input: str) -> str:
13
+ if raw_input is None:
13
14
  return "Unknown"
14
15
 
15
- os_name = input.lower()
16
+ os_name = raw_input.lower()
16
17
  if "darwin" in os_name:
17
18
  return "Macintosh"
18
19
  elif "win" in os_name:
@@ -1,9 +1,8 @@
1
1
  from rp_python_sdk.endpoints.util.system_info import get_system_info
2
2
  from rp_python_sdk.endpoints.util.version_info import get_version_info
3
- from rp_python_sdk.sdk_config import SdkConfig
4
3
 
5
4
 
6
- def build_user_agent(config: SdkConfig) -> str:
5
+ def build_user_agent(client_id: str) -> str:
7
6
  version = get_version_info()
8
7
  platform = get_system_info()
9
- return f"cid-rp-python-sdk/{version} ({platform}) +{config.client_id}"
8
+ return f"cid-rp-python-sdk/{version} ({platform}) +{client_id}"
@@ -0,0 +1,2 @@
1
+ def get_version_info() -> str:
2
+ return "1.1.1" # Version number
@@ -1,15 +1,16 @@
1
+ from datetime import datetime, date
1
2
  from typing import List
2
3
 
3
4
  from rp_python_sdk.model import Participant
4
5
  from rp_python_sdk.sdk_config import CertificationFilter
5
- from datetime import datetime, date
6
6
 
7
7
 
8
8
  def remove_out_of_date_certifications(participants: List[Participant], reference_date: date) -> List[Participant]:
9
9
  for participant in participants:
10
10
  for auth_server in participant.authorisation_servers:
11
11
  filtered = [certification for certification in auth_server.authorisation_server_certifications if
12
- to_date(certification.certification_start_date) < reference_date < to_date(certification.certification_expiration_date)]
12
+ to_date(certification.certification_start_date) < reference_date < to_date(
13
+ certification.certification_expiration_date)]
13
14
  auth_server.authorisation_server_certifications = filtered
14
15
 
15
16
  return participants
@@ -93,6 +94,6 @@ def filter_for_fallback_identity_service_providers(participants: List[Participan
93
94
  return participants
94
95
 
95
96
 
96
- def to_date(date_string: str):
97
+ def to_date(date_string: str) -> date:
97
98
  date_format = "%d/%m/%Y"
98
99
  return datetime.strptime(date_string, date_format).date()
@@ -2,6 +2,7 @@ from dataclasses import dataclass
2
2
  from typing import Any, Dict, Optional
3
3
  from typing import List
4
4
 
5
+
5
6
  @dataclass
6
7
  class Participant:
7
8
  organisation_id: str
@@ -45,11 +46,13 @@ class OrgDomainClaim:
45
46
  registration_id: str
46
47
  status: str
47
48
 
49
+
48
50
  @dataclass
49
51
  class ApiDiscoveryEndpoint:
50
52
  api_discovery_id: str
51
53
  api_endpoint: str
52
54
 
55
+
53
56
  @dataclass
54
57
  class ApiResource:
55
58
  api_resource_id: str
@@ -64,6 +67,7 @@ class ApiResource:
64
67
  certification_expiration_date: str
65
68
  api_family_type: Optional[str]
66
69
 
70
+
67
71
  @dataclass
68
72
  class AuthorisationServer:
69
73
  authorisation_server_id: str
@@ -1,2 +0,0 @@
1
- def get_version_info():
2
- return "1.1.0" # Version number