From 0f674fd9ba247de2eda3740f9c56a11406b56772 Mon Sep 17 00:00:00 2001 From: dansubak Date: Fri, 17 Oct 2025 16:23:09 -0400 Subject: [PATCH 1/5] Adds WIP LTI 1.3 integration. "Successfully" completes a launch request against a tool emulator configured at https://saltire.lti.app/tool --- authentication/urls.py | 4 +- authentication/views.py | 160 +++++- env/backend.env | 4 +- main/settings.py | 2 + poetry.lock | 664 ++++++++++++++++++++++++- pyproject.toml | 1 + templates/lti_launch_request_form.html | 30 ++ 7 files changed, 859 insertions(+), 6 deletions(-) create mode 100644 templates/lti_launch_request_form.html diff --git a/authentication/urls.py b/authentication/urls.py index 5be5f98353..e22a09c57a 100644 --- a/authentication/urls.py +++ b/authentication/urls.py @@ -2,9 +2,11 @@ from django.urls import re_path -from authentication.views import CustomLoginView, CustomLogoutView +from authentication.views import CustomLoginView, CustomLogoutView, lti_auth, lti_login urlpatterns = [ re_path(r"^logout", CustomLogoutView.as_view(), name="logout"), re_path(r"^login", CustomLoginView.as_view(), name="login"), + re_path(r"^lti_login", lti_login, name="lti_login"), + re_path(r"^lti_auth", lti_auth, name="lti_auth"), ] diff --git a/authentication/views.py b/authentication/views.py index 91a8a1e91b..001bf7498d 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,13 +1,17 @@ +# ruff: noqa: ARG001, F821, E501, TD002, TD003, FIX002, ERA001, SIM115, PTH123, T201, D401 """Authentication views""" import logging from urllib.parse import urljoin from django.contrib.auth import logout -from django.shortcuts import redirect +from django.http import HttpResponse +from django.shortcuts import redirect, render from django.utils.http import url_has_allowed_host_and_scheme, urlencode from django.utils.text import slugify from django.views import View +from django.views.decorators.csrf import csrf_exempt +from lti_consumer.data import Lti1p3LaunchData from main import settings from main.middleware.apisix_user import ApisixUserMiddleware, decode_apisix_headers @@ -120,3 +124,157 @@ def get( profile.save() return redirect(redirect_url) + + +# lti_oidc_url = 'localhost:8000/oidc/initiate' +# lti_launch_url = 'localhost:8000/launch' +# client_id = 'learn-jupyter-notebooks' +# deployment_id = '0cb94445-2066-4295-9c03-4bfdc1d8aacb' + +lti_oidc_url = "https://saltire.lti.app/tool" +lti_launch_url = "https://saltire.lti.app/tool" +redirect_uris = [lti_launch_url] +client_id = "learn-jupyter-notebooks" +deployment_id = "cb5fd9ae5c92eddbc5054e27fa010c5478d2f7c3" +platform_private_key_id = "JZOHScC4BQ" +platform_public_key = open("platform_id_rsa.pub").read() +platform_private_key = open("platform_id_rsa").read() +print(platform_public_key) +rsa_key = platform_private_key +rsa_key_id = platform_private_key_id +iss = "http://api.open.odl.local" +tool_public_key = None # PEM tool public key, not required just to launch LTI + +""" +# lti1p3platform - the only platform impl I could find packaged. Not well supported, no GH. +# Consider using https://github.com/academic-innovation/django-lti for client, though it's probably not gonna work OOTB +# Below is the OpenEdX impl. It's technically request agnostic, so we could vendor it in and just use pieces we care about +# https://github.com/openedx/xblock-lti-consumer/blob/92ea78f9fee5aa8551511db5f0587d1673e25159/lti_consumer/lti_1p3/README.md?plain=1 +Render a view which starts an LTI negotiation +Take a notebook link as the next parameter +At the end of the negotiation, we want to end up on a page that shows our learn logged in user's info at minimum + +""" + + +def lti_login( + request, + *args, + **kwargs, +): + """ + Render form taking relevant bits of info to start LTI negotiation + """ + + return lti_preflight_request(request) + return HttpResponse( + f"LTI_Login GET endpoint - successfully imported lti_consumer and logged in as user {request.user}. Is Superuser {request.user.is_superuser}" + ) + + +@csrf_exempt +def lti_auth( + request, + *args, + **kwargs, +): + """ + Perform LTI negotiation and redirect to notebook URL + """ + return lti_launch_endpoint(request) + return HttpResponse( + f"LTI_Login POST endpoint - successfully imported lti_consumer and logged in as user {request.user}. Is Superuser {request.user.is_superuser}" + ) + + +def _get_lti1p3_consumer(): + """ + Returns an configured instance of LTI consumer. + """ + from lti_consumer.lti_1p3.consumer import LtiConsumer1p3 + + return LtiConsumer1p3( + # Tool urls + lti_oidc_url=lti_oidc_url, + lti_launch_url=lti_launch_url, + redirect_uris=redirect_uris, + # Platform and deployment configuration + iss=iss, # Whatever the host is for the platform? + client_id=client_id, + deployment_id=deployment_id, + # Platform key + rsa_key=rsa_key, + rsa_key_id=rsa_key_id, + # Tool key + # tool_key=tool_public_key, + ) + + +def public_keyset(request): + """ + Return LTI Public Keyset url. + + This endpoint must be configured in the tool. + """ + return JsonResponse( + _get_lti1p3_consumer().get_public_keyset(), content_type="application/json" + ) + + +def lti_preflight_request(request): + """ + Endpoint that'll render the initial OIDC authorization request form + and submit it to the tool. + + The platform needs to know the tool OIDC endpoint. + """ + lti_consumer = _get_lti1p3_consumer() + # AFAICT, config_id and resource_link_id are pretty much used to get stuff out of cache later + launch_data = Lti1p3LaunchData( + # TODO: This doesn't work w/ anonymous users as they don't have a global_id. Shouldn't happen in practice, so just bail + # user_id=request.user.global_id, + # user_role="admin" if request.user.is_superuser else "student", + user_id="test-user-id", + user_role="staff", + config_id=client_id, + resource_link_id="link_id", + ) + # This template should render a simple redirection to the URL + # provided by the context through the `oidc_url` key above. + # This can also be a redirect. + return redirect(lti_consumer.prepare_preflight_url(launch_data)) + + +def lti_launch_endpoint(request): + """ + Platform endpoint that'll receive OIDC login request variables and generate launch request. + """ + lti_consumer = _get_lti1p3_consumer() + context = {} + + # Required user claim data + # TODO: For some reason (CORS?) we're dropping our session after the redirect from the OIDC initiation endpoint. + # This will need to be solved. + lti_consumer.set_user_data( + # user_id=request.user.global_id, + # Pass django user role to library + # role="admin" if request.user.is_superuser else "student", + user_id="test-user-id", + role="staff", + ) + # TODO: Not sure what this is used for but it's required. Look it up later + lti_consumer.set_resource_link_claim("link_id") + + context.update( + { + "preflight_response": dict(request.POST), + "launch_request": lti_consumer.generate_launch_request( + preflight_response=request.POST + ), + } + ) + + context.update({"launch_url": lti_consumer.launch_url}) + # This template should render a form, and then submit it to the tool's launch URL, as + # described in http://www.imsglobal.org/spec/lti/v1p3/#lti-message-general-details + return render(request, "lti_launch_request_form.html", context) diff --git a/env/backend.env b/env/backend.env index ce267160e2..709ac2d4bf 100644 --- a/env/backend.env +++ b/env/backend.env @@ -6,8 +6,8 @@ CELERY_TASK_ALWAYS_EAGER=False CELERY_WORKER_CONCURRENCY=8 # local hostname shenanigans -CORS_ALLOWED_ORIGINS='["http://open.odl.local:8062"]' -CSRF_TRUSTED_ORIGINS='["http://open.odl.local:8062", "http://api.open.odl.local:8063"]' +CORS_ALLOWED_ORIGINS='["http://open.odl.local:8062", "https://saltire.lti.app"]' +CSRF_TRUSTED_ORIGINS='["http://open.odl.local:8062", "http://api.open.odl.local:8063", "https://saltire.lti.app"]' CSRF_COOKIE_DOMAIN=open.odl.local CSRF_COOKIE_SECURE=False MITOL_COOKIE_DOMAIN=open.odl.local diff --git a/main/settings.py b/main/settings.py index c767607372..f69690ae24 100644 --- a/main/settings.py +++ b/main/settings.py @@ -854,3 +854,5 @@ def get_all_config_keys(): OPENTELEMETRY_TRACES_BATCH_SIZE = get_int("OPENTELEMETRY_TRACES_BATCH_SIZE", 512) OPENTELEMETRY_EXPORT_TIMEOUT_MS = get_int("OPENTELEMETRY_EXPORT_TIMEOUT_MS", 5000) CANVAS_TUTORBOT_FOLDER = get_string("CANVAS_TUTORBOT_FOLDER", "web_resources/ai/tutor/") +# Required while geenrating the LTI launch message +PLATFORM_NAME = "mit-learn" diff --git a/poetry.lock b/poetry.lock index 19cd555631..592615ca69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -223,6 +223,18 @@ files = [ {file = "anys-0.3.1.tar.gz", hash = "sha256:5bcda88fcc490eda840a247c8c18b7a350051f530c288ad530e94c8d139602de"}, ] +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + [[package]] name = "asgiref" version = "3.8.1" @@ -382,6 +394,24 @@ files = [ {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, ] +[[package]] +name = "bleach" +version = "6.2.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, + {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, +] + +[package.dependencies] +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.5)"] + [[package]] name = "blessed" version = "1.21.0" @@ -1520,6 +1550,23 @@ files = [ [package.extras] dev = ["black", "flake8", "therapist", "tox", "twine"] +[[package]] +name = "django-config-models" +version = "2.9.0" +description = "Configuration models for Django allowing config management with auditing." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "django_config_models-2.9.0-py2.py3-none-any.whl", hash = "sha256:7a5348471af12119591d169b9be5cfb5aaa274036797b48b0aef64422edea97a"}, + {file = "django_config_models-2.9.0.tar.gz", hash = "sha256:08896b80f0d5723f55a4a2dc67294fecd501aa4058c5560e94702fcb7145b42d"}, +] + +[package.dependencies] +Django = "*" +djangorestframework = ">=3.6" +edx-django-utils = "*" + [[package]] name = "django-cors-headers" version = "4.7.0" @@ -1536,6 +1583,21 @@ files = [ asgiref = ">=3.6" django = ">=4.2" +[[package]] +name = "django-crum" +version = "0.7.9" +description = "Django middleware to capture current request and user." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "django-crum-0.7.9.tar.gz", hash = "sha256:65e9bc0f070a663fafc4d9e357f45fd4e6f01838b20a9e2fb7670f5706754288"}, + {file = "django_crum-0.7.9-py2.py3-none-any.whl", hash = "sha256:037cc8b822975bb1d41cd24269b59a512cc77448fadc3f34ab9a17b229b4b471"}, +] + +[package.dependencies] +django = ">=1.8" + [[package]] name = "django-filter" version = "2.4.0" @@ -1706,6 +1768,22 @@ files = [ [package.dependencies] django = "*" +[[package]] +name = "django-statici18n" +version = "2.6.0" +description = "A Django app that compiles i18n JavaScript catalogs to static files." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "django_statici18n-2.6.0-py2.py3-none-any.whl", hash = "sha256:1bedd5cbffc2cf1772808995fb818756592fc9cd88b79547b6a25a747b23087d"}, + {file = "django_statici18n-2.6.0.tar.gz", hash = "sha256:87ed5adb15d6b32d20f438df76f39f16cc5804f452a8785bebc64729ff08cfb4"}, +] + +[package.dependencies] +Django = ">=3.2" +django-appconf = ">=1.0" + [[package]] name = "django-storages" version = "1.14.6" @@ -1770,6 +1848,24 @@ files = [ django = "*" typing-extensions = "*" +[[package]] +name = "django-waffle" +version = "5.0.0" +description = "A feature flipper for Django." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "django_waffle-5.0.0-py3-none-any.whl", hash = "sha256:3312851d9d926b76b9e90712355781700a383b82b5bf2b61e1f1be97532c0f3d"}, + {file = "django_waffle-5.0.0.tar.gz", hash = "sha256:62f9d00eedf68dafb82657beab56e601bddedc1ea1ccfef91d83df8658708509"}, +] + +[package.dependencies] +django = ">=4.2" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] + [[package]] name = "django-webpack-loader" version = "3.1.1" @@ -1797,6 +1893,27 @@ files = [ [package.dependencies] django = ">=4.2" +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] + [[package]] name = "docopt" version = "0.6.2" @@ -1892,6 +2009,60 @@ uritemplate = ">=2.0.0" offline = ["drf-spectacular-sidecar"] sidecar = ["drf-spectacular-sidecar"] +[[package]] +name = "edx-ccx-keys" +version = "2.0.2" +description = "Opaque key support custom courses on edX" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "edx_ccx_keys-2.0.2-py3-none-any.whl", hash = "sha256:4071fc0f3a76288c2f7d784a8b53aa8c5e672604a1d8c5a1891fa1bf5612791a"}, + {file = "edx_ccx_keys-2.0.2.tar.gz", hash = "sha256:da17b2b3281011a540d5ab847acf7946c4a57dd10c0931cec3ff9ac3c1fa33a0"}, +] + +[package.dependencies] +edx-opaque-keys = ">=2.0.0" +six = ">=1.10.0" + +[[package]] +name = "edx-django-utils" +version = "8.0.1" +description = "EdX utilities for Django Application development." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "edx_django_utils-8.0.1-py2.py3-none-any.whl", hash = "sha256:849c253b5e14ddfbdc47c5e36143fad54147778a53e0b49095c1fecab24edd9e"}, + {file = "edx_django_utils-8.0.1.tar.gz", hash = "sha256:fadf4954b12556a79fadd9cc6bc8efd45da4a90c043a63c915339a3cc90d610b"}, +] + +[package.dependencies] +click = "*" +Django = "*" +django-crum = "*" +django-waffle = "*" +psutil = "*" +PyNaCl = "*" +stevedore = "*" + +[[package]] +name = "edx-opaque-keys" +version = "3.0.0" +description = "" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "edx_opaque_keys-3.0.0-py3-none-any.whl", hash = "sha256:1a03a37649bf389365a8e2ac4e2395436fbfcfa596adb59af58417a0fdb87df0"}, + {file = "edx_opaque_keys-3.0.0.tar.gz", hash = "sha256:e304181450b153683c83ca2f3aac7106cc2ee2112df46003bb851a48615ba794"}, +] + +[package.dependencies] +pymongo = "*" +stevedore = ">=0.14.1" +typing-extensions = "*" + [[package]] name = "events" version = "0.5" @@ -2252,6 +2423,43 @@ files = [ {file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"}, ] +[[package]] +name = "fs" +version = "2.4.16" +description = "Python's filesystem abstraction layer" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "fs-2.4.16-py2.py3-none-any.whl", hash = "sha256:660064febbccda264ae0b6bace80a8d1be9e089e0a5eb2427b7d517f9a91545c"}, + {file = "fs-2.4.16.tar.gz", hash = "sha256:ae97c7d51213f4b70b6a958292530289090de3a7e15841e108fbe144f069d313"}, +] + +[package.dependencies] +appdirs = ">=1.4.3,<1.5.0" +setuptools = "*" +six = ">=1.10,<2.0" + +[package.extras] +scandir = ["scandir (>=1.5,<2.0) ; python_version < \"3.5\""] + +[[package]] +name = "fs-s3fs" +version = "1.1.1" +description = "Amazon S3 filesystem for PyFilesystem2" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "fs-s3fs-1.1.1.tar.gz", hash = "sha256:b57f8c7664460ff7b451b4b44ca2ea9623a374d74e1284c2d5e6df499dc7976c"}, + {file = "fs_s3fs-1.1.1-py2.py3-none-any.whl", hash = "sha256:9ba160eaa93390cc5992a857675666cb2fbb3721b872474dfdc659a715c39280"}, +] + +[package.dependencies] +boto3 = ">=1.9,<2.0" +fs = ">=2.4,<3.0" +six = ">=1.10,<2.0" + [[package]] name = "fsspec" version = "2025.3.2" @@ -3360,6 +3568,21 @@ files = [ {file = "joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5"}, ] +[[package]] +name = "jsonfield" +version = "3.2.0" +description = "A reusable Django field that allows you to store validated JSON in your model." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "jsonfield-3.2.0-py3-none-any.whl", hash = "sha256:ca4f6bf89c819f293e77074d613c0021e3c4e8521be95c73d03caecb4372e1ee"}, + {file = "jsonfield-3.2.0.tar.gz", hash = "sha256:ca53871bc3308ae4f4cddc3b4f99ed5c6fc6abb1832fbfb499bc6da566c70e4a"}, +] + +[package.dependencies] +django = ">=4.2" + [[package]] name = "jsonpatch" version = "1.33" @@ -3643,6 +3866,22 @@ openai-agents = ["openai-agents (>=0.0.3,<0.1)"] otel = ["opentelemetry-api (>=1.30.0,<2.0.0)", "opentelemetry-exporter-otlp-proto-http (>=1.30.0,<2.0.0)", "opentelemetry-sdk (>=1.30.0,<2.0.0)"] pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"] +[[package]] +name = "lazy" +version = "1.6" +description = "Lazy attributes for Python objects" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "lazy-1.6-py2.py3-none-any.whl", hash = "sha256:449375c125c7acac6b7a93f71b8e7ccb06546c37b161613f92d2d3981f793244"}, + {file = "lazy-1.6.tar.gz", hash = "sha256:7127324ec709e8324f08cb4611c1abe01776bda53bb9ce68dc5dfa46ca0ed3e9"}, +] + +[package.extras] +docs = ["sphinx (==5.3.0)", "sphinx-rtd-theme (==1.0.0)"] +mypy = ["mypy"] + [[package]] name = "litellm" version = "1.76.0" @@ -3998,6 +4237,38 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] +[[package]] +name = "lti-consumer-xblock" +version = "9.14.2" +description = "This XBlock implements the consumer side of the LTI specification." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "lti_consumer_xblock-9.14.2-py3-none-any.whl", hash = "sha256:001c353f748ac30adba7590b124f6909e637a06e0bf83b1cd2f110116cbf02f5"}, + {file = "lti_consumer_xblock-9.14.2.tar.gz", hash = "sha256:737f2c053464b416dce33da5fdb79a3a5646355def5c6eac73837609fc24bab8"}, +] + +[package.dependencies] +attrs = "*" +bleach = ">=6.0.0" +django = "*" +django-config-models = "*" +django-filter = "*" +django-statici18n = "*" +edx-ccx-keys = "*" +edx-opaque-keys = "*" +jsonfield = "*" +lazy = "*" +lxml = "*" +mako = "*" +oauthlib = "*" +openedx-django-pyfs = "*" +openedx-filters = "*" +pycryptodomex = "*" +pyjwt = "*" +XBlock = "*" + [[package]] name = "lxml" version = "6.0.1" @@ -4130,6 +4401,26 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] +[[package]] +name = "mako" +version = "1.3.10" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown" version = "3.8" @@ -5007,6 +5298,40 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] +[[package]] +name = "openedx-django-pyfs" +version = "3.8.0" +description = "Django pyfilesystem integration" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "openedx_django_pyfs-3.8.0-py3-none-any.whl", hash = "sha256:5b17122534d0febf38729fa994d581db5fffc55565895b0290884116cea3892f"}, + {file = "openedx_django_pyfs-3.8.0.tar.gz", hash = "sha256:79f19822880fcabb91531bf578a133b8b86381c160f030436c3fd1bbc616b420"}, +] + +[package.dependencies] +django = "*" +fs = ">=2.0.1" +fs-s3fs = "*" + +[[package]] +name = "openedx-filters" +version = "2.1.0" +description = "Open edX Filters from Hooks Extensions Framework (OEP-50)." +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "openedx_filters-2.1.0-py2.py3-none-any.whl", hash = "sha256:33c5b7388e8de3f8cfda5cdee0220097999b5d4ba06a644889b5fdfd9dca0a36"}, + {file = "openedx_filters-2.1.0.tar.gz", hash = "sha256:6b721ae4a1855843a06522517dd17743bef91b1f76d6208d5fc9e4e349932167"}, +] + +[package.dependencies] +django = "*" +edx-opaque-keys = {version = "*", extras = ["django"]} +setuptools = "*" + [[package]] name = "opensearch-dsl" version = "2.1.0" @@ -5962,7 +6287,7 @@ version = "6.1.1" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, @@ -6241,6 +6566,57 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pycryptodomex" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodomex-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:add243d204e125f189819db65eed55e6b4713f70a7e9576c043178656529cec7"}, + {file = "pycryptodomex-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1c6d919fc8429e5cb228ba8c0d4d03d202a560b421c14867a65f6042990adc8e"}, + {file = "pycryptodomex-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1c3a65ad441746b250d781910d26b7ed0a396733c6f2dbc3327bd7051ec8a541"}, + {file = "pycryptodomex-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:47f6d318fe864d02d5e59a20a18834819596c4ed1d3c917801b22b92b3ffa648"}, + {file = "pycryptodomex-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:d9825410197a97685d6a1fa2a86196430b01877d64458a20e95d4fd00d739a08"}, + {file = "pycryptodomex-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:267a3038f87a8565bd834317dbf053a02055915acf353bf42ededb9edaf72010"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51"}, + {file = "pycryptodomex-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:febec69c0291efd056c65691b6d9a339f8b4bc43c6635b8699471248fe897fea"}, + {file = "pycryptodomex-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:c84b239a1f4ec62e9c789aafe0543f0594f0acd90c8d9e15bcece3efe55eca66"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7de1e40a41a5d7f1ac42b6569b10bcdded34339950945948529067d8426d2785"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bffc92138d75664b6d543984db7893a628559b9e78658563b0395e2a5fb47ed9"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df027262368334552db2c0ce39706b3fb32022d1dce34673d0f9422df004b96a"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e79f1aaff5a3a374e92eb462fa9e598585452135012e2945f96874ca6eeb1ff"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:27e13c80ac9a0a1d050ef0a7e0a18cc04c8850101ec891815b6c5a0375e8a245"}, + {file = "pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da"}, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -6455,6 +6831,100 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pymongo" +version = "4.15.3" +description = "PyMongo - the Official MongoDB Python driver" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pymongo-4.15.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:482ca9b775747562ce1589df10c97a0e62a604ce5addf933e5819dd967c5e23c"}, + {file = "pymongo-4.15.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7eb497519f42ac89c30919a51f80e68a070cfc2f3b0543cac74833cd45a6b9c"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a0a054e9937ec8fdb465835509b176f6b032851c8648f6a5d1b19932d0eacd6"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49fd6e158cf75771b2685a8a221a40ab96010ae34dd116abd06371dc6c38ab60"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82a490f1ade4ec6a72068e3676b04c126e3043e69b38ec474a87c6444cf79098"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:982107c667921e896292f4be09c057e2f1a40c645c9bfc724af5dd5fb8398094"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45aebbd369ca79b7c46eaea5b04d2e4afca4eda117b68965a07a9da05d774e4d"}, + {file = "pymongo-4.15.3-cp310-cp310-win32.whl", hash = "sha256:90ad56bd1d769d2f44af74f0fd0c276512361644a3c636350447994412cbc9a1"}, + {file = "pymongo-4.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:8bd6dd736f5d07a825caf52c38916d5452edc0fac7aee43ec67aba6f61c2dbb7"}, + {file = "pymongo-4.15.3-cp310-cp310-win_arm64.whl", hash = "sha256:300eaf83ad053e51966be1839324341b08eaf880d3dc63ada7942d5912e09c49"}, + {file = "pymongo-4.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a13d8f7141294404ce46dfbabb2f2d17e9b1192456651ae831fa351f86fbeb"}, + {file = "pymongo-4.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:17d13458baf4a6a9f2e787d95adf8ec50d412accb9926a044bd1c41029c323b2"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fe4bcb8acfb288e238190397d4a699aeb4adb70e8545a6f4e44f99d4e8096ab1"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d09d895c7f08bcbed4d2e96a00e52e9e545ae5a37b32d2dc10099b205a21fc6d"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21c0a95a4db72562fd0805e2f76496bf432ba2e27a5651f4b9c670466260c258"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89e45d7fa987f4e246cdf43ff001e3f911f73eb19ba9dabc2a6d80df5c97883b"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1246a82fa6dd73ac2c63aa7e463752d5d1ca91e0c7a23396b78f21273befd3a7"}, + {file = "pymongo-4.15.3-cp311-cp311-win32.whl", hash = "sha256:9483521c03f6017336f54445652ead3145154e8d3ea06418e52cea57fee43292"}, + {file = "pymongo-4.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:c57dad9f289d72af1d7c47a444c4d9fa401f951cedbbcc54c7dd0c2107d6d786"}, + {file = "pymongo-4.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:2fd3b99520f2bb013960ac29dece1b43f2f1b6d94351ca33ba1b1211ecf79a09"}, + {file = "pymongo-4.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd0497c564b0ae34fb816464ffc09986dd9ca29e2772a0f7af989e472fecc2ad"}, + {file = "pymongo-4.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:292fd5a3f045751a823a54cdea75809b2216a62cc5f74a1a96b337db613d46a8"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:959ef69c5e687b6b749fbf2140c7062abdb4804df013ae0507caabf30cba6875"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de3bc878c3be54ae41c2cabc9e9407549ed4fec41f4e279c04e840dddd7c630c"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07bcc36d11252f24fe671e7e64044d39a13d997b0502c6401161f28cc144f584"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b63bac343b79bd209e830aac1f5d9d552ff415f23a924d3e51abbe3041265436"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b33d59bf6fa1ca1d7d96d4fccff51e41312358194190d53ef70a84c070f5287e"}, + {file = "pymongo-4.15.3-cp312-cp312-win32.whl", hash = "sha256:b3a0ec660d61efb91c16a5962ec937011fe3572c4338216831f102e53d294e5c"}, + {file = "pymongo-4.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:f6b0513e5765fdde39f36e6a29a36c67071122b5efa748940ae51075beb5e4bc"}, + {file = "pymongo-4.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4fdd8e6eab8ff77c1c8041792b5f760d48508623cd10b50d5639e73f1eec049"}, + {file = "pymongo-4.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a47a3218f7900f65bf0f36fcd1f2485af4945757360e7e143525db9d715d2010"}, + {file = "pymongo-4.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09440e78dff397b2f34a624f445ac8eb44c9756a2688b85b3bf344d351d198e1"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97f9babdb98c31676f97d468f7fe2dc49b8a66fb6900effddc4904c1450196c8"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71413cd8f091ae25b1fec3af7c2e531cf9bdb88ce4079470e64835f6a664282a"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:76a8d4de8dceb69f6e06736198ff6f7e1149515ef946f192ff2594d2cc98fc53"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:77353978be9fc9e5fe56369682efed0aac5f92a2a1570704d62b62a3c9e1a24f"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9897a837677e3814873d0572f7e5d53c23ce18e274f3b5b87f05fb6eea22615b"}, + {file = "pymongo-4.15.3-cp313-cp313-win32.whl", hash = "sha256:d66da207ccb0d68c5792eaaac984a0d9c6c8ec609c6bcfa11193a35200dc5992"}, + {file = "pymongo-4.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:52f40c4b8c00bc53d4e357fe0de13d031c4cddb5d201e1a027db437e8d2887f8"}, + {file = "pymongo-4.15.3-cp313-cp313-win_arm64.whl", hash = "sha256:fb384623ece34db78d445dd578a52d28b74e8319f4d9535fbaff79d0eae82b3d"}, + {file = "pymongo-4.15.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dcff15b9157c16bc796765d4d3d151df669322acfb0357e4c3ccd056153f0ff4"}, + {file = "pymongo-4.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1f681722c9f27e86c49c2e8a838e61b6ecf2285945fd1798bd01458134257834"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c96dde79bdccd167b930a709875b0cd4321ac32641a490aebfa10bdcd0aa99b"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2d4ca446348d850ac4a5c3dc603485640ae2e7805dbb90765c3ba7d79129b37"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0fd3de3a12ff0a8113a3f64cedb01f87397ab8eaaffa88d7f18ca66cd39385"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e84dec392cf5f72d365e0aac73f627b0a3170193ebb038c3f7e7df11b7983ee7"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d4b01a48369ea6d5bc83fea535f56279f806aa3e4991189f0477696dd736289"}, + {file = "pymongo-4.15.3-cp314-cp314-win32.whl", hash = "sha256:3561fa96c3123275ec5ccf919e595547e100c412ec0894e954aa0da93ecfdb9e"}, + {file = "pymongo-4.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:9df2db6bd91b07400879b6ec89827004c0c2b55fc606bb62db93cafb7677c340"}, + {file = "pymongo-4.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:ff99864085d2c7f4bb672c7167680ceb7d273e9a93c1a8074c986a36dbb71cc6"}, + {file = "pymongo-4.15.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ffe217d2502f3fba4e2b0dc015ce3b34f157b66dfe96835aa64432e909dd0d95"}, + {file = "pymongo-4.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:390c4954c774eda280898e73aea36482bf20cba3ecb958dbb86d6a68b9ecdd68"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7dd2a49f088890ca08930bbf96121443b48e26b02b84ba0a3e1ae2bf2c5a9b48"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f6feb678f26171f2a6b2cbb340949889154c7067972bd4cc129b62161474f08"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446417a34ff6c2411ce3809e17ce9a67269c9f1cb4966b01e49e0c590cc3c6b3"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cfa4a0a0f024a0336640e1201994e780a17bda5e6a7c0b4d23841eb9152e868b"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b03db2fe37c950aff94b29ded5c349b23729bccd90a0a5907bbf807d8c77298"}, + {file = "pymongo-4.15.3-cp314-cp314t-win32.whl", hash = "sha256:e7cde58ef6470c0da922b65e885fb1ffe04deef81e526bd5dea429290fa358ca"}, + {file = "pymongo-4.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fae552767d8e5153ed498f1bca92d905d0d46311d831eefb0f06de38f7695c95"}, + {file = "pymongo-4.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:47ffb068e16ae5e43580d5c4e3b9437f05414ea80c32a1e5cac44a835859c259"}, + {file = "pymongo-4.15.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:58d0f4123855f05c0649f9b8ee083acc5b26e7f4afde137cd7b8dc03e9107ff3"}, + {file = "pymongo-4.15.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9bc9f99e7702fdb0dcc3ff1dd490adc5d20b3941ad41e58f887d4998b9922a14"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:86b1b5b63f4355adffc329733733a9b71fdad88f37a9dc41e163aed2130f9abc"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a054d282dd922ac400b6f47ea3ef58d8b940968d76d855da831dc739b7a04de"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc583a1130e2516440b93bb2ecb55cfdac6d5373615ae472a9d1f26801f58749"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c78237e878e0296130e398151b0d4aa6c9eaf82e38fb6e0aaae2029bc7ef0ce"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c85a4c72b7965033f95c94c42dac27d886c01dbc23fe337ccb14f052a0ccc29"}, + {file = "pymongo-4.15.3-cp39-cp39-win32.whl", hash = "sha256:17fc94d1e067556b122eeb09e25c003268e8c0ea1f2f78e745b33bb59a1209c4"}, + {file = "pymongo-4.15.3-cp39-cp39-win_amd64.whl", hash = "sha256:5bf879a6ed70264574d4d8fb5a467c2a64dc76ecd72c0cb467c4464f849c8c77"}, + {file = "pymongo-4.15.3-cp39-cp39-win_arm64.whl", hash = "sha256:2f3d66f7c495efc3cfffa611b36075efe86da1860a7df75522a6fe499ee10383"}, + {file = "pymongo-4.15.3.tar.gz", hash = "sha256:7a981271347623b5319932796690c2d301668ac3a1965974ac9f5c3b8a22cea5"}, +] + +[package.dependencies] +dnspython = ">=1.16.0,<3.0.0" + +[package.extras] +aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] +docs = ["furo (==2025.7.19)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<9)", "sphinx-autobuild (>=2020.9.1)", "sphinx-rtd-theme (>=2,<4)", "sphinxcontrib-shellcheck (>=1,<2)"] +encryption = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.13.0,<2.0.0)"] +gssapi = ["pykerberos ; os_name != \"nt\"", "winkerberos (>=0.5.0) ; os_name == \"nt\""] +ocsp = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"] +zstd = ["zstandard"] + [[package]] name = "pynacl" version = "1.5.0" @@ -7922,6 +8392,126 @@ wsproto = "*" dev = ["flake8", "pytest", "pytest-cov", "tox"] docs = ["sphinx"] +[[package]] +name = "simplejson" +version = "3.20.2" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5" +groups = ["main"] +files = [ + {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:11847093fd36e3f5a4f595ff0506286c54885f8ad2d921dfb64a85bce67f72c4"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d291911d23b1ab8eb3241204dd54e3ec60ddcd74dfcb576939d3df327205865"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:da6d16d7108d366bbbf1c1f3274662294859c03266e80dd899fc432598115ea4"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9ddf9a07694c5bbb4856271cbc4247cc6cf48f224a7d128a280482a2f78bae3d"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3a0d2337e490e6ab42d65a082e69473717f5cc75c3c3fb530504d3681c4cb40c"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8ba88696351ed26a8648f8378a1431223f02438f8036f006d23b4f5b572778fa"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:00bcd408a4430af99d1f8b2b103bb2f5133bb688596a511fcfa7db865fbb845e"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4fc62feb76f590ccaff6f903f52a01c58ba6423171aa117b96508afda9c210f0"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7"}, + {file = "simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53"}, + {file = "simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413"}, + {file = "simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961"}, + {file = "simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544"}, + {file = "simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54"}, + {file = "simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769"}, + {file = "simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661"}, + {file = "simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608"}, + {file = "simplejson-3.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a135941a50795c934bdc9acc74e172b126e3694fe26de3c0c1bc0b33ea17e6ce"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ba488decb18738f5d6bd082018409689ed8e74bc6c4d33a0b81af6edf1c9f4"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d81f8e982923d5e9841622ff6568be89756428f98a82c16e4158ac32b92a3787"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdad497ccb1edc5020bef209e9c3e062a923e8e6fca5b8a39f0fb34380c8a66c"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a3f1db97bcd9fb592928159af7a405b18df7e847cbcc5682a209c5b2ad5d6b1"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:215b65b0dc2c432ab79c430aa4f1e595f37b07a83c1e4c4928d7e22e6b49a748"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:ece4863171ba53f086a3bfd87f02ec3d6abc586f413babfc6cf4de4d84894620"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4a76d7c47d959afe6c41c88005f3041f583a4b9a1783cf341887a3628a77baa0"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:e9b0523582a57d9ea74f83ecefdffe18b2b0a907df1a9cef06955883341930d8"}, + {file = "simplejson-3.20.2-cp36-cp36m-win32.whl", hash = "sha256:16366591c8e08a4ac76b81d76a3fc97bf2bcc234c9c097b48d32ea6bfe2be2fe"}, + {file = "simplejson-3.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:732cf4c4ac1a258b4e9334e1e40a38303689f432497d3caeb491428b7547e782"}, + {file = "simplejson-3.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6c3a98e21e5f098e4f982ef302ebb1e681ff16a5d530cfce36296bea58fe2396"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cf9ca1363dc3711c72f4ec7c1caed2bbd9aaa29a8d9122e31106022dc175c6"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:106762f8aedf3fc3364649bfe8dc9a40bf5104f872a4d2d86bae001b1af30d30"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b21659898b7496322e99674739193f81052e588afa8b31b6a1c7733d8829b925"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fa1db6a02bca88829f2b2057c76a1d2dc2fccb8c5ff1199e352f213e9ec719"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:156139d94b660448ec8a4ea89f77ec476597f752c2ff66432d3656704c66b40e"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:b2620ac40be04dff08854baf6f4df10272f67079f61ed1b6274c0e840f2e2ae1"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:9ccef5b5d3e3ac5d9da0a0ca1d2de8cf2b0fb56b06aa0ab79325fa4bcc5a1d60"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f526304c2cc9fd8b8d18afacb75bc171650f83a7097b2c92ad6a431b5d7c1b72"}, + {file = "simplejson-3.20.2-cp37-cp37m-win32.whl", hash = "sha256:e0f661105398121dd48d9987a2a8f7825b8297b3b2a7fe5b0d247370396119d5"}, + {file = "simplejson-3.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dab98625b3d6821e77ea59c4d0e71059f8063825a0885b50ed410e5c8bd5cb66"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b8205f113082e7d8f667d6cd37d019a7ee5ef30b48463f9de48e1853726c6127"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc8da64929ef0ff16448b602394a76fd9968a39afff0692e5ab53669df1f047f"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfe704864b5fead4f21c8d448a89ee101c9b0fc92a5f40b674111da9272b3a90"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ca7cbe7d2f423b97ed4e70989ef357f027a7e487606628c11b79667639dc84"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cec1868b237fe9fb2d466d6ce0c7b772e005aadeeda582d867f6f1ec9710cad"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:792debfba68d8dd61085ffb332d72b9f5b38269cda0c99f92c7a054382f55246"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e022b2c4c54cb4855e555f64aa3377e3e5ca912c372fa9e3edcc90ebbad93dce"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5de26f11d5aca575d3825dddc65f69fdcba18f6ca2b4db5cef16f41f969cef15"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e2162b2a43614727ec3df75baeda8881ab129824aa1b49410d4b6c64f55a45b4"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e11a1d6b2f7e72ca546bdb4e6374b237ebae9220e764051b867111df83acbd13"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:daf7cd18fe99eb427fa6ddb6b437cfde65125a96dc27b93a8969b6fe90a1dbea"}, + {file = "simplejson-3.20.2-cp38-cp38-win32.whl", hash = "sha256:da795ea5f440052f4f497b496010e2c4e05940d449ea7b5c417794ec1be55d01"}, + {file = "simplejson-3.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:6a4b5e7864f952fcce4244a70166797d7b8fd6069b4286d3e8403c14b88656b6"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bf76512ccb07d47944ebdca44c65b781612d38b9098566b4bb40f713fc4047"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:214e26acf2dfb9ff3314e65c4e168a6b125bced0e2d99a65ea7b0f169db1e562"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fb1259ca9c385b0395bad59cdbf79535a5a84fb1988f339a49bfbc57455a35a"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34e028a2ba8553a208ded1da5fa8501833875078c4c00a50dffc33622057881"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b538f9d9e503b0dd43af60496780cb50755e4d8e5b34e5647b887675c1ae9fee"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab998e416ded6c58f549a22b6a8847e75a9e1ef98eb9fbb2863e1f9e61a4105b"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8f1c307edf5fbf0c6db3396c5d3471409c4a40c7a2a466fbc762f20d46601a"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a7bbac80bdb82a44303f5630baee140aee208e5a4618e8b9fde3fc400a42671"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5ef70ec8fe1569872e5a3e4720c1e1dcb823879a3c78bc02589eb88fab920b1f"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11c09c99253a74c36925d461c86ea25f0140f3b98ff678322734ddc0f038d7"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:66f7c78c6ef776f8bd9afaad455e88b8197a51e95617bcc44b50dd974a7825ba"}, + {file = "simplejson-3.20.2-cp39-cp39-win32.whl", hash = "sha256:619ada86bfe3a5aa02b8222ca6bfc5aa3e1075c1fb5b3263d24ba579382df472"}, + {file = "simplejson-3.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:44a6235e09ca5cc41aa5870a952489c06aa4aee3361ae46daa947d8398e57502"}, + {file = "simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017"}, + {file = "simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649"}, +] + [[package]] name = "six" version = "1.17.0" @@ -8205,6 +8795,18 @@ files = [ genshimagic = ["Genshi"] kidmagic = ["kid"] +[[package]] +name = "stevedore" +version = "5.5.0" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf"}, + {file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"}, +] + [[package]] name = "strenum" version = "0.4.15" @@ -8703,6 +9305,21 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "web-fragments" +version = "3.1.0" +description = "Web fragments" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "web_fragments-3.1.0-py2.py3-none-any.whl", hash = "sha256:3cd6a4d5d768b95fb81cd0d8f1c7eb15ad960ff19ab63b897afccc99805b0095"}, + {file = "web_fragments-3.1.0.tar.gz", hash = "sha256:b0ba8bac5cce6c5e40bba53c429c00324fe39d49fc2a7b0a258161497bb66df6"}, +] + +[package.extras] +django = ["Django (>=4.2,<4.3)"] + [[package]] name = "webencodings" version = "0.5.1" @@ -8715,6 +9332,22 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +[[package]] +name = "webob" +version = "1.8.9" +description = "WSGI request and response object" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "WebOb-1.8.9-py2.py3-none-any.whl", hash = "sha256:45e34c58ed0c7e2ecd238ffd34432487ff13d9ad459ddfd77895e67abba7c1f9"}, + {file = "webob-1.8.9.tar.gz", hash = "sha256:ad6078e2edb6766d1334ec3dee072ac6a7f95b1e32ce10def8ff7f0f02d56589"}, +] + +[package.extras] +docs = ["Sphinx (>=1.7.5)", "pylons-sphinx-themes"] +testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"] + [[package]] name = "websocket-client" version = "1.8.0" @@ -8884,6 +9517,33 @@ files = [ [package.dependencies] h11 = ">=0.9.0,<1" +[[package]] +name = "xblock" +version = "5.2.0" +description = "XBlock Core Library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "XBlock-5.2.0-py3-none-any.whl", hash = "sha256:399edefb64c953a4b7e4775d8d0c028c8de2baa6ba9e9f9e577b57994a415c60"}, + {file = "XBlock-5.2.0.tar.gz", hash = "sha256:d3d1deb758a8a0ff84c64a9e10d0413821221c572ee4d5ee431e6d327d977a08"}, +] + +[package.dependencies] +fs = "*" +lxml = "*" +mako = "*" +markupsafe = "*" +python-dateutil = "*" +pytz = "*" +pyyaml = "*" +simplejson = "*" +web-fragments = "*" +webob = "*" + +[package.extras] +django = ["lazy", "openedx-django-pyfs (>=1.0.5)"] + [[package]] name = "xmltodict" version = "0.14.2" @@ -9241,4 +9901,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "~3.12" -content-hash = "1359d8865bf8d3a2dcfa719d1fa6b18a0d6497d76860a519e30b821c7d26109f" +content-hash = "c0d3a67b643203a39d03a7bc86d7c600b36347dc3c4e1ee4d123b6d819825b94" diff --git a/pyproject.toml b/pyproject.toml index c98764e95c..f11933c06a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,6 +114,7 @@ wrapt = "^1.14.1" youtube-transcript-api = "^1.0.0" pypdfium2 = "^4.30.0" pyarrow = "^21.0.0" +lti-consumer-xblock = "9.14.2" diff --git a/templates/lti_launch_request_form.html b/templates/lti_launch_request_form.html new file mode 100644 index 0000000000..96ba3c2e2c --- /dev/null +++ b/templates/lti_launch_request_form.html @@ -0,0 +1,30 @@ + + + + + LTI + + + + + + From d874ef6a9883595787c2f124d6f7ccfbe48cddae Mon Sep 17 00:00:00 2001 From: dansubak Date: Fri, 17 Oct 2025 17:12:50 -0400 Subject: [PATCH 2/5] Remove dead returns --- authentication/views.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/authentication/views.py b/authentication/views.py index 001bf7498d..5d2f5bab37 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -5,7 +5,6 @@ from urllib.parse import urljoin from django.contrib.auth import logout -from django.http import HttpResponse from django.shortcuts import redirect, render from django.utils.http import url_has_allowed_host_and_scheme, urlencode from django.utils.text import slugify @@ -167,9 +166,6 @@ def lti_login( """ return lti_preflight_request(request) - return HttpResponse( - f"LTI_Login GET endpoint - successfully imported lti_consumer and logged in as user {request.user}. Is Superuser {request.user.is_superuser}" - ) @csrf_exempt @@ -182,9 +178,6 @@ def lti_auth( Perform LTI negotiation and redirect to notebook URL """ return lti_launch_endpoint(request) - return HttpResponse( - f"LTI_Login POST endpoint - successfully imported lti_consumer and logged in as user {request.user}. Is Superuser {request.user.is_superuser}" - ) def _get_lti1p3_consumer(): From 6e6a95722170f9e4ba020565f440775fef370877 Mon Sep 17 00:00:00 2001 From: dansubak Date: Wed, 22 Oct 2025 11:35:34 -0400 Subject: [PATCH 3/5] Working local jupyterhub integration, still needs dropped sessions fixed --- authentication/urls.py | 9 ++++++++- authentication/views.py | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/authentication/urls.py b/authentication/urls.py index e22a09c57a..ea4749c4e3 100644 --- a/authentication/urls.py +++ b/authentication/urls.py @@ -2,11 +2,18 @@ from django.urls import re_path -from authentication.views import CustomLoginView, CustomLogoutView, lti_auth, lti_login +from authentication.views import ( + CustomLoginView, + CustomLogoutView, + lti_auth, + lti_login, + public_keyset, +) urlpatterns = [ re_path(r"^logout", CustomLogoutView.as_view(), name="logout"), re_path(r"^login", CustomLoginView.as_view(), name="login"), re_path(r"^lti_login", lti_login, name="lti_login"), re_path(r"^lti_auth", lti_auth, name="lti_auth"), + re_path(r"^lti_jwks", public_keyset, name="lti_jwks"), ] diff --git a/authentication/views.py b/authentication/views.py index 5d2f5bab37..68af944713 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,10 +1,11 @@ -# ruff: noqa: ARG001, F821, E501, TD002, TD003, FIX002, ERA001, SIM115, PTH123, T201, D401 +# ruff: noqa: ARG001, E501, TD002, TD003, FIX002, ERA001, SIM115, PTH123, D401 """Authentication views""" import logging from urllib.parse import urljoin from django.contrib.auth import logout +from django.http import JsonResponse from django.shortcuts import redirect, render from django.utils.http import url_has_allowed_host_and_scheme, urlencode from django.utils.text import slugify @@ -130,15 +131,20 @@ def get( # client_id = 'learn-jupyter-notebooks' # deployment_id = '0cb94445-2066-4295-9c03-4bfdc1d8aacb' -lti_oidc_url = "https://saltire.lti.app/tool" -lti_launch_url = "https://saltire.lti.app/tool" -redirect_uris = [lti_launch_url] +# lti_oidc_url = "http://127.0.0.1:8000" +# lti_launch_url = "http://127.0.0.1:8000" + +lti_oidc_url = "http://127.0.0.1:8000/hub/lti13/oauth_login" +# The launch URL includes the next param, but all other bits of state are passed via launch request claims +lti_launch_url = ( + "http://127.0.0.1:8000/hub/lti13/oauth_callback?next=/hub/spawn/test-user-id" +) +redirect_uris = ["http://127.0.0.1:8000/hub/lti13/oauth_callback", lti_launch_url] client_id = "learn-jupyter-notebooks" deployment_id = "cb5fd9ae5c92eddbc5054e27fa010c5478d2f7c3" platform_private_key_id = "JZOHScC4BQ" platform_public_key = open("platform_id_rsa.pub").read() platform_private_key = open("platform_id_rsa").read() -print(platform_public_key) rsa_key = platform_private_key rsa_key_id = platform_private_key_id iss = "http://api.open.odl.local" @@ -209,6 +215,7 @@ def public_keyset(request): This endpoint must be configured in the tool. """ + return JsonResponse( _get_lti1p3_consumer().get_public_keyset(), content_type="application/json" ) @@ -232,10 +239,14 @@ def lti_preflight_request(request): config_id=client_id, resource_link_id="link_id", ) + # This template should render a simple redirection to the URL # provided by the context through the `oidc_url` key above. # This can also be a redirect. - return redirect(lti_consumer.prepare_preflight_url(launch_data)) + url = lti_consumer.prepare_preflight_url(launch_data) + urlencode( + {"next": "http://127.0.0.1:8000/user/test-user-id/"} + ) + return redirect(url) def lti_launch_endpoint(request): @@ -258,11 +269,20 @@ def lti_launch_endpoint(request): # TODO: Not sure what this is used for but it's required. Look it up later lti_consumer.set_resource_link_claim("link_id") + # We can add any custom parameters we want to the launch request here. + lti_consumer.set_custom_parameters( + { + "image": "quay.io/jupyter/minimal-notebook", + "course_name": "Cool Test Course Name", + } + ) + + payload = request.POST if request.method == "POST" else request.GET context.update( { - "preflight_response": dict(request.POST), + "preflight_response": dict(payload), "launch_request": lti_consumer.generate_launch_request( - preflight_response=request.POST + preflight_response=payload ), } ) From 5c8ab8165ca61919140be1981b40ca6f22493d50 Mon Sep 17 00:00:00 2001 From: dansubak Date: Thu, 23 Oct 2025 10:16:45 -0400 Subject: [PATCH 4/5] Add wip customizable form-based submission --- authentication/views.py | 46 +++++++++++++++++++++++++------ templates/lti_preflight_form.html | 22 +++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 templates/lti_preflight_form.html diff --git a/authentication/views.py b/authentication/views.py index 68af944713..1a704e609b 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,10 +1,12 @@ -# ruff: noqa: ARG001, E501, TD002, TD003, FIX002, ERA001, SIM115, PTH123, D401 +# ruff: noqa: ARG001, E501, TD002, TD003, FIX002, ERA001, SIM115, PTH123, D401, T201, RET503 """Authentication views""" import logging from urllib.parse import urljoin from django.contrib.auth import logout +from django.core.cache import caches +from django.core.exceptions import PermissionDenied from django.http import JsonResponse from django.shortcuts import redirect, render from django.utils.http import url_has_allowed_host_and_scheme, urlencode @@ -170,8 +172,25 @@ def lti_login( """ Render form taking relevant bits of info to start LTI negotiation """ - - return lti_preflight_request(request) + if request.method == "GET": + # Render form for LTI preflight with image and course_name fields + return render(request, "lti_preflight_form.html") + elif request.method == "POST": + if request.user.is_authenticated: + cache = caches["redis"] + cache.set( + f"{request.user.global_id}:lti_image", + request.POST.get("image"), + timeout=30, + ) + cache.set( + f"{request.user.global_id}:lti_course_name", + request.POST.get("course_name"), + timeout=30, + ) + else: + raise PermissionDenied + return lti_preflight_request(request) @csrf_exempt @@ -270,12 +289,21 @@ def lti_launch_endpoint(request): lti_consumer.set_resource_link_claim("link_id") # We can add any custom parameters we want to the launch request here. - lti_consumer.set_custom_parameters( - { - "image": "quay.io/jupyter/minimal-notebook", - "course_name": "Cool Test Course Name", - } - ) + if request.user.is_authenticated: + cache = caches["redis"] + image = cache.get(f"{request.user.global_id}:lti_image") + course = cache.get(f"{request.user.global_id}:lti_course_name") + if image and course: + lti_consumer.set_custom_parameters( + { + "image": image, + "course_name": course, + } + ) + else: + print("No cached information found for current user") + else: + print("No authenticated user found") payload = request.POST if request.method == "POST" else request.GET context.update( diff --git a/templates/lti_preflight_form.html b/templates/lti_preflight_form.html new file mode 100644 index 0000000000..282b8235e8 --- /dev/null +++ b/templates/lti_preflight_form.html @@ -0,0 +1,22 @@ + + + + LTI Preflight Form + + +

Start LTI Negotiation

+
+ {% csrf_token %} + +

+ +

+ +
+ + From e5a46788b4bd38b0f76696c51a5a2bd1cf1efa1a Mon Sep 17 00:00:00 2001 From: dansubak Date: Fri, 24 Oct 2025 09:31:02 -0400 Subject: [PATCH 5/5] Minor tweaks to LTI endpoints --- authentication/views.py | 51 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/authentication/views.py b/authentication/views.py index 1a704e609b..a9276ed596 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -176,7 +176,7 @@ def lti_login( # Render form for LTI preflight with image and course_name fields return render(request, "lti_preflight_form.html") elif request.method == "POST": - if request.user.is_authenticated: + if request.user and request.user.is_authenticated: cache = caches["redis"] cache.set( f"{request.user.global_id}:lti_image", @@ -247,14 +247,15 @@ def lti_preflight_request(request): The platform needs to know the tool OIDC endpoint. """ + # TODO: This doesn't work w/ anonymous users as they don't have a global_id. Shouldn't happen in practice, so just bail + user = request.user + if user and not user.is_authenticated: + raise PermissionDenied lti_consumer = _get_lti1p3_consumer() # AFAICT, config_id and resource_link_id are pretty much used to get stuff out of cache later launch_data = Lti1p3LaunchData( - # TODO: This doesn't work w/ anonymous users as they don't have a global_id. Shouldn't happen in practice, so just bail - # user_id=request.user.global_id, - # user_role="admin" if request.user.is_superuser else "student", - user_id="test-user-id", - user_role="staff", + user_id=user.global_id, + user_role="admin" if user.is_superuser else "student", config_id=client_id, resource_link_id="link_id", ) @@ -274,36 +275,34 @@ def lti_launch_endpoint(request): """ lti_consumer = _get_lti1p3_consumer() context = {} + user = request.user + if user and not user.is_authenticated: + raise PermissionDenied # Required user claim data - # TODO: For some reason (CORS?) we're dropping our session after the redirect from the OIDC initiation endpoint. # This will need to be solved. lti_consumer.set_user_data( - # user_id=request.user.global_id, + user_id=user.global_id, # Pass django user role to library - # role="admin" if request.user.is_superuser else "student", - user_id="test-user-id", - role="staff", + role="admin" if user.is_superuser else "student", ) - # TODO: Not sure what this is used for but it's required. Look it up later + # TODO: Not sure what this is used for but it's required. + # It doesn't seem to affect the basic test, but we should adjust it later lti_consumer.set_resource_link_claim("link_id") # We can add any custom parameters we want to the launch request here. - if request.user.is_authenticated: - cache = caches["redis"] - image = cache.get(f"{request.user.global_id}:lti_image") - course = cache.get(f"{request.user.global_id}:lti_course_name") - if image and course: - lti_consumer.set_custom_parameters( - { - "image": image, - "course_name": course, - } - ) - else: - print("No cached information found for current user") + cache = caches["redis"] + image = cache.get(f"{user.global_id}:lti_image") + course = cache.get(f"{request.user.global_id}:lti_course_name") + if image and course: + lti_consumer.set_custom_parameters( + { + "image": image, + "course_name": course, + } + ) else: - print("No authenticated user found") + print("No cached information found for current user") payload = request.POST if request.method == "POST" else request.GET context.update(