From e5a6caaf31c2f6711da2d3994cefb5435445229c Mon Sep 17 00:00:00 2001 From: Conor Walsh Date: Thu, 6 Nov 2025 12:56:33 +0000 Subject: [PATCH] [FEATURE] Add SDK language and version to request metadata - LRN-48968 --- .../assessment/standalone_assessment.py | 24 +++++++++++++++++-- learnosity_sdk/request/dataapi.py | 9 ++++--- tests/unit/test_dataapi.py | 23 ++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/docs/quickstart/assessment/standalone_assessment.py b/docs/quickstart/assessment/standalone_assessment.py index aef0274..5361b6c 100644 --- a/docs/quickstart/assessment/standalone_assessment.py +++ b/docs/quickstart/assessment/standalone_assessment.py @@ -7,6 +7,7 @@ # Include server side Learnosity SDK, and set up variables related to user access from learnosity_sdk.request import Init, DataApi from learnosity_sdk.utils import Uuid +from learnosity_sdk._version import __version__ from .. import config # Load consumer key and secret from config.py # Include web server and Jinja templating libraries. from http.server import BaseHTTPRequestHandler, HTTPServer @@ -547,7 +548,6 @@ def do_GET(self) -> None:

{{ name }}

This demo shows how to use the Data API to retrieve items from the Learnosity itembank.

-

Demo 1: Manual Iteration (5 items)

Using request() method with manual pagination via the 'next' pointer.

@@ -565,7 +565,10 @@ def do_GET(self) -> None:

Using results_iter() method to automatically iterate over individual items.

{{ demo3_output }}
- +
+

Request Metadata

+ {{ metadata_info }} +

Back to API Examples

@@ -579,6 +582,22 @@ def do_GET(self) -> None: } data_api = DataApi() + # Extract and display metadata that will be sent with requests + consumer = data_api._extract_consumer(security_packet) + action = data_api._derive_action(itembank_uri, 'get') + sdk_version = __version__.lstrip('v') + sdk_info = f'Python:{sdk_version}' + + metadata_html = f""" +
+ SDK: {sdk_info} +
+ Consumer: {consumer} +
+ Action: {action} +
+ """ + # Demo 1: Manual iteration demo1_html = "" try: @@ -652,6 +671,7 @@ def do_GET(self) -> None: response = template.render( name='Data API Example', + metadata_info=metadata_html, demo1_output=demo1_html, demo2_output=demo2_html, demo3_output=demo3_html diff --git a/learnosity_sdk/request/dataapi.py b/learnosity_sdk/request/dataapi.py index a4ad2ec..e8cf645 100644 --- a/learnosity_sdk/request/dataapi.py +++ b/learnosity_sdk/request/dataapi.py @@ -7,6 +7,7 @@ from learnosity_sdk.exceptions import DataApiException from learnosity_sdk.request import Init +from learnosity_sdk._version import __version__ class DataApi(object): @@ -46,9 +47,9 @@ def _derive_action(self, endpoint: str, action: str) -> str: path_parts = path.split('/') if len(path_parts) > 1: first_segment = path_parts[1].lower() - # Match version patterns: v1, v2, v2023.1.lts, etc. + # Match version patterns: v1, v2, v2023.1.lts, v2025.3.preview1, etc. # Also match: latest, latest-lts, developer - if (re.fullmatch(r"v[\d.]+(?:\.lts)?", first_segment) or + if (re.fullmatch(r"v[\d.]+(?:\.(?:lts|preview\d+))?", first_segment) or first_segment in ("latest", "latest-lts", "developer")): path = '/' + '/'.join(path_parts[2:]) @@ -84,9 +85,11 @@ def request(self, endpoint: str, security_packet: Dict[str, str], derived_action = self._derive_action(endpoint, action) # Add metadata as HTTP headers for ALB routing + sdk_version = __version__.lstrip('v') headers = { 'X-Learnosity-Consumer': consumer, - 'X-Learnosity-Action': derived_action + 'X-Learnosity-Action': derived_action, + 'X-Learnosity-SDK': f'Python:{sdk_version}' } return requests.post(endpoint, data=init.generate(), headers=headers) diff --git a/tests/unit/test_dataapi.py b/tests/unit/test_dataapi.py index b33028d..fd8127d 100644 --- a/tests/unit/test_dataapi.py +++ b/tests/unit/test_dataapi.py @@ -63,6 +63,11 @@ def test_request(self) -> None: assert responses.calls[0].request.headers['X-Learnosity-Consumer'] == 'yis0TYCu7U9V4o7M' assert 'X-Learnosity-Action' in responses.calls[0].request.headers assert responses.calls[0].request.headers['X-Learnosity-Action'] == 'get_/itembank/items' + assert 'X-Learnosity-SDK' in responses.calls[0].request.headers + # Verify SDK header format is "Python:X.Y.Z" (without 'v' prefix) + sdk_header = responses.calls[0].request.headers['X-Learnosity-SDK'] + assert sdk_header.startswith('Python:') + assert not sdk_header.startswith('Python:v') @responses.activate def test_request_iter(self) -> None: @@ -203,6 +208,19 @@ def test_derive_action_with_developer(self) -> None: action = client._derive_action('https://data.learnosity.com/developer/sessions/responses', 'get') assert action == 'get_/sessions/responses' + def test_derive_action_with_preview_version(self) -> None: + """Verify that preview version format like v2025.3.preview1 is correctly stripped""" + client = DataApi() + action = client._derive_action('https://data.learnosity.com/v2025.3.preview1/itembank/items', 'get') + assert action == 'get_/itembank/items' + + def test_derive_action_with_preview_version_multi_digit(self) -> None: + """Verify that preview version with multi-digit preview number is correctly stripped""" + client = DataApi() + action = client._derive_action('https://data.learnosity.com/v2025.1.preview123/itembank/questions', 'get') + assert action == 'get_/itembank/questions' + + @responses.activate def test_metadata_headers_in_paginated_requests(self) -> None: """Verify that metadata headers are sent in all paginated requests""" @@ -220,3 +238,8 @@ def test_metadata_headers_in_paginated_requests(self) -> None: assert call.request.headers['X-Learnosity-Consumer'] == 'yis0TYCu7U9V4o7M' assert 'X-Learnosity-Action' in call.request.headers assert call.request.headers['X-Learnosity-Action'] == 'get_/itembank/items' + assert 'X-Learnosity-SDK' in call.request.headers + # Verify SDK header format is "Python:X.Y.Z" (without 'v' prefix) + sdk_header = call.request.headers['X-Learnosity-SDK'] + assert sdk_header.startswith('Python:') + assert not sdk_header.startswith('Python:v')