Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions docs/quickstart/assessment/standalone_assessment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -547,7 +548,6 @@ def do_GET(self) -> None:
<body>
<h1>{{ name }}</h1>
<p>This demo shows how to use the Data API to retrieve items from the Learnosity itembank.</p>

<div class="demo-section">
<h2>Demo 1: Manual Iteration (5 items)</h2>
<p>Using <code>request()</code> method with manual pagination via the 'next' pointer.</p>
Expand All @@ -565,7 +565,10 @@ def do_GET(self) -> None:
<p>Using <code>results_iter()</code> method to automatically iterate over individual items.</p>
{{ demo3_output }}
</div>

<div class="demo-section">
<h2>Request Metadata</h2>
{{ metadata_info }}
</div>
<p><a href="/">Back to API Examples</a></p>
</body>
</html>
Expand All @@ -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"""
<div>
<strong>SDK:</strong> {sdk_info}
<br>
<strong>Consumer:</strong> {consumer}
<br>
<strong>Action:</strong> {action}
</div>
"""

# Demo 1: Manual iteration
demo1_html = ""
try:
Expand Down Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions learnosity_sdk/request/dataapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:])

Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/test_dataapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"""
Expand All @@ -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')
Loading