From 6477330762d756dd2b9a4cebd5f8880bf34d78de Mon Sep 17 00:00:00 2001 From: Rodrigo Pessoa Date: Tue, 5 Nov 2024 11:30:13 +0000 Subject: [PATCH 1/8] Ail 162/feature/setup precommit basic linting (#95) [FEATURE] Adds pre-commit hooks and Github action for basic linting - AIL-162 --- .github/dependabot.yml | 4 ++-- .github/workflows/ci.yml | 17 +++++++++++++++++ .pre-commit-config.yaml | 18 ++++++++++++++++++ CONTRIBUTING.md | 2 +- ChangeLog.md | 8 ++++++-- README.md | 14 +++++++------- REFERENCE.md | 4 ++-- docs/quickstart/config.py | 4 ++-- learnosity_sdk/request/init.py | 2 +- setup.py | 1 + tests/integration/test_dataapi.py | 2 +- tests/unit/test_dataapi.py | 2 +- 12 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b869b0f..4356ec3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,5 +5,5 @@ updates: schedule: interval: "daily" time: "09:00" - reviewers: - - "@Learnosity/ai-labs-dev" \ No newline at end of file + reviewers: + - "@Learnosity/ai-labs-dev" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70f0f2f..01720ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,23 @@ on: pull_request: jobs: + pre_commit: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install Pre-commit Dependencies + run: pip install pre-commit + + - name: Run Pre-commit Hooks + run: pre-commit run --all-files + tests: runs-on: ubuntu-latest strategy: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9e969a6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-yaml + - id: check-json + - id: pretty-format-json + args: [ --autofix ] + - id: check-merge-conflict + - id: check-symlinks + - id: detect-private-key + + - repo: https://github.com/crate-ci/typos + rev: v1.26.8 + hooks: + - id: typos diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58c6197..328ab94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contribution in the form of [PRs] are welcome. -Why We Are No Longer Accepting Public Issues +Why We Are No Longer Accepting Public Issues After careful consideration, we’ve decided to discontinue accepting issues via GitHub Issues for our public repositories. Here’s why: diff --git a/ChangeLog.md b/ChangeLog.md index 88f59c7..2989ef3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [unreleased] - 2024-11-01 +### Added +- Added pre-commit hooks and Github CI action for code formatting and linting. + ## [v0.3.11] - 2024-11-01 ### Fixed - Deprecation warning for `datetime.utcnow()` @@ -74,9 +78,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [v0.3.1] - 2019-08-07 ### Fixed -- Fixed an issue where the `DataApi` class's `results_iter` method would return no data +- Fixed an issue where the `DataApi` class's `results_iter` method would return no data when receiving responses from Data API endpoints that set the "`data`" field of the - response to an object (like the [itembank/questions endpoint](https://reference.learnosity.com/data-api/endpoints/itembank_endpoints#getQuestions) + response to an object (like the [itembank/questions endpoint](https://reference.learnosity.com/data-api/endpoints/itembank_endpoints#getQuestions) when `item_references` is included in the request). ## [v0.3.0] - 2019-06-17 diff --git a/README.md b/README.md index f587553..7b9f8b7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Learnosity SDK - Python

-

Everything you need to start building your app in Learnosity, with the Python programming language.
+

Everything you need to start building your app in Learnosity, with the Python programming language.
(Prefer another language? Click here)
An official Learnosity open-source project.

@@ -72,7 +72,7 @@ Then, if you're following the tutorial on this page, also run: pip install learnosity_sdk[quickstart] ### **Alternative method 1: download the zip file** -Download the latest version of the SDK as a self-contained ZIP file from the [GitHub Releases](https://github.com/Learnosity/learnosity-sdk-python/releases) page. The distribution ZIP file contains all the necessary dependencies. +Download the latest version of the SDK as a self-contained ZIP file from the [GitHub Releases](https://github.com/Learnosity/learnosity-sdk-python/releases) page. The distribution ZIP file contains all the necessary dependencies. Note: after installation, run this command in the SDK root folder: @@ -163,7 +163,7 @@ host = "localhost" port = 8000 ``` -Now we'll declare the configuration parameters for Items API. These specify which assessment content should be rendered, how it should be displayed, which user is taking this assessment and how their responses should be stored. +Now we'll declare the configuration parameters for Items API. These specify which assessment content should be rendered, how it should be displayed, which user is taking this assessment and how their responses should be stored. ``` python items_request = { @@ -252,10 +252,10 @@ The call to `init()` returns an instance of the ItemsApp, which we can use to pr The Jinja template is rendered by the following line, which will bring in those variables. ``` python - response = template.render(name='Standalone Assessment Example', generated_request=generated_request) + response = template.render(name='Standalone Assessment Example', generated_request=generated_request) ``` -There is some additional code in [standalone_assessment.py](docs/quickstart/assessment/standalone_assessment.py), which runs Python's built-in web server. +There is some additional code in [standalone_assessment.py](docs/quickstart/assessment/standalone_assessment.py), which runs Python's built-in web server. This marks the end of the quick start guide. From here, try modifying the example files yourself, you are welcome to use this code as a basis for your own projects. As mentioned earlier, the Jinja template used here can be easily re-used in another framework, for example Python Flask or Django. @@ -271,7 +271,7 @@ See a more detailed breakdown of all the SDK features, and examples of how to us ### **Additional quick start guides** There are more quick start guides, going beyond the initial quick start topic of loading an assessment, these further tutorials show how to set up authoring and analytics: * [Authoring Items quick start guide](https://help.learnosity.com/hc/en-us/articles/360000754958-Getting-Started-With-the-Author-API) (Author API) - create and edit new Questions and Items for your Item bank, then group your assessment Items into Activities, and -* [Analytics / student reporting quick start guide](https://help.learnosity.com/hc/en-us/articles/360000755838-Getting-Started-With-the-Reports-API) (Reports API) - view the results and scores from an assessment Activity. +* [Analytics / student reporting quick start guide](https://help.learnosity.com/hc/en-us/articles/360000755838-Getting-Started-With-the-Reports-API) (Reports API) - view the results and scores from an assessment Activity. ### **Learnosity demos repository** On our [demo site](https://demos.learnosity.com/), browse through many examples of Learnosity API integration. You can also download the entire demo site source code, the code for any single demo, or browse the codebase directly on GitHub. @@ -318,7 +318,7 @@ We use this data to enable better support and feature planning. [(Back to top)](#table-of-contents) ## Further reading -Thanks for reading to the end! Find more information about developing an app with Learnosity on our documentation sites: +Thanks for reading to the end! Find more information about developing an app with Learnosity on our documentation sites: * [help.learnosity.com](http://help.learnosity.com/hc/en-us) -- general help portal and tutorials, * [reference.learnosity.com](http://reference.learnosity.com) -- developer reference site, and diff --git a/REFERENCE.md b/REFERENCE.md index bc952c0..3ac01a6 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -292,11 +292,11 @@ print(signed_request) ``` ## Further reading -Thanks for reading to the end! Find more information about developing an app with Learnosity on our documentation sites: +Thanks for reading to the end! Find more information about developing an app with Learnosity on our documentation sites: -Back to [README.md](README.md) \ No newline at end of file +Back to [README.md](README.md) diff --git a/docs/quickstart/config.py b/docs/quickstart/config.py index 61084dd..4e4f2c9 100644 --- a/docs/quickstart/config.py +++ b/docs/quickstart/config.py @@ -1,8 +1,8 @@ # The consumerKey and consumerSecret are the public & private # security keys required to access Learnosity APIs and # data. Learnosity will provide keys for your own private account. -# Note: The consumer secret should be in a properly secured credential store, -# and *NEVER* checked into version control. +# Note: The consumer secret should be in a properly secured credential store, +# and *NEVER* checked into version control. # The keys listed here grant access to Learnosity's public demos account. consumer_key = 'yis0TYCu7U9V4o7M' diff --git a/learnosity_sdk/request/init.py b/learnosity_sdk/request/init.py index 90474e2..cab08fd 100644 --- a/learnosity_sdk/request/init.py +++ b/learnosity_sdk/request/init.py @@ -130,7 +130,7 @@ def generate_signature(self) -> str: vals = [] # Add each valid security field. - # The order is signifcant. + # The order is significant. for key in self.security_keys: if key in self.security: vals.append(self.security[key]) diff --git a/setup.py b/setup.py index f0b932e..364eec3 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ ] TEST_REQUIRES = [ + 'pre-commit', 'pytest >=4.6.6', 'pytest-cov >=2.8.1', 'pytest-subtests', diff --git a/tests/integration/test_dataapi.py b/tests/integration/test_dataapi.py index 9df141e..bfd5a42 100644 --- a/tests/integration/test_dataapi.py +++ b/tests/integration/test_dataapi.py @@ -11,7 +11,7 @@ } # WARNING: Normally the consumer secret should not be committed to a public -# repository like this one. Only this specific key is publically available. +# repository like this one. Only this specific key is publicly available. consumer_secret = '74c5fd430cf1242a527f6223aebd42d30464be22' action = 'get' diff --git a/tests/unit/test_dataapi.py b/tests/unit/test_dataapi.py index c98c012..68fad2a 100644 --- a/tests/unit/test_dataapi.py +++ b/tests/unit/test_dataapi.py @@ -16,7 +16,7 @@ def setUp(self): 'domain': 'demos.learnosity.com' } # WARNING: Normally the consumer secret should not be committed to a public - # repository like this one. Only this specific key is publically available. + # repository like this one. Only this specific key is publicly available. self.consumer_secret = '74c5fd430cf1242a527f6223aebd42d30464be22' self.request = { # These items should already exist for the demos consumer From 4f029d85d9bba644ce275de13bea749119f9283d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:08:51 +0000 Subject: [PATCH 2/8] Bump actions/setup-python from 4 to 5 (#97) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01720ac..439af62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' From 3b617f31904582fe0a78b4f326c9ccf9148f1d80 Mon Sep 17 00:00:00 2001 From: Rodrigo Pessoa Date: Wed, 6 Nov 2024 13:44:53 +0000 Subject: [PATCH 3/8] [FEATURE] Adds type validation GitHub action (#96) - Adds type checking to github CI, updates typing & fixes some MyPy issues --- .github/workflows/ci.yml | 17 ++++++++++++++ ChangeLog.md | 1 + .../assessment/standalone_assessment.py | 6 ++--- learnosity_sdk/request/init.py | 16 +++++++------- pyproject.toml | 3 +++ setup.py | 3 +++ tests/integration/test_dataapi.py | 22 ++++++++++--------- tests/unit/test_dataapi.py | 21 ++++++++++-------- tests/unit/test_init.py | 9 ++++---- tests/unit/test_sdk.py | 15 +++++++------ tests/unit/test_uuid.py | 4 ++-- 11 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 439af62..d43b433 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,23 @@ jobs: - name: Run Pre-commit Hooks run: pre-commit run --all-files + type_check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Install Test Dependencies + run: pip install .[test] + + - name: Type Checking with Mypy + run: mypy + tests: runs-on: ubuntu-latest strategy: diff --git a/ChangeLog.md b/ChangeLog.md index 2989ef3..7ca701b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [unreleased] - 2024-11-01 ### Added - Added pre-commit hooks and Github CI action for code formatting and linting. +- Added MyPy with strict settings to enforce type hints (and Github CI action). ## [v0.3.11] - 2024-11-01 ### Fixed diff --git a/docs/quickstart/assessment/standalone_assessment.py b/docs/quickstart/assessment/standalone_assessment.py index cacca7d..deec11d 100644 --- a/docs/quickstart/assessment/standalone_assessment.py +++ b/docs/quickstart/assessment/standalone_assessment.py @@ -322,7 +322,7 @@ # Set up the HTML page template, for serving to the built-in Python web server class LearnosityServer(BaseHTTPRequestHandler): - def createResponse(self,response): + def createResponse(self, response: str) -> None: # Send headers and data back to the client. self.send_response(200) self.send_header("Content-type", "text/html") @@ -330,7 +330,7 @@ def createResponse(self,response): # Send the response to the client. self.wfile.write(response.encode("utf-8")) - def do_GET(self): + def do_GET(self) -> None: if self.path.endswith("/"): @@ -542,7 +542,7 @@ def do_GET(self): self.createResponse(response) -def main(): +def main() -> None: web_server = HTTPServer((host, port), LearnosityServer) print("Server started http://%s:%s. Press Ctrl-c to quit." % (host, port)) try: diff --git a/learnosity_sdk/request/init.py b/learnosity_sdk/request/init.py index cab08fd..1f0ead1 100644 --- a/learnosity_sdk/request/init.py +++ b/learnosity_sdk/request/init.py @@ -35,13 +35,13 @@ class Init(object): def __init__( self, service: str, security: Dict[str, Any], secret: str, - request: Optional[Dict[str, Any]] = None, action:Optional[str] = None) -> None: - # Using None as a default value will throw mypy typecheck issues. This should be addressed + request: Optional[Union[Dict[str, Any], str]] = None, action:Optional[str] = None) -> None: self.service = service self.security = security.copy() self.secret = secret self.request = request - if hasattr(request, 'copy'): + # TODO: Fix improper handling when request is a string + if isinstance(request, dict): self.request = request.copy() self.action = action @@ -193,7 +193,7 @@ def set_service_options(self) -> None: elif self.service == 'assess': self.sign_request_data = False - if 'questionsApiActivity' in self.request: + if self.request is not None and 'questionsApiActivity' in self.request: questionsApi = self.request['questionsApiActivity'] if 'domain' in self.security: @@ -223,14 +223,14 @@ def set_service_options(self) -> None: self.request['questionsApiActivity'].update(questionsApi) elif self.service == 'items' or self.service == 'reports': - if 'user_id' not in self.security and \ - 'user_id' in self.request: + if self.request is not None and ('user_id' not in self.security and 'user_id' in self.request): self.security['user_id'] = self.request['user_id'] elif self.service == 'events': self.sign_request_data = False hashed_users = {} - for user in self.request.get('users', []): + users = self.request.get('users', []) if self.request is not None else [] + for user in users: concat = "{}{}".format(user, self.secret) hashed_users[user] = hashlib.sha256(concat.encode('utf-8')).hexdigest() @@ -244,7 +244,7 @@ def hash_list(self, l: Iterable[Any]) -> str: return '$02$' + signature def add_telemetry_data(self) -> None: - if self.__telemetry_enabled: + if self.request is not None and self.__telemetry_enabled: if 'meta' in self.request: self.request['meta']['sdk'] = self.get_sdk_meta() else: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..31b2311 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.mypy] +strict = true +files = ["learnosity_sdk", "docs", "tests"] diff --git a/setup.py b/setup.py index 364eec3..f175d54 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,9 @@ 'pytest-cov >=2.8.1', 'pytest-subtests', 'responses >=0.8.1', + 'types-requests', + 'types-Jinja2', + 'mypy', ] # Extract the markdown content of the README to be sent to Pypi as the project description page. diff --git a/tests/integration/test_dataapi.py b/tests/integration/test_dataapi.py index bfd5a42..e6b08fe 100644 --- a/tests/integration/test_dataapi.py +++ b/tests/integration/test_dataapi.py @@ -1,3 +1,4 @@ +from typing import Any, Dict, List, cast import unittest import os from learnosity_sdk.request import DataApi @@ -33,7 +34,7 @@ class IntegrationTestDataApiClient(unittest.TestCase): @staticmethod - def __build_base_url(): + def __build_base_url() -> str: env = os.environ env_domain = '' region_domain = '.learnosity.com' @@ -52,7 +53,7 @@ def __build_base_url(): return base_url - def test_real_request(self): + def test_real_request(self) -> None: """Make a request against Data Api to ensure the SDK works""" client = DataApi() res = client.request(self.__build_base_url() + items_endpoint, security, consumer_secret, items_request, @@ -62,9 +63,10 @@ def test_real_request(self): assert len(returned_json['data']) > 0 returned_ref = returned_json['data'][0]['reference'] - assert returned_ref in items_request['references'] + references: List[str] = cast(List[str], items_request['references']) + assert returned_ref in references - def test_paging(self): + def test_paging(self) -> None: """Verify that paging works""" client = DataApi() pages = client.request_iter(self.__build_base_url() + items_endpoint, security, consumer_secret, @@ -78,14 +80,14 @@ def test_paging(self): assert len(results) == 2 assert results == {'item_2', 'item_3'} - def test_real_request_with_special_characters(self): + def test_real_request_with_special_characters(self) -> None: """Make a request against Data Api to ensure the SDK works""" client = DataApi() # Add a reference containing special characters to ensure # signature creation works with special characters in the request - local_items_request = items_request.copy() # prevent modifying the base fixture - local_items_request['references'] = items_request['references'].copy() # prevent modifying the base fixture's list + local_items_request: Dict[str, Any] = items_request.copy() # prevent modifying the base fixture + local_items_request['references'] = cast(List[str], items_request['references'])[:] # prevent modifying the base fixture's list local_items_request['references'].append('тест') res = client.request(self.__build_base_url() + items_endpoint, security, consumer_secret, items_request, @@ -95,9 +97,9 @@ def test_real_request_with_special_characters(self): assert len(returned_json['data']) > 0 returned_ref = returned_json['data'][0]['reference'] - assert returned_ref in items_request['references'] + assert returned_ref in cast(List[str], items_request['references']) - def test_real_question_request(self): + def test_real_question_request(self) -> None: """Make a request against Data Api to ensure the SDK works""" client = DataApi() @@ -114,7 +116,7 @@ def test_real_question_request(self): assert keys == {'py-sdk-test-2019-1', 'py-sdk-test-2019-2'} - def test_question_paging(self): + def test_question_paging(self) -> None: """Verify that paging works""" client = DataApi() diff --git a/tests/unit/test_dataapi.py b/tests/unit/test_dataapi.py index 68fad2a..40fed73 100644 --- a/tests/unit/test_dataapi.py +++ b/tests/unit/test_dataapi.py @@ -1,3 +1,4 @@ +from typing import Any, Dict, cast import unittest import responses from learnosity_sdk.request import DataApi @@ -8,7 +9,7 @@ class UnitTestDataApiClient(unittest.TestCase): Tests to ensure that the Data API client functions correctly. """ - def setUp(self): + def setUp(self) -> None: # This test uses the consumer key and secret for the demos consumer # this is the only consumer with publicly available keys self.security = { @@ -44,7 +45,7 @@ def setUp(self): self.invalid_json = "This is not valid JSON!" @responses.activate - def test_request(self): + def test_request(self) -> None: """ Verify that `request` sends a request after it has been signed """ @@ -55,10 +56,10 @@ def test_request(self): self.action) assert res.json() == self.dummy_responses[0] assert responses.calls[0].request.url == self.endpoint - assert 'signature' in responses.calls[0].request.body + assert 'signature' in cast(Dict[str, Any], responses.calls[0].request.body) @responses.activate - def test_request_iter(self): + def test_request_iter(self) -> None: """Verify that `request_iter` returns an iterator of pages""" for dummy in self.dummy_responses: responses.add(responses.POST, self.endpoint, json=dummy) @@ -74,7 +75,7 @@ def test_request_iter(self): assert results[1]['data'][0]['id'] == 'b' @responses.activate - def test_results_iter(self): + def test_results_iter(self) -> None: """Verify that `result_iter` returns an iterator of results""" self.dummy_responses[1]['data'] = {'id': 'b'} for dummy in self.dummy_responses: @@ -89,7 +90,7 @@ def test_results_iter(self): assert results[1]['id'] == 'b' @responses.activate - def test_results_iter_error_status(self): + def test_results_iter_error_status(self) -> None: """Verify that a DataApiException is raised http status is not ok""" for dummy in self.dummy_responses: responses.add(responses.POST, self.endpoint, json={}, status=500) @@ -99,10 +100,12 @@ def test_results_iter_error_status(self): self.request, self.action)) @responses.activate - def test_results_iter_no_meta_status(self): + def test_results_iter_no_meta_status(self) -> None: """Verify that a DataApiException is raised when 'meta' 'status' is None""" for response in self.dummy_responses: - response['meta']['status'] = None + # This is for typing purposes only, and should always be True + if isinstance(response['meta'], dict): + response['meta']['status'] = None for dummy in self.dummy_responses: responses.add(responses.POST, self.endpoint, json=dummy) @@ -112,7 +115,7 @@ def test_results_iter_no_meta_status(self): self.request, self.action)) @responses.activate - def test_results_iter_invalid_response_data(self): + def test_results_iter_invalid_response_data(self) -> None: """Verify that a DataApiException is raised response data isn't valid JSON""" for dummy in self.dummy_responses: responses.add(responses.POST, self.endpoint, json=None) diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index cd12dc0..0c7654b 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -1,10 +1,11 @@ import collections +from typing import Dict, Optional import unittest import learnosity_sdk.request ServiceTestSpec = collections.namedtuple( - "TestSpec", [ + "ServiceTestSpec", [ "service", "valid", "security", # security can be None to use the default, or an Dict to extend the default @@ -111,7 +112,7 @@ class TestServiceRequests(unittest.TestCase): domain = 'localhost' timestamp = '20140626-0528' - def test_init_generate(self): + def test_init_generate(self) -> None: """ Test that Init.generate() generates the desired initOptions """ @@ -125,7 +126,7 @@ def test_init_generate(self): self.assertFalse(init.is_telemetry_enabled(), 'Telemetry still enabled') self.assertEqual(t.signature, init.generate_signature(), 'Signature mismatch') - def test_no_parameter_mangling(self): + def test_no_parameter_mangling(self) -> None: """ Test that Init.generate() does not modify its parameters """ learnosity_sdk.request.Init.enable_telemetry() for t in ServiceTests: @@ -143,7 +144,7 @@ def test_no_parameter_mangling(self): self.assertEqual(security, security_copy, 'Original security modified by SDK') self.assertEqual(t.request, request_copy, 'Original request modified by SDK') - def _prepare_security(self, add_security=None): + def _prepare_security(self, add_security: Optional[Dict[str, str]]=None) -> Dict[str, str]: # TODO(cera): Much more validation security = { 'consumer_key': self.key, diff --git a/tests/unit/test_sdk.py b/tests/unit/test_sdk.py index 3ff7774..ee4657b 100644 --- a/tests/unit/test_sdk.py +++ b/tests/unit/test_sdk.py @@ -1,8 +1,9 @@ import collections +from typing import Any, Dict import unittest SdkTestSpec = collections.namedtuple( - "TestSpec", ["import_line", "object"]) + "SdkTestSpec", ["import_line", "object"]) SdkImportTests = [ SdkTestSpec( @@ -34,9 +35,9 @@ ), ] -def _run_test(t): - globals = {} - locals = {} +def _run_test(t: Any) -> None: + globals: Dict[str, Any] = {} + locals: Dict[str, Any] = {} exec(t.import_line, globals, locals) eval(t.object, globals, locals) @@ -45,15 +46,15 @@ class TestSdkImport(unittest.TestCase): Tests importing the SDK """ - def test_sdk_imports(self): + def test_sdk_imports(self) -> None: for t in SdkImportTests: _run_test(t) -class TestSdkImport(unittest.TestCase): +class TestSdkModuleImport(unittest.TestCase): """ Tests importing the modules """ - def test_sdk_imports(self): + def test_sdk_imports(self) -> None: for t in SdkModuleTests: _run_test(t) diff --git a/tests/unit/test_uuid.py b/tests/unit/test_uuid.py index 61bb40a..222763e 100644 --- a/tests/unit/test_uuid.py +++ b/tests/unit/test_uuid.py @@ -3,7 +3,7 @@ from learnosity_sdk.utils import Uuid class TestUuid(unittest.TestCase): - def test_generate(self): + def test_generate(self) -> None: """ Tests correctness of the generate() method in Uuid. """ @@ -12,5 +12,5 @@ def test_generate(self): prog = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') result = prog.match(generated) - assert result != None + assert result is not None assert result.group() == generated From 532c40aa88c5052cecbd3befaa7d0302a8b2cc23 Mon Sep 17 00:00:00 2001 From: Ferdia Soper Mac Cafraidh Date: Fri, 22 Nov 2024 15:56:08 +0000 Subject: [PATCH 4/8] [TESTS] Adds pytest-randomly to shuffle tests (#98) * [TESTS] Adds pytest-randomly to shuffle tests * [TESTS] Unit test modifying global data --- ChangeLog.md | 3 ++- learnosity_sdk/_version.py | 2 +- setup.py | 1 + tests/integration/test_dataapi.py | 5 +++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 7ca701b..15c44a3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,10 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [unreleased] - 2024-11-01 +## [v0.3.12] - 2024-11-22 ### Added - Added pre-commit hooks and Github CI action for code formatting and linting. - Added MyPy with strict settings to enforce type hints (and Github CI action). +- Added `pytest-randomly` to shuffle test order ## [v0.3.11] - 2024-11-01 ### Fixed diff --git a/learnosity_sdk/_version.py b/learnosity_sdk/_version.py index 1f8f12d..8d0ba10 100644 --- a/learnosity_sdk/_version.py +++ b/learnosity_sdk/_version.py @@ -1 +1 @@ -__version__ = 'v0.3.11' +__version__ = 'v0.3.12' diff --git a/setup.py b/setup.py index f175d54..52f531f 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ 'pytest >=4.6.6', 'pytest-cov >=2.8.1', 'pytest-subtests', + 'pytest-randomly', 'responses >=0.8.1', 'types-requests', 'types-Jinja2', diff --git a/tests/integration/test_dataapi.py b/tests/integration/test_dataapi.py index e6b08fe..7693ae3 100644 --- a/tests/integration/test_dataapi.py +++ b/tests/integration/test_dataapi.py @@ -103,8 +103,9 @@ def test_real_question_request(self) -> None: """Make a request against Data Api to ensure the SDK works""" client = DataApi() - questions_request['limit'] = 3 - res = client.request(self.__build_base_url() + questions_endpoint, security, consumer_secret, questions_request, + local_questions_request = questions_request.copy() + local_questions_request['limit'] = 3 + res = client.request(self.__build_base_url() + questions_endpoint, security, consumer_secret, local_questions_request, action) returned_json = res.json() From 11679b74a9d68ec073b6c6b5180563747108aa64 Mon Sep 17 00:00:00 2001 From: Conor Walsh Date: Tue, 21 Oct 2025 11:45:41 +0100 Subject: [PATCH 5/8] [FEATURE] Add Consumer and Action to request metadata - LRN-48802 --- README.md | 1 + .../assessment/standalone_assessment.py | 212 ++++++++++++++---- docs/quickstart/css/style.css | 136 ++++++++++- learnosity_sdk/request/dataapi.py | 58 ++++- tests/unit/test_dataapi.py | 97 ++++++++ 5 files changed, 445 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 7b9f8b7..61585b7 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Following are the routes to access our APIs. * Items API : http://localhost:8000/itemsapi * Reports API : http://localhost:8000/reportsapi * Question Editor API : http://localhost:8000/questioneditorapi +* Data API : http://localhost:8000/dataapi Open these pages with your web browser. These are all basic examples of Learnosity's integration. You can interact with these demo pages to try out the various APIs. The Items API example is a basic example of an assessment loaded into a web page with Learnosity's assessment player. You can interact with this demo assessment to try out the various Question types. diff --git a/docs/quickstart/assessment/standalone_assessment.py b/docs/quickstart/assessment/standalone_assessment.py index deec11d..aef0274 100644 --- a/docs/quickstart/assessment/standalone_assessment.py +++ b/docs/quickstart/assessment/standalone_assessment.py @@ -5,12 +5,14 @@ # with `rendering_type: "assess"`. # Include server side Learnosity SDK, and set up variables related to user access -from learnosity_sdk.request import Init +from learnosity_sdk.request import Init, DataApi from learnosity_sdk.utils import Uuid from .. import config # Load consumer key and secret from config.py # Include web server and Jinja templating libraries. from http.server import BaseHTTPRequestHandler, HTTPServer from jinja2 import Template +import json +import os # - - - - - - Section 1: Learnosity server-side configuration - - - - - - # @@ -32,13 +34,6 @@ "domain": host, } -# Author Aide does not accept user_id so we need a separate security object -authorAideSecurity = { - "consumer_key": config.consumer_key, - # Change to the domain used in the browser, e.g. 127.0.0.1, learnosity.com - "domain": host, -} - # Items API configuration parameters. items_request = { # Unique student identifier, a UUID generated above. @@ -268,16 +263,6 @@ } } -# Author Aide API configuration parameters. -author_aide_request = { - "user": { - "id": 'python-demo-user', - "firstname": 'Demos', - "lastname": 'User', - "email": 'demos@learnosity.com' - } -} - # Set up Learnosity initialization data. initItems = Init( "items", security, config.consumer_secret, @@ -304,18 +289,12 @@ request = question_editor_request ) -initAuthorAide = Init( - "authoraide", authorAideSecurity, config.consumer_secret, - request = author_aide_request -) - # Generated request(initOptions) w.r.t all apis generated_request_Items = initItems.generate() generated_request_Questions = initQuestions.generate() generated_request_Author = initAuthor.generate() generated_request_Reports = initReports.generate() generated_request_QuestionEditor = initQuestionEditor.generate() -generated_request_AuthorAide = initAuthorAide.generate() # - - - - - - Section 2: your web page configuration - - - - - -# @@ -332,12 +311,36 @@ def createResponse(self, response: str) -> None: def do_GET(self) -> None: + # Serve CSS file + if self.path == "/css/style.css": + try: + # Get the directory where this script is located + script_dir = os.path.dirname(os.path.abspath(__file__)) + # Navigate to the CSS file location + css_path = os.path.join(script_dir, "..", "css", "style.css") + + with open(css_path, 'r') as f: + css_content = f.read() + + self.send_response(200) + self.send_header("Content-type", "text/css") + self.end_headers() + self.wfile.write(css_content.encode("utf-8")) + return + except FileNotFoundError: + self.send_response(404) + self.send_header("Content-type", "text/plain") + self.end_headers() + self.wfile.write(b"CSS file not found") + return + if self.path.endswith("/"): # Define the page HTML, as a Jinja template, with {{variables}} passed in. template = Template(""" +