diff --git a/openassessment/fileupload/backends/__init__.py b/openassessment/fileupload/backends/__init__.py index 57ad14696a..f127923ad8 100644 --- a/openassessment/fileupload/backends/__init__.py +++ b/openassessment/fileupload/backends/__init__.py @@ -3,7 +3,7 @@ from django.conf import settings -from . import django_storage, filesystem, s3, swift +from . import django_storage, filesystem, gcs, s3, swift def get_backend(): @@ -18,6 +18,8 @@ def get_backend(): return filesystem.Backend() elif backend_setting == "swift": return swift.Backend() + elif backend_setting == "gcs": + return gcs.Backend() elif backend_setting == "django": return django_storage.Backend() else: diff --git a/openassessment/fileupload/backends/gcs.py b/openassessment/fileupload/backends/gcs.py new file mode 100644 index 0000000000..eb472af405 --- /dev/null +++ b/openassessment/fileupload/backends/gcs.py @@ -0,0 +1,73 @@ +"""GCS Bucket File Upload Backend.""" +import functools +import logging + +from ..exceptions import FileUploadInternalError +from .base import BaseBackend + +log = logging.getLogger("openassessment.fileupload.api") # pylint: disable=invalid-name + + +def catch_broad_exception(method): + """Decorator to catch broad exceptions, log them, and raise a FileUploadInternalError.""" + @functools.wraps(method) + def wrapper(*args, **kwargs): + try: + return method(*args, **kwargs) + except Exception as ex: # pylint: disable=broad-except + log.exception( + f"Internal exception occurred while executing ora2 file-upload backend gcs.{method.__name__}: {str(ex)}" + ) + raise FileUploadInternalError(ex) from ex + return wrapper + + +class Backend(BaseBackend): + """ GCS Bucket File Upload Backend. """ + + @catch_broad_exception + def get_upload_url(self, key, content_type): + """Get a signed URL for uploading a file to GCS""" + bucket_name, key_name = self._retrieve_parameters(key) + blob = get_blob_object(bucket_name, key_name) + + return blob.generate_signed_url( + version="v4", + expiration=self.UPLOAD_URL_TIMEOUT, + method="PUT", + content_type=content_type, + ) + + @catch_broad_exception + def get_download_url(self, key): + """Get a signed URL for downloading a file from GCS""" + bucket_name, key_name = self._retrieve_parameters(key) + blob = get_blob_object(bucket_name, key_name) + if not blob.exists(): + return "" + + return blob.generate_signed_url( + version="v4", + expiration=self.DOWNLOAD_URL_TIMEOUT, + method="GET", + ) + + @catch_broad_exception + def remove_file(self, key): + """Remove a file from GCS""" + bucket_name, key_name = self._retrieve_parameters(key) + blob = get_blob_object(bucket_name, key_name) + if blob.exists(): + blob.delete() + return True + + return False + + +def get_blob_object(bucket_name, key_name): + """Get a blob object from GCS""" + # By default; avoid the need of google-cloud-storage library. It will be only needed if gcs backend is used. + from google.cloud import storage # pylint: disable=import-outside-toplevel + + client = storage.Client() + return client.bucket(bucket_name).blob(key_name) diff --git a/openassessment/fileupload/tests/test_api.py b/openassessment/fileupload/tests/test_api.py index 7b49554ea6..760ae0c858 100644 --- a/openassessment/fileupload/tests/test_api.py +++ b/openassessment/fileupload/tests/test_api.py @@ -19,6 +19,7 @@ from openassessment.fileupload import api, exceptions, urls from openassessment.fileupload import views_filesystem as views from openassessment.fileupload.backends.base import Settings as FileUploadSettings +from openassessment.fileupload.backends.gcs import get_blob_object from openassessment.fileupload.backends.filesystem import ( get_cache as get_filesystem_cache, ) @@ -499,3 +500,127 @@ def test_remove(self, key): # File no longer exists download_url = self.backend.get_download_url(self.key) self.assertIsNone(download_url) + + +@override_settings( + ORA2_FILEUPLOAD_BACKEND="gcs", + DEFAULT_FILE_STORAGE="storages.backends.gcloud.GoogleCloudStorage", + FILE_UPLOAD_STORAGE_PREFIX="submissions_on_gcp", + FILE_UPLOAD_STORAGE_BUCKET_NAME="testbucket", + LMS_ROOT_URL="http://foobar.example.com", +) +@ddt.ddt +class TestFileUploadServiceWithGoogleStorageBackend(TestCase): + """ + Test open assessment file upload using GCS storage backend. + """ + class MockBlob: + def __init__(self): + self.exist_testing_flag = False + self.delete_called_during_test = False + + def generate_signed_url(self, **kwargs): + return "http://foobar.example.com/submissions_on_gcs/signed_url_called.txt" + + def delete(self): + self.delete_called_during_test = True + + def exists(self): + return self.exist_testing_flag + + def setUp(self): + super().setUp() + self.backend = api.backends.get_backend() + self.patchers = { + "get_blob_object": patch( + "openassessment.fileupload.backends.gcs.get_blob_object", + return_value=self.MockBlob(), + ), + "log": patch("openassessment.fileupload.backends.gcs.log.exception"), + } + + self.mock_get_blob_object = self.patchers["get_blob_object"].start() + self.mock_log = self.patchers["log"].start() + + def tearDown(self): + """ + Stop the patcher. + """ + for patcher in self.patchers.values(): + patcher.stop() + + def test_get_backend(self): + """ + Ensure the django storage backend is returned when ORA2_FILEUPLOAD_BACKEND="gcs". + """ + self.assertTrue(isinstance(self.backend, api.backends.gcs.Backend)) + + @ddt.data( + ("get_upload_url", {'key': 'whatever', 'content_type': 'whatever'}), + ("get_download_url", {'key': 'whatever'}), + ("remove_file", {'key': 'whatever'}), + ) + @ddt.unpack + def test_errors_raise_file_upload_internal_error(self, method_name, kwargs): + """ + Ensure that exceptions are caught and raised as FileUploadInternalError. + """ + self.mock_get_blob_object.side_effect = Exception("Some error!") + with raises(exceptions.FileUploadInternalError): + getattr(self.backend, method_name)(**kwargs) + self.mock_log.assert_called_once_with( + f"Internal exception occurred while executing ora2 file-upload backend gcs.{method_name}: Some error!" + ) + + def test_get_upload_url(self): + """ + Verify the upload URL. + """ + url = self.backend.get_upload_url("foo", "_text") + self.mock_get_blob_object.assert_called_once_with("testbucket", "submissions_on_gcp/foo") + self.assertEqual(url, "http://foobar.example.com/submissions_on_gcs/signed_url_called.txt") + + def test_get_download_url(self): + """ + Verify the download URL. + """ + url = self.backend.get_download_url("foo") + self.mock_get_blob_object.assert_called_once_with("testbucket", "submissions_on_gcp/foo") + self.assertEqual(url, "") + + self.mock_get_blob_object.return_value.exist_testing_flag = True + url = self.backend.get_download_url("foo") + self.assertEqual(url, "http://foobar.example.com/submissions_on_gcs/signed_url_called.txt") + + def test_remove_file(self): + """ + Verify the remove file method. + """ + self.assertFalse(self.mock_get_blob_object.return_value.delete_called_during_test) + result = self.backend.remove_file("foo") + self.mock_get_blob_object.assert_called_once_with("testbucket", "submissions_on_gcp/foo") + self.assertFalse(result) + + self.mock_get_blob_object.return_value.exist_testing_flag = True + self.assertFalse(self.mock_get_blob_object.return_value.delete_called_during_test) + result = self.backend.remove_file("foo") + self.assertTrue(result) + self.assertTrue(self.mock_get_blob_object.return_value.delete_called_during_test) + + def test_get_blob_object(self): + """ + Verify the get_blob_object method. + """ + with patch("google.cloud.storage") as mock_storage: + blob = Mock(id="the_test_blob") + + bucket = Mock() + bucket.blob.return_value = blob + + client = Mock() + client.bucket.return_value = bucket + + mock_storage.Client.return_value = client + + result = get_blob_object("testbucket", "submissions_on_gcp/foo") + self.assertEqual(result.id, "the_test_blob") diff --git a/openassessment/staffgrader/tests/test_list_staff_workflows.py b/openassessment/staffgrader/tests/test_list_staff_workflows.py index cef2949023..960b2f32e4 100644 --- a/openassessment/staffgrader/tests/test_list_staff_workflows.py +++ b/openassessment/staffgrader/tests/test_list_staff_workflows.py @@ -564,6 +564,7 @@ def assert_annotated_staff_workflow_equal(self, expected, actual, i): @freeze_time(TEST_START_DATE) def test_bulk_fetch_annotated_staff_workflows(self, xblock, set_up_grades, set_up_locks): """ Unit test for bulk_fetch_annotated_staff_workflows """ + assessment_ids = None if set_up_grades: # If we are grading, student_0 graded by staff_1, student_1 ungraded, # student_2 graded by staff_0, student_3 by staff_1 diff --git a/openassessment/xblock/team_mixin.py b/openassessment/xblock/team_mixin.py index 8521f0a539..e62ff77969 100644 --- a/openassessment/xblock/team_mixin.py +++ b/openassessment/xblock/team_mixin.py @@ -134,7 +134,7 @@ def add_team_submission_context( raise TypeError("One of team_submission_uuid or individual_submission_uuid must be provided") if team_submission_uuid: team_submission = get_team_submission(team_submission_uuid) - elif individual_submission_uuid: + else: team_submission = get_team_submission_from_individual_submission(individual_submission_uuid) team = self.teams_service.get_team_by_team_id(team_submission['team_id']) diff --git a/openassessment/xblock/utils/xml.py b/openassessment/xblock/utils/xml.py index 71a6949d14..708a47e4dd 100644 --- a/openassessment/xblock/utils/xml.py +++ b/openassessment/xblock/utils/xml.py @@ -625,6 +625,7 @@ def serialize_training_examples(examples, assessment_el): answer_el = etree.SubElement(example_el, 'answer') try: answer = example_dict.get('answer') + parts = [] if answer is None: parts = [] elif isinstance(answer, dict): diff --git a/requirements/quality.txt b/requirements/quality.txt index 1e387a3ab2..9814230c11 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# make upgrade +# pip-compile --output-file=requirements/quality.txt requirements/quality.in # amqp==5.2.0 # via @@ -20,7 +20,7 @@ asgiref==3.8.1 # via # -r requirements/test.txt # django -astroid==3.1.0 +astroid==3.2.2 # via # pylint # pylint-celery @@ -60,6 +60,7 @@ botocore==1.34.91 cachetools==5.3.3 # via # -r requirements/test.txt + # google-auth # tox celery==5.4.0 # via -r requirements/test.txt @@ -120,11 +121,11 @@ cookiecutter==2.6.0 # via # -r requirements/test.txt # xblock-sdk -coverage[toml]==7.5.0 +coverage[toml]==7.5.3 # via # -r requirements/test.txt # pytest-cov -cryptography==42.0.5 +cryptography==42.0.8 # via # -r requirements/test.txt # moto @@ -146,6 +147,7 @@ django==4.2.11 # -r requirements/test.txt # django-crum # django-model-utils + # django-storages # django-waffle # djangorestframework # edx-django-utils @@ -169,6 +171,8 @@ django-simple-history==3.1.1 # via # -c requirements/constraints.txt # -r requirements/test.txt +django-storages[google]==1.14.3 + # via -r requirements/test.txt django-waffle==4.1.0 # via # -r requirements/test.txt @@ -201,7 +205,7 @@ exceptiongroup==1.2.1 # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==24.11.0 +faker==25.6.0 # via # -r requirements/test.txt # factory-boy @@ -209,7 +213,7 @@ fastavro==1.9.4 # via # -r requirements/test.txt # openedx-events -filelock==3.13.4 +filelock==3.14.0 # via # -r requirements/test.txt # tox @@ -229,6 +233,38 @@ fs-s3fs==0.1.8 # -c requirements/constraints.txt # -r requirements/test.txt # xblock-sdk +google-api-core==2.19.0 + # via + # -r requirements/test.txt + # google-cloud-core + # google-cloud-storage +google-auth==2.30.0 + # via + # -r requirements/test.txt + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.4.1 + # via + # -r requirements/test.txt + # google-cloud-storage +google-cloud-storage==2.16.0 + # via + # -r requirements/test.txt + # django-storages +google-crc32c==1.5.0 + # via + # -r requirements/test.txt + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.0 + # via + # -r requirements/test.txt + # google-cloud-storage +googleapis-common-protos==1.63.1 + # via + # -r requirements/test.txt + # google-api-core html5lib==1.1 # via -r requirements/test.txt idna==2.8 @@ -331,7 +367,7 @@ pbr==6.0.0 # via # -r requirements/test.txt # stevedore -platformdirs==4.2.1 +platformdirs==4.2.2 # via # -r requirements/test.txt # pylint @@ -346,25 +382,44 @@ polib==1.2.0 # via # -r requirements/test.txt # edx-i18n-tools -prompt-toolkit==3.0.43 +prompt-toolkit==3.0.46 # via # -r requirements/test.txt # click-repl +proto-plus==1.23.0 + # via + # -r requirements/test.txt + # google-api-core +protobuf==4.25.3 + # via + # -r requirements/test.txt + # google-api-core + # googleapis-common-protos + # proto-plus psutil==5.9.8 # via # -r requirements/test.txt # edx-django-utils +pyasn1==0.6.0 + # via + # -r requirements/test.txt + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via + # -r requirements/test.txt + # google-auth pycodestyle==2.11.1 # via -r requirements/quality.in pycparser==2.22 # via # -r requirements/test.txt # cffi -pygments==2.17.2 +pygments==2.18.0 # via # -r requirements/test.txt # rich -pylint==3.1.0 +pylint==3.2.3 # via # edx-lint # pylint-celery @@ -394,7 +449,7 @@ pyproject-api==1.6.1 # via # -r requirements/test.txt # tox -pytest==8.1.1 +pytest==8.2.2 # via # -r requirements/test.txt # pytest-cov @@ -440,11 +495,13 @@ requests==2.31.0 # via # -r requirements/test.txt # cookiecutter + # google-api-core + # google-cloud-storage # moto # python-swiftclient # responses # xblock-sdk -responses==0.25.0 +responses==0.25.2 # via # -r requirements/test.txt # moto @@ -452,6 +509,10 @@ rich==13.7.1 # via # -r requirements/test.txt # cookiecutter +rsa==4.9 + # via + # -r requirements/test.txt + # google-auth s3transfer==0.10.1 # via # -r requirements/test.txt @@ -482,7 +543,7 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys -testfixtures==8.1.0 +testfixtures==8.2.0 # via -r requirements/test.txt text-unidecode==1.3 # via @@ -496,9 +557,9 @@ tomli==2.0.1 # pyproject-api # pytest # tox -tomlkit==0.12.4 +tomlkit==0.12.5 # via pylint -tox==4.14.2 +tox==4.15.1 # via -r requirements/test.txt types-python-dateutil==2.9.0.20240316 # via @@ -510,7 +571,6 @@ typing-extensions==4.11.0 # asgiref # astroid # edx-opaque-keys - # faker # kombu # pylint # rich @@ -531,7 +591,7 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.26.0 +virtualenv==20.26.2 # via # -r requirements/test.txt # tox @@ -558,7 +618,7 @@ webob==1.8.7 # -r requirements/test.txt # xblock # xblock-sdk -werkzeug==3.0.2 +werkzeug==3.0.3 # via # -r requirements/test.txt # moto diff --git a/requirements/test-acceptance.txt b/requirements/test-acceptance.txt index c628ff4cc9..436cc98329 100644 --- a/requirements/test-acceptance.txt +++ b/requirements/test-acceptance.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# make upgrade +# pip-compile --output-file=requirements/test-acceptance.txt requirements/test-acceptance.in # amqp==5.2.0 # via @@ -56,6 +56,7 @@ botocore==1.34.91 cachetools==5.3.3 # via # -r requirements/test.txt + # google-auth # tox celery==5.4.0 # via -r requirements/test.txt @@ -111,11 +112,11 @@ cookiecutter==2.6.0 # via # -r requirements/test.txt # xblock-sdk -coverage[toml]==7.5.0 +coverage[toml]==7.5.3 # via # -r requirements/test.txt # pytest-cov -cryptography==42.0.5 +cryptography==42.0.8 # via # -r requirements/test.txt # moto @@ -136,6 +137,7 @@ django==4.2.11 # -r requirements/test.txt # django-crum # django-model-utils + # django-storages # django-waffle # djangorestframework # edx-django-utils @@ -159,6 +161,8 @@ django-simple-history==3.1.1 # via # -c requirements/constraints.txt # -r requirements/test.txt +django-storages[google]==1.14.3 + # via -r requirements/test.txt django-waffle==4.1.0 # via # -r requirements/test.txt @@ -189,7 +193,7 @@ exceptiongroup==1.2.1 # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==24.11.0 +faker==25.6.0 # via # -r requirements/test.txt # factory-boy @@ -197,7 +201,7 @@ fastavro==1.9.4 # via # -r requirements/test.txt # openedx-events -filelock==3.13.4 +filelock==3.14.0 # via # -r requirements/test.txt # tox @@ -217,6 +221,38 @@ fs-s3fs==0.1.8 # -c requirements/constraints.txt # -r requirements/test.txt # xblock-sdk +google-api-core==2.19.0 + # via + # -r requirements/test.txt + # google-cloud-core + # google-cloud-storage +google-auth==2.30.0 + # via + # -r requirements/test.txt + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.4.1 + # via + # -r requirements/test.txt + # google-cloud-storage +google-cloud-storage==2.16.0 + # via + # -r requirements/test.txt + # django-storages +google-crc32c==1.5.0 + # via + # -r requirements/test.txt + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.0 + # via + # -r requirements/test.txt + # google-cloud-storage +googleapis-common-protos==1.63.1 + # via + # -r requirements/test.txt + # google-api-core html5lib==1.1 # via -r requirements/test.txt idna==2.8 @@ -315,7 +351,7 @@ pbr==6.0.0 # via # -r requirements/test.txt # stevedore -platformdirs==4.2.1 +platformdirs==4.2.2 # via # -r requirements/test.txt # tox @@ -329,19 +365,38 @@ polib==1.2.0 # via # -r requirements/test.txt # edx-i18n-tools -prompt-toolkit==3.0.43 +prompt-toolkit==3.0.46 # via # -r requirements/test.txt # click-repl +proto-plus==1.23.0 + # via + # -r requirements/test.txt + # google-api-core +protobuf==4.25.3 + # via + # -r requirements/test.txt + # google-api-core + # googleapis-common-protos + # proto-plus psutil==5.9.8 # via # -r requirements/test.txt # edx-django-utils +pyasn1==0.6.0 + # via + # -r requirements/test.txt + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via + # -r requirements/test.txt + # google-auth pycparser==2.22 # via # -r requirements/test.txt # cffi -pygments==2.17.2 +pygments==2.18.0 # via # -r requirements/test.txt # rich @@ -363,7 +418,7 @@ pyproject-api==1.6.1 # via # -r requirements/test.txt # tox -pytest==8.1.1 +pytest==8.2.2 # via # -r requirements/test-acceptance.in # -r requirements/test.txt @@ -410,11 +465,13 @@ requests==2.31.0 # via # -r requirements/test.txt # cookiecutter + # google-api-core + # google-cloud-storage # moto # python-swiftclient # responses # xblock-sdk -responses==0.25.0 +responses==0.25.2 # via # -r requirements/test.txt # moto @@ -422,6 +479,10 @@ rich==13.7.1 # via # -r requirements/test.txt # cookiecutter +rsa==4.9 + # via + # -r requirements/test.txt + # google-auth s3transfer==0.10.1 # via # -r requirements/test.txt @@ -451,7 +512,7 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys -testfixtures==8.1.0 +testfixtures==8.2.0 # via -r requirements/test.txt text-unidecode==1.3 # via @@ -464,7 +525,7 @@ tomli==2.0.1 # pyproject-api # pytest # tox -tox==4.14.2 +tox==4.15.1 # via -r requirements/test.txt types-python-dateutil==2.9.0.20240316 # via @@ -475,7 +536,6 @@ typing-extensions==4.11.0 # -r requirements/test.txt # asgiref # edx-opaque-keys - # faker # kombu # rich tzdata==2024.1 @@ -495,7 +555,7 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.26.0 +virtualenv==20.26.2 # via # -r requirements/test.txt # tox @@ -522,7 +582,7 @@ webob==1.8.7 # -r requirements/test.txt # xblock # xblock-sdk -werkzeug==3.0.2 +werkzeug==3.0.3 # via # -r requirements/test.txt # moto diff --git a/requirements/test.in b/requirements/test.in index 2d65218ec7..302f0b7508 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -17,3 +17,4 @@ tox more-itertools xblock-sdk celery # Celery task queue framework +django-storages[google] # Django storage backend for Google Cloud Storage diff --git a/requirements/test.txt b/requirements/test.txt index c265e367bf..47c927f3ac 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# make upgrade +# pip-compile --output-file=requirements/test.txt requirements/test.in # amqp==5.2.0 # via kombu @@ -46,7 +46,9 @@ botocore==1.34.91 # moto # s3transfer cachetools==5.3.3 - # via tox + # via + # google-auth + # tox celery==5.4.0 # via -r requirements/test.in certifi==2024.2.2 @@ -90,11 +92,11 @@ colorama==0.4.6 # via tox cookiecutter==2.6.0 # via xblock-sdk -coverage[toml]==7.5.0 +coverage[toml]==7.5.3 # via # -r requirements/test.in # pytest-cov -cryptography==42.0.5 +cryptography==42.0.8 # via moto ddt==1.0.0 # via @@ -104,11 +106,13 @@ defusedxml==0.7.1 # via -r requirements/base.txt distlib==0.3.8 # via virtualenv +django==4.2.11 # via # -c requirements/constraints.txt # -r requirements/base.txt # django-crum # django-model-utils + # django-storages # django-waffle # djangorestframework # edx-django-utils @@ -132,11 +136,14 @@ django-simple-history==3.1.1 # via # -c requirements/constraints.txt # -r requirements/base.txt +django-storages[google]==1.14.3 + # via -r requirements/test.in django-waffle==4.1.0 # via # -r requirements/base.txt # edx-django-utils # edx-toggles +djangorestframework==3.15.1 # via # -r requirements/base.txt # edx-submissions @@ -159,13 +166,13 @@ exceptiongroup==1.2.1 # via pytest factory-boy==3.3.0 # via -r requirements/test.in -faker==24.11.0 +faker==25.6.0 # via factory-boy fastavro==1.9.4 # via # -r requirements/base.txt # openedx-events -filelock==3.13.4 +filelock==3.14.0 # via # tox # virtualenv @@ -183,6 +190,27 @@ fs-s3fs==0.1.8 # via # -c requirements/constraints.txt # xblock-sdk +google-api-core==2.19.0 + # via + # google-cloud-core + # google-cloud-storage +google-auth==2.30.0 + # via + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.4.1 + # via google-cloud-storage +google-cloud-storage==2.16.0 + # via django-storages +google-crc32c==1.5.0 + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.0 + # via google-cloud-storage +googleapis-common-protos==1.63.1 + # via google-api-core html5lib==1.1 # via -r requirements/base.txt idna==2.8 @@ -272,7 +300,7 @@ pbr==6.0.0 # via # -r requirements/base.txt # stevedore -platformdirs==4.2.1 +platformdirs==4.2.2 # via # tox # virtualenv @@ -284,17 +312,30 @@ polib==1.2.0 # via # -r requirements/base.txt # edx-i18n-tools -prompt-toolkit==3.0.43 +prompt-toolkit==3.0.46 # via click-repl +proto-plus==1.23.0 + # via google-api-core +protobuf==4.25.3 + # via + # google-api-core + # googleapis-common-protos + # proto-plus psutil==5.9.8 # via # -r requirements/base.txt # edx-django-utils +pyasn1==0.6.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth pycparser==2.22 # via # -r requirements/base.txt # cffi -pygments==2.17.2 +pygments==2.18.0 # via rich pymongo==3.13.0 # via @@ -308,7 +349,7 @@ pypng==0.20220715.0 # via xblock-sdk pyproject-api==1.6.1 # via tox -pytest==8.1.1 +pytest==8.2.2 # via # -r requirements/test.in # pytest-cov @@ -354,14 +395,18 @@ requests==2.31.0 # via # -r requirements/base.txt # cookiecutter + # google-api-core + # google-cloud-storage # moto # python-swiftclient # responses # xblock-sdk -responses==0.25.0 +responses==0.25.2 # via moto rich==13.7.1 # via cookiecutter +rsa==4.9 + # via google-auth s3transfer==0.10.1 # via # -r requirements/base.txt @@ -391,7 +436,7 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys -testfixtures==8.1.0 +testfixtures==8.2.0 # via -r requirements/test.in text-unidecode==1.3 # via @@ -403,7 +448,7 @@ tomli==2.0.1 # pyproject-api # pytest # tox -tox==4.14.2 +tox==4.15.1 # via -r requirements/test.in types-python-dateutil==2.9.0.20240316 # via arrow @@ -412,7 +457,6 @@ typing-extensions==4.11.0 # -r requirements/base.txt # asgiref # edx-opaque-keys - # faker # kombu # rich tzdata==2024.1 @@ -430,7 +474,7 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.26.0 +virtualenv==20.26.2 # via tox voluptuous==0.14.2 # via @@ -453,7 +497,7 @@ webob==1.8.7 # -r requirements/base.txt # xblock # xblock-sdk -werkzeug==3.0.2 +werkzeug==3.0.3 # via moto xblock==4.0.1 # via diff --git a/tox.ini b/tox.ini index 05a0be162d..c8b33a3c4b 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ deps = django42: Django>=4.2,<5.0 commands = - pytest + pytest {posargs} [testenv:js] allowlist_externals = make