From ae0098899ac47126200026f02e75abe81283c378 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 14:55:34 -0600 Subject: [PATCH 01/20] add tapipy; Tapis v3 OAuth token and flow --- .pylintrc | 3 +- README.md | 7 +- conf/env_files/designsafe.sample.env | 30 +- designsafe/LoginTest.py | 117 +-- .../designsafe/apps/accounts/register.html | 4 +- designsafe/apps/accounts/views.py | 4 +- designsafe/apps/api/users/utils.py | 29 +- designsafe/apps/api/users/views.py | 25 +- designsafe/apps/auth/README.md | 16 +- designsafe/apps/auth/backends.py | 217 +++--- designsafe/apps/auth/context_processors.py | 14 - designsafe/apps/auth/middleware.py | 87 ++- designsafe/apps/auth/models.py | 163 ++-- designsafe/apps/auth/models_unit_test.py | 46 ++ .../templates/designsafe/apps/auth/login.html | 4 +- designsafe/apps/auth/unit_test.py | 98 +++ designsafe/apps/auth/urls.py | 65 +- designsafe/apps/auth/views.py | 299 ++++---- designsafe/apps/auth/views_unit_test.py | 95 +++ designsafe/conftest.py | 50 +- designsafe/settings/common_settings.py | 11 +- designsafe/settings/test_settings.py | 4 +- designsafe/templates/includes/header.html | 10 +- designsafe/urls.py | 2 - poetry.lock | 700 ++++++++---------- pyproject.toml | 2 +- 26 files changed, 1116 insertions(+), 986 deletions(-) delete mode 100644 designsafe/apps/auth/context_processors.py create mode 100644 designsafe/apps/auth/models_unit_test.py create mode 100644 designsafe/apps/auth/unit_test.py create mode 100644 designsafe/apps/auth/views_unit_test.py diff --git a/.pylintrc b/.pylintrc index 8f56da028e..cbf324a0ef 100644 --- a/.pylintrc +++ b/.pylintrc @@ -430,7 +430,8 @@ disable=raw-checker-failed, useless-suppression, deprecated-pragma, use-symbolic-message-instead, - duplicate-code + duplicate-code, + logging-fstring-interpolation # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/README.md b/README.md index 392638ee7a..fb5a9a734c 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,8 @@ $ docker-compose -f conf/docker/docker-compose-dev.all.debug.yml up $ npm run dev ``` -When using this compose file, your Agave Client should be configured with a `callback_url` -of `http://$DOCKER_HOST_IP:8000/auth/agave/callback/`. +When using this compose file, your Tapis Client should be configured with a `callback_url` +of `http://$DOCKER_HOST_IP:8000/auth/tapis/callback/`. For developing some services, e.g. Box.com integration, https support is required. To enable an Nginx http proxy run using the [`docker-compose-http.yml`](docker-compose-http.yml) @@ -238,9 +238,6 @@ $ docker-compose -f docker-compose-http.yml build $ docker-compose -f docker-compose-http.yml up ``` -When using this compose file, your Agave Client should be configured with a `callback_url` -of `https://$DOCKER_HOST_IP/auth/agave/callback/`. - ### Agave filesystem setup 1. Delete all of the old metadata objects using this command: diff --git a/conf/env_files/designsafe.sample.env b/conf/env_files/designsafe.sample.env index 8e02f3b6f2..7b08b33f24 100644 --- a/conf/env_files/designsafe.sample.env +++ b/conf/env_files/designsafe.sample.env @@ -57,13 +57,13 @@ OPBEAT_SECRET_TOKEN= # # To configure Agave Authentication for DesignSafe-CI Portal, you need to # generate a set of API client keys. The client MUST be configured with a `callbackUrl` -# which should be the URL path to the `designsafe.apps.auth.views.agave_oauth_callback` -# view. In production, this would be `https://www.designsafe-ci.org/auth/agave/callback/`. +# which should be the URL path to the `designsafe.apps.auth.views.tapis_oauth_callback` +# view. In production, this would be `https://www.designsafe-ci.org/auth/tapis/callback/`. # If you are following the "First time setup" from the README, this would be -# `http://$DOCKER_HOST_IP:8000/auth/agave/callback/`, where `$DOCKER_HOST_IP` is either +# `http://$DOCKER_HOST_IP:8000/auth/tapis/callback/`, where `$DOCKER_HOST_IP` is either # `localhost` or the result of the `docker-machine ip machine-name` command. # -# See https://agave.readthedocs.io/en/latest/agave/guides/clients/introduction.html +# See https://tapis.readthedocs.io/en/latest/technical/authentication.html#creating-clients # for more information # AGAVE_TENANT_ID=designsafe @@ -81,6 +81,28 @@ AGAVE_JWT_HEADER= AGAVE_JWT_USER_CLAIM_FIELD= AGAVE_JWT_SERVICE_ACCOUNT= +######################## +# TAPIS v3 SETTINGS +# NOTE: ONLY USED FOR TAPIS V3 DEVELOPMENT. +# YOU CAN IGNORE THIS FOR TAPIS V2 DEVELOPMENT. +######################## + +# Admin account +PORTAL_ADMIN_USERNAME= + +# Tapis Tenant. +TAPIS_TENANT_BASEURL= + +# Tapis Client Configuration +TAPIS_CLIENT_ID= +TAPIS_CLIENT_KEY= + +# Long-live portal admin access token +TAPIS_ADMIN_JWT= + +# Key service token for registering public keys with cloud.corral +KEY_SERVICE_TOKEN= + ### # Box.com Integration # diff --git a/designsafe/LoginTest.py b/designsafe/LoginTest.py index 05f648a313..49f76b52af 100644 --- a/designsafe/LoginTest.py +++ b/designsafe/LoginTest.py @@ -1,98 +1,27 @@ -#python manage.py test designsafe.LoginTest --settings=designsafe.settings.test_settings -from django.test import TestCase, RequestFactory -from django.contrib.auth import get_user_model -from django.urls import reverse +# python manage.py test designsafe.LoginTest --settings=designsafe.settings.test_settings import mock -from designsafe.apps.auth.models import AgaveOAuthToken -from designsafe.apps.auth.tasks import check_or_create_agave_home_dir -from designsafe.apps.auth.views import agave_oauth_callback +from django.test import TestCase +from designsafe.apps.auth.views import tapis_oauth_callback class LoginTestClass(TestCase): - def setUp (self): - User = get_user_model() - - user_with_agave = User.objects.create_user('test_with_agave', 'test@test.com', 'test') - token = AgaveOAuthToken( - token_type="bearer", - scope="default", - access_token="1234abcd", - refresh_token="123123123", - expires_in=14400, - created=1523633447) - token.user = user_with_agave - token.save() - self.rf = RequestFactory() - - user_without_agave = User.objects.create_user('test_without_agave', 'test2@test.com', 'test') - token = AgaveOAuthToken( - token_type="bearer", - scope="default", - access_token="5555abcd", - refresh_token="5555555", - expires_in=14400, - created=1523633447) - token.user = user_without_agave - token.save() - - - def tearDown(self): - return - - """ @mock.patch('designsafe.apps.auth.models.AgaveOAuthToken.client') - @mock.patch('agavepy.agave.Agave') - def test_has_agave_file_listing(self, agave_client, agave): - #agaveclient.return_value.files.list.return_value = [] // whatever the listing should look like - #request.post.return_value = {} // some object that looks like a requests response - - self.client.login(username='test_with_agave', password='test') - - agave_client.files.list.return_value = { - "name": "test", - "system": "designsafe.storage.default", - "trail": [{"path": "/", "name": "/", "system": "designsafe.storage.default"}, - {"path": "/test", "name": "test", "system": "designsafe.storage.default"}], - "path": "test", - "type": "dir", - "children": [], - "permissions": "READ" - } - - resp = self.client.get('/api/agave/files/listing/agave/designsafe.storage.default/test', follow=True) - - self.assertEqual(resp.status_code, 200) - self.assertJSONEqual(resp.content, agave_client.files.list.return_value, msg='Agave homedir listing has unexpected values') """ - - @mock.patch('designsafe.apps.auth.models.AgaveOAuthToken.client') - @mock.patch('agavepy.agave.Agave') - def test_no_agave_file_listing(self, agave_client, agave): - self.client.login(username='test_without_agave', password='test') - session = self.client.session - session['auth_state'] = 'test' - session.save() - - request_without_agave = self.client.post("/auth/agave/callback/?state=test&code=test", follow = True) - print('Initial Query Status: ' + str(request_without_agave.status_code)) - """ request_without_agave.get.return_value = { - 'state': 'test', - 'session': {'auth_state': 'test'}, - 'code': 'test' - } """ - - """ agave_client.files.list.return_value = { - "status": "error", - "message": "File/folder does not exist", - "version": "test" - } """ - - resp = agave_oauth_callback(request_without_agave) - print('Oauth Callback Status: ' + str(resp)) - self.assertEqual(resp.status_code, 200) - - """ def test_user_without_agave_homedir_gets_redirected(self, mock_client, mock_Agave): - - pass """ - - """ def test_agave_callbak(self): - - resp = self.client.post("/auth/agave/callback?code=code&state=test", data=data) """ + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch("designsafe.apps.auth.models.TapisOAuthToken.client") + @mock.patch("tapipy.tapis.Tapis") + def test_no_tapis_file_listing(self, tapis_client, tapis): + self.client.login(username="test_without_tapis", password="test") + session = self.client.session + session["auth_state"] = "test" + session.save() + + request_without_tapis = self.client.post( + "/auth/tapis/callback/?state=test&code=test", follow=True + ) + resp = tapis_oauth_callback(request_without_tapis) + print("Oauth Callback Status: " + str(resp)) + self.assertEqual(resp.status_code, 200) diff --git a/designsafe/apps/accounts/templates/designsafe/apps/accounts/register.html b/designsafe/apps/accounts/templates/designsafe/apps/accounts/register.html index 9a73fb665a..f00c56c014 100644 --- a/designsafe/apps/accounts/templates/designsafe/apps/accounts/register.html +++ b/designsafe/apps/accounts/templates/designsafe/apps/accounts/register.html @@ -19,7 +19,7 @@ <h1 class="headline headline-research" style="margin-bottom:40px;">Register an A If you already have a TACC account, log in with your TACC username and password to access DesignSafe. </p> <p class="text-center"> - <a href="{% url 'designsafe_auth:agave_oauth' %}"><button class="btn btn-default" + <a href="{% url 'designsafe_auth:tapis_oauth' %}"><button class="btn btn-default" style="width:100%; font-weight: bold;">Log in</button></a> </p> @@ -61,4 +61,4 @@ <h1 class="headline headline-research" style="margin-bottom:40px;">Register an A </div> {% endblock %} -{% block footer %}{% include 'includes/footer.html' %}{% endblock footer %} \ No newline at end of file +{% block footer %}{% include 'includes/footer.html' %}{% endblock footer %} diff --git a/designsafe/apps/accounts/views.py b/designsafe/apps/accounts/views.py index 7a6671deb6..a624448010 100644 --- a/designsafe/apps/accounts/views.py +++ b/designsafe/apps/accounts/views.py @@ -278,7 +278,7 @@ def register(request): if not captcha_json.get("success", False): messages.error(request, "Please complete the reCAPTCHA before submitting your account request.") return render(request,'designsafe/apps/accounts/register.html', context) - + # Once captcha is verified, send request to TRAM. tram_headers = {"tram-services-key": settings.TRAM_SERVICES_KEY} tram_body = {"project_id": settings.TRAM_PROJECT_ID, @@ -290,7 +290,7 @@ def register(request): tram_resp.raise_for_status() logger.info("Received response from TRAM: %s", tram_resp.json()) messages.success(request, "Your request has been received. Please check your email for a project invitation.") - + except requests.HTTPError as exc: logger.debug(exc) messages.error(request, "An unknown error occurred. Please try again later.") diff --git a/designsafe/apps/api/users/utils.py b/designsafe/apps/api/users/utils.py index 0923178d1c..0eb576a8e3 100644 --- a/designsafe/apps/api/users/utils.py +++ b/designsafe/apps/api/users/utils.py @@ -1,10 +1,34 @@ +import logging +from pytas.http import TASClient from django.db.models import Q +from django.conf import settings -import logging -import json logger = logging.getLogger(__name__) + +def get_tas_client(): + """Return a TAS Client with pytas""" + return TASClient( + baseURL=settings.TAS_URL, + credentials={ + 'username': settings.TAS_CLIENT_KEY, + 'password': settings.TAS_CLIENT_SECRET + } + ) + + +def get_user_data(username): + """Returns user contact information + + : returns: user_data + : rtype: dict + """ + tas_client = get_tas_client() + user_data = tas_client.get_user(username=username) + return user_data + + def list_to_model_queries(q_comps): query = None if len(q_comps) > 2: @@ -17,6 +41,7 @@ def list_to_model_queries(q_comps): query |= Q(last_name__icontains = q_comps[1]) return query + def q_to_model_queries(q): if not q: return None diff --git a/designsafe/apps/api/users/views.py b/designsafe/apps/api/users/views.py index 792f66181b..0ffa7a833c 100644 --- a/designsafe/apps/api/users/views.py +++ b/designsafe/apps/api/users/views.py @@ -17,9 +17,9 @@ def check_public_availability(username): es_client = new_es_client() - query = Q({'multi_match': {'fields': ['project.value.teamMembers', - 'project.value.coPis', - 'project.value.pi'], + query = Q({'multi_match': {'fields': ['project.value.teamMembers', + 'project.value.coPis', + 'project.value.pi'], 'query': username}}) res = IndexedPublication.search(using=es_client).filter(query).execute() return res.hits.total.value > 0 @@ -50,14 +50,13 @@ def get(self, request): "last_name": u.last_name, "email": u.email, "oauth": { - "access_token": u.agave_oauth.access_token, - "expires_in": u.agave_oauth.expires_in, - "scope": u.agave_oauth.scope, - } + "expires_in": u.tapis_oauth.expires_in, + }, + "isStaff": u.is_staff, } return JsonResponse(out) - return HttpResponse('Unauthorized', status=401) + return JsonResponse({'message': 'Unauthorized'}, status=401) class SearchView(View): @@ -120,7 +119,7 @@ def get(self, request): return JsonResponse(resp, safe=False) else: return HttpResponseNotFound() - + class ProjectUserView(BaseApiView): """View for handling search for project users""" @@ -128,17 +127,17 @@ def get(self, request: HttpRequest): """retrieve a user by their exact TACC username.""" if not request.user.is_authenticated: raise ApiException(message="Authentication required", status=401) - + username_query = request.GET.get("q") user_match = get_user_model().objects.filter(username__iexact=username_query) user_resp = [{"fname": u.first_name, "lname": u.last_name, "inst": u.profile.institution, - "email": u.email, + "email": u.email, "username": u.username} for u in user_match] - + return JsonResponse({"result": user_resp}) - + class PublicView(View): diff --git a/designsafe/apps/auth/README.md b/designsafe/apps/auth/README.md index ed0e415cbb..de247e4a59 100644 --- a/designsafe/apps/auth/README.md +++ b/designsafe/apps/auth/README.md @@ -9,27 +9,27 @@ support the various authentication requirements of DesignSafe. Authenticate directly against TACC's TAS Identity Store. This backend is used when authenticating directly to the Django Admin app. An OAuth token will not be obtained when -using this backend, so using Agave/DesignSafe API features will not work. +using this backend, so using Tapis/DesignSafe API features will not work. -### AgaveOAuthBackend +### TapisOAuthBackend -Authenticate using Agave OAuth Webflow (authorization code). See the [Agave Authentication Docs][1] +Authenticate using Tapis OAuth Webflow (authorization code). See the [Tapis Authentication Docs][1] for complete documentation. -#### AgaveTokenRefreshMiddleware +#### TapisTokenRefreshMiddleware -OAuth tokens obtained from Agave are valid for a limited time, usually one hour (3600s). +OAuth tokens obtained from Tapis are valid for a limited time, usually ten days (14400s). The app can automatically refresh the OAuth token as necessary. Add the refresh middleware in `settings.py`. The middleware *must* appear after `django.contrib.sessions.middleware.SessionMiddleware`: ``` -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( ..., 'django.contrib.sessions.middleware.SessionMiddleware', - designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware, + designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware, ..., ) ``` -[1]: http://agaveapi.co/documentation/authorization-guide/#authorization_code_flow \ No newline at end of file +[1]: https://tapis.readthedocs.io/en/latest/technical/authentication.html#authorization-code-grant-generating-tokens-for-users diff --git a/designsafe/apps/auth/backends.py b/designsafe/apps/auth/backends.py index f822bf649a..8e933c8966 100644 --- a/designsafe/apps/auth/backends.py +++ b/designsafe/apps/auth/backends.py @@ -1,51 +1,57 @@ +"""Auth backends""" + +import logging +import re from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend +from tapipy.tapis import Tapis +from tapipy.errors import BaseTapyException +from designsafe.apps.accounts.models import DesignSafeProfile, NotificationPreferences +from designsafe.apps.users.utils import get_user_data +from desingsafe.apps.auth.models.TapisOAuthToken import get_masked_token from django.contrib.auth.signals import user_logged_out from django.contrib import messages -from django.contrib.auth.backends import ModelBackend from django.core.exceptions import ValidationError from django.dispatch import receiver -from designsafe.apps.accounts.models import DesignSafeProfile, NotificationPreferences -from designsafe.apps.api.agave import get_service_account_client from designsafe.apps.auth.tasks import update_institution_from_tas from pytas.http import TASClient -import logging -import re -import requests -from requests.auth import HTTPBasicAuth + +logger = logging.getLogger(__name__) @receiver(user_logged_out) def on_user_logged_out(sender, request, user, **kwargs): - backend = request.session.get('_auth_user_backend', None) - tas_backend_name = '%s.%s' % (TASBackend.__module__, - TASBackend.__name__) - agave_backend_name = '%s.%s' % (AgaveOAuthBackend.__module__, - AgaveOAuthBackend.__name__) + "Signal processor for user_logged_out" + backend = request.session.get("_auth_user_backend", None) + tas_backend_name = "%s.%s" % (TASBackend.__module__, TASBackend.__name__) + tapis_backend_name = "%s.%s" % ( + TapisOAuthBackend.__module__, + TapisOAuthBackend.__name__, + ) if backend == tas_backend_name: - login_provider = 'TACC' - elif backend == agave_backend_name: - login_provider = 'TACC' - else: - login_provider = 'your authentication provider' - - logger = logging.getLogger(__name__) - logger.debug("attempting call to revoke agave token function: %s", user.agave_oauth.token) - a = AgaveOAuthBackend() - AgaveOAuthBackend.revoke(a,user.agave_oauth) - - logout_message = '<h4>You are Logged Out!</h4>' \ - 'You are now logged out of DesignSafe! However, you may still ' \ - 'be logged in at %s. To ensure security, you should close your ' \ - 'browser to end all authenticated sessions.' % login_provider + login_provider = "TACC" + elif backend == tapis_backend_name: + login_provider = "TACC" + + logger.info( + "Revoking tapis token: %s", get_masked_token(user.tapis_oauth.access_token) + ) + backend = TapisOAuthBackend() + TapisOAuthBackend.revoke(backend, user.tapis_oauth.access_token) + + logout_message = ( + "<h4>You are Logged Out!</h4>" + "You are now logged out of DesignSafe! However, you may still " + f"be logged in at {login_provider}. To ensure security, you should close your " + "browser to end all authenticated sessions." + ) messages.warning(request, logout_message) class TASBackend(ModelBackend): - logger = logging.getLogger(__name__) - def __init__(self): self.tas = TASClient() @@ -56,20 +62,31 @@ def authenticate(self, request, username=None, password=None, **kwargs): if username is not None and password is not None: tas_user = None if request is not None: - self.logger.info('Attempting login via TAS for user "%s" from IP "%s"' % (username, request.META.get('REMOTE_ADDR'))) + self.logger.info( + 'Attempting login via TAS for user "%s" from IP "%s"' + % (username, request.META.get("REMOTE_ADDR")) + ) else: - self.logger.info('Attempting login via TAS for user "%s" from IP "%s"' % (username, 'unknown')) + self.logger.info( + 'Attempting login via TAS for user "%s" from IP "%s"' + % (username, "unknown") + ) try: # Check if this user is valid on the mail server if self.tas.authenticate(username, password): tas_user = self.tas.get_user(username=username) self.logger.info('Login successful for user "%s"' % username) else: - raise ValidationError('Authentication Error', 'Your username or password is incorrect.') + raise ValidationError( + "Authentication Error", + "Your username or password is incorrect.", + ) except Exception as e: self.logger.warning(e.args) - if re.search(r'PendingEmailConfirmation', e.args[1]): - raise ValidationError('Please confirm your email address before logging in.') + if re.search(r"PendingEmailConfirmation", e.args[1]): + raise ValidationError( + "Please confirm your email address before logging in." + ) else: raise ValidationError(e.args[1]) @@ -78,27 +95,30 @@ def authenticate(self, request, username=None, password=None, **kwargs): try: # Check if the user exists in Django's local database user = UserModel.objects.get(username=username) - user.first_name = tas_user['firstName'] - user.last_name = tas_user['lastName'] - user.email = tas_user['email'] + user.first_name = tas_user["firstName"] + user.last_name = tas_user["lastName"] + user.email = tas_user["email"] user.save() except UserModel.DoesNotExist: # Create a user in Django's local database - self.logger.info('Creating local user record for "%s" from TAS Profile' % username) + self.logger.info( + 'Creating local user record for "%s" from TAS Profile' + % username + ) user = UserModel.objects.create_user( username=username, - first_name=tas_user['firstName'], - last_name=tas_user['lastName'], - email=tas_user['email'] - ) + first_name=tas_user["firstName"], + last_name=tas_user["lastName"], + email=tas_user["email"], + ) try: profile = DesignSafeProfile.objects.get(user=user) - profile.institution = tas_user.get('institution', None) + profile.institution = tas_user.get("institution", None) profile.save() except DesignSafeProfile.DoesNotExist: profile = DesignSafeProfile(user=user) - profile.institution = tas_user.get('institution', None) + profile.institution = tas_user.get("institution", None) profile.save() try: @@ -110,72 +130,67 @@ def authenticate(self, request, username=None, password=None, **kwargs): return user -# class CILogonBackend(ModelBackend): - -# def authenticate(self, **kwargs): -# return None - - -class AgaveOAuthBackend(ModelBackend): - - logger = logging.getLogger(__name__) +class TapisOAuthBackend(ModelBackend): def authenticate(self, *args, **kwargs): user = None - if 'backend' in kwargs and kwargs['backend'] == 'agave': - token = kwargs['token'] - base_url = getattr(settings, 'AGAVE_TENANT_BASEURL') + if "backend" in kwargs and kwargs["backend"] == "tapis": + token = kwargs["token"] - self.logger.info('Attempting login via Agave with token "%s"' % - token[:8].ljust(len(token), '-')) + logger.info( + 'Attempting login via Tapis with token "%s"' % get_masked_token(token) + ) + client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL, access_token=token) - # TODO make this into an AgavePy call - response = requests.get('%s/profiles/v2/me' % base_url, - headers={'Authorization': 'Bearer %s' % token}) - if response.status_code >= 200 and response.status_code <= 299: - json_result = response.json() - agave_user = json_result['result'] - username = agave_user['username'] - UserModel = get_user_model() - try: - user = UserModel.objects.get(username=username) - user.first_name = agave_user['first_name'] - user.last_name = agave_user['last_name'] - user.email = agave_user['email'] - user.save() - except UserModel.DoesNotExist: - self.logger.info('Creating local user record for "%s" ' - 'from Agave Profile' % username) - user = UserModel.objects.create_user( - username=username, - first_name=agave_user['first_name'], - last_name=agave_user['last_name'], - email=agave_user['email'] - ) + try: + tapis_user_info = client.authenticator.get_userinfo() + except BaseTapyException as e: + logger.info("Tapis Authentication failed: %s", e.message) + return None - try: - profile = DesignSafeProfile.objects.get(user=user) - except DesignSafeProfile.DoesNotExist: - profile = DesignSafeProfile(user=user) - profile.save() - update_institution_from_tas.apply_async(args=[username], queue='api') + username = tapis_user_info.username - try: - prefs = NotificationPreferences.objects.get(user=user) - except NotificationPreferences.DoesNotExist: - prefs = NotificationPreferences(user=user) - prefs.save() + try: + user_data = get_user_data(username=username) + defaults = { + "first_name": user_data["firstName"], + "last_name": user_data["lastName"], + "email": user_data["email"], + } + except Exception: + logger.exception( + "Error retrieving TAS user profile data for user: %s", username + ) + defaults = { + "first_name": tapis_user_info.given_name, + "last_name": tapis_user_info.last_name, + "email": tapis_user_info.email, + } + + user, created = get_user_model().objects.update_or_create( + username=username, defaults=defaults + ) + + if created: + logger.info( + 'Created local user record for "%s" from TAS Profile', username + ) + + DesignSafeProfile.objects.get_or_create(user=user) + NotificationPreferences.objects.get_or_create(user=user) + + update_institution_from_tas.apply_async(args=[username], queue="api") + + logger.info('Login successful for user "%s"', username) - self.logger.error('Login successful for user "%s"' % username) - else: - self.logger.error('Agave Authentication failed: %s' % response.text) return user - def revoke(self, user): - base_url = getattr(settings, 'AGAVE_TENANT_BASEURL') - self.logger.info("attempting to revoke agave token %s" % user.masked_token) - response = requests.post('{base_url}/revoke'.format(base_url = base_url), - auth=HTTPBasicAuth(settings.AGAVE_CLIENT_KEY, settings.AGAVE_CLIENT_SECRET), - data={'token': user.access_token}) + def revoke(self, token): + self.logger.info( + "Attempting to revoke Tapis token %s" % get_masked_token(token) + ) + + client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL, access_token=token) + response = client.authenticator.revoke_token(token=token) self.logger.info("revoke response is %s" % response) diff --git a/designsafe/apps/auth/context_processors.py b/designsafe/apps/auth/context_processors.py deleted file mode 100644 index b4ec8e65a0..0000000000 --- a/designsafe/apps/auth/context_processors.py +++ /dev/null @@ -1,14 +0,0 @@ -from designsafe.apps.auth.models import AgaveOAuthToken - - -def auth(request): - try: - ag_token = request.user.agave_oauth - context = { - 'agave_ready': ag_token is not None - } - except (AttributeError, AgaveOAuthToken.DoesNotExist): - context = { - 'agave_ready': False - } - return context diff --git a/designsafe/apps/auth/middleware.py b/designsafe/apps/auth/middleware.py index acfc8174a5..877657d0e1 100644 --- a/designsafe/apps/auth/middleware.py +++ b/designsafe/apps/auth/middleware.py @@ -1,36 +1,67 @@ +""" +Auth middleware +""" + +import logging from django.contrib.auth import logout from django.core.exceptions import ObjectDoesNotExist -from requests.exceptions import RequestException, HTTPError -import logging -from django.utils.deprecation import MiddlewareMixin +from django.db import transaction +from django.urls import reverse +from tapipy.errors import BaseTapyException +from designsafe.apps.auth.models import TapisOAuthToken logger = logging.getLogger(__name__) -class AgaveTokenRefreshMiddleware(MiddlewareMixin): +class TapisTokenRefreshMiddleware: + """Refresh Middleware for a User's Tapis OAuth Token""" - def process_request(self, request): - if request.path != '/logout/' and request.user.is_authenticated: - try: - agave_oauth = request.user.agave_oauth - if agave_oauth.expired: - try: - agave_oauth.client.token.refresh() - except HTTPError: - logger.exception('Agave Token refresh failed; Forcing logout', - extra={'user': request.user.username}) - logout(request) - except ObjectDoesNotExist: - logger.warn('Authenticated user missing Agave API Token', - extra={'user': request.user.username}) - logout(request) - except RequestException: - logger.exception('Agave Token refresh failed. Forcing logout', - extra={'user': request.user.username}) - logout(request) - - def process_response(self, request, response): - if hasattr(request, 'user'): - if request.user.is_authenticated: - response['Authorization'] = 'Bearer ' + request.user.agave_oauth.access_token + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.path != reverse("logout") and request.user.is_authenticated: + self.process_request(request) + + response = self.get_response(request) return response + + def process_request(self, request): + """Processes requests to backend and refreshes Tapis Token atomically if token is expired.""" + try: + tapis_oauth = request.user.tapis_oauth + except ObjectDoesNotExist: + logger.warning( + "Authenticated user missing Tapis OAuth Token", + extra={"user": request.user.username}, + ) + logout(request) + + if not tapis_oauth.expired: + return + + logger.info( + f"Tapis OAuth token expired for user {request.user.username}. Refreshing token" + ) + with transaction.atomic(): + # Get a lock on this user's token row in db. + latest_token = ( + TapisOAuthToken.objects.select_for_update() + .filter(user=request.user) + .first() + ) + if latest_token.expired: + try: + logger.info("Refreshing Tapis OAuth token") + tapis_oauth.refresh_tokens() + except BaseTapyException: + logger.exception( + "Tapis Token refresh failed. Forcing logout", + extra={"user": request.user.username}, + ) + logout(request) + + else: + logger.info( + "Token updated by another request. Refreshing token from DB." + ) diff --git a/designsafe/apps/auth/models.py b/designsafe/apps/auth/models.py index 9f57bc9fb8..6db226f3f0 100644 --- a/designsafe/apps/auth/models.py +++ b/designsafe/apps/auth/models.py @@ -1,126 +1,115 @@ -from django.db import models -from django.conf import settings -from agavepy.agave import Agave -from agavepy import agave +"""Auth models +""" + import logging -import six import time -import requests -from requests import HTTPError -# from .signals import * -from designsafe.libs.common.decorators import deprecated +from django.db import models +from django.conf import settings +from tapipy.tapis import Tapis logger = logging.getLogger(__name__) TOKEN_EXPIRY_THRESHOLD = 600 -AGAVE_RESOURCES = agave.load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) -class AgaveOAuthToken(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='agave_oauth', on_delete=models.CASCADE) - token_type = models.CharField(max_length=255) - scope = models.CharField(max_length=255) - access_token = models.CharField(max_length=255) - refresh_token = models.CharField(max_length=255) +class TapisOAuthToken(models.Model): + """Represents an Tapis OAuth Token object. + + Use this class to store login details as well as refresh a token. + """ + + user = models.OneToOneField( + settings.AUTH_USER_MODEL, related_name="tapis_oauth", on_delete=models.CASCADE + ) + access_token = models.CharField(max_length=2048) + refresh_token = models.CharField(max_length=2048) expires_in = models.BigIntegerField() created = models.BigIntegerField() - @property - def masked_token(self): - return self.access_token[:8].ljust(len(self.access_token), '-') - @property def expired(self): - current_time = time.time() - return self.created + self.expires_in - current_time - TOKEN_EXPIRY_THRESHOLD <= 0 + """Check if token is expired + + :return: True or False, depending if the token is expired. + :rtype: bool + """ + return self.is_token_expired(self.created, self.expires_in) @property def created_at(self): - """ - Map the agavepy.Token property to model property + """Map the tapipy.Token property to model property + :return: The Epoch timestamp this token was created + :rtype: int """ return self.created_at @created_at.setter def created_at(self, value): - """ - Map the agavepy.Token property to model property - :param value: The Epoch timestamp this token was created + """Map the tapipy.Token property to model property + + :param int value: The Epoch timestamp this token was created """ self.created = value @property def token(self): + """Token dictionary. + + :return: Full token object + :rtype: dict + """ return { - 'access_token': self.access_token, - 'refresh_token': self.refresh_token, - 'token_type': self.token_type, - 'scope': self.scope, - 'created': self.created, - 'expires_in': self.expires_in + "access_token": self.access_token, + "refresh_token": self.refresh_token, + "created": self.created, + "expires_in": self.expires_in, } @property def client(self): - return Agave(api_server=getattr(settings, 'AGAVE_TENANT_BASEURL'), - api_key=getattr(settings, 'AGAVE_CLIENT_KEY'), - api_secret=getattr(settings, 'AGAVE_CLIENT_SECRET'), - token=self.access_token, - resources=AGAVE_RESOURCES, - refresh_token=self.refresh_token, - token_callback=self.update) + """Tapis client to limit one request to Tapis per User. + + :return: Tapis client using refresh token. + :rtype: :class:Tapis + """ + return Tapis( + base_url=getattr(settings, "TAPIS_TENANT_BASEURL"), + client_id=getattr(settings, "TAPIS_CLIENT_ID"), + client_key=getattr(settings, "TAPIS_CLIENT_KEY"), + access_token=self.access_token, + refresh_token=self.refresh_token, + ) def update(self, **kwargs): - for k, v in six.iteritems(kwargs): + """Bulk update model attributes""" + for k, v in kwargs.items(): setattr(self, k, v) self.save() - @deprecated - def refresh(self): - """ - DEPRECATED - :return: - """ - logger.debug('Refreshing Agave OAuth token for user=%s' % self.user.username) - ag = Agave(api_server=getattr(settings, 'AGAVE_TENANT_BASEURL'), - api_key=getattr(settings, 'AGAVE_CLIENT_KEY'), - api_secret=getattr(settings, 'AGAVE_CLIENT_SECRET'), - resources=AGAVE_RESOURCES, - token=self.access_token, - refresh_token=self.refresh_token) + def refresh_tokens(self): + """Refresh and update Tapis OAuth Tokens""" + self.client.refresh_tokens() + self.update( + created=int(time.time()), + access_token=self.client.access_token.access_token, + refresh_token=self.client.refresh_token.refresh_token, + expires_in=self.client.access_token.expires_in().total_seconds(), + ) + + def __str__(self): + access_token_masked = self.access_token[-5:] + refresh_token_masked = self.refresh_token[-5:] + return f"access_token:{access_token_masked} refresh_token:{refresh_token_masked} expires_in:{self.expires_in} created:{self.created}" + + @staticmethod + def is_token_expired(created, expires_in): + """Check if token is expired, with TOKEN_EXPIRY_THRESHOLD buffer.""" current_time = time.time() - ag.token.refresh() - self.created = int(current_time) - self.update(**ag.token.token_info) - logger.debug('Agave OAuth token for user=%s refreshed: %s' % (self.user.username, - self.masked_token)) - - -class AgaveServiceStatus(object): - page_id = getattr(settings, 'AGAVE_STATUSIO_PAGE_ID', '53a1e022814a437c5a000781') - status_io_base_url = getattr(settings, 'STATUSIO_BASE_URL', - 'https://api.status.io/1.0') - status_overall = {} - status = [] - incidents = [] - maintenance = { - 'active': [], - 'upcoming': [], - } - - def __init__(self): - self.update() - - def update(self): - try: - resp = requests.get('%s/status/%s' % (self.status_io_base_url, self.page_id)) - data = resp.json() - if 'result' in data: - for k, v, in six.iteritems(data['result']): - setattr(self, k, v) - else: - raise Exception(data) - except HTTPError: - logger.warning('Agave Service Status update failed') + return created + expires_in - current_time - TOKEN_EXPIRY_THRESHOLD <= 0 + + @staticmethod + def get_masked_token(token): + """Return a token as a masked string""" + return token[:8].ljust(len(token), "-") diff --git a/designsafe/apps/auth/models_unit_test.py b/designsafe/apps/auth/models_unit_test.py new file mode 100644 index 0000000000..cbd9d159c2 --- /dev/null +++ b/designsafe/apps/auth/models_unit_test.py @@ -0,0 +1,46 @@ +import pytest +import time +from datetime import timedelta +from designsafe.apps.auth.models import TapisOAuthToken + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def authenticated_user_with_expired_token(authenticated_user): + authenticated_user.tapis_oauth.expires_in = 0 + authenticated_user.tapis_oauth.save() + yield authenticated_user + + +@pytest.fixture +def authenticated_user_with_valid_token(authenticated_user): + authenticated_user.tapis_oauth.created = time.time() + authenticated_user.tapis_oauth.save() + yield authenticated_user + + +@pytest.fixture() +def tapis_client_mock(mocker): + mock_client = mocker.patch("designsafe.apps.auth.models.TapisOAuthToken.client") + mock_client.access_token.access_token = ("XYZXYZXYZ",) + mock_client.access_token.expires_in.return_value = timedelta(seconds=2000) + yield mock_client + + +def test_valid_user(client, authenticated_user_with_valid_token, tapis_client_mock): + tapis_oauth = ( + TapisOAuthToken.objects.filter(user=authenticated_user_with_valid_token) + .select_for_update() + .get() + ) + assert not tapis_oauth.expired + + +def test_expired_user(client, authenticated_user_with_expired_token, tapis_client_mock): + tapis_oauth = ( + TapisOAuthToken.objects.filter(user=authenticated_user_with_expired_token) + .select_for_update() + .get() + ) + assert tapis_oauth.expired diff --git a/designsafe/apps/auth/templates/designsafe/apps/auth/login.html b/designsafe/apps/auth/templates/designsafe/apps/auth/login.html index 6b8e292501..a10dbc2408 100644 --- a/designsafe/apps/auth/templates/designsafe/apps/auth/login.html +++ b/designsafe/apps/auth/templates/designsafe/apps/auth/login.html @@ -23,13 +23,13 @@ <h1 class="headline headline-community"><span class="hl-community">Log in</span> {% endif %} <div class="login-option-block"> <p> - <a href="{% url 'designsafe_auth:agave_oauth' %}?{{ request.META.QUERY_STRING }}" class="btn btn-lg btn-primary img-responsive"> + <a href="{% url 'designsafe_auth:tapis_oauth' %}?{{ request.META.QUERY_STRING }}" class="btn btn-lg btn-primary img-responsive"> <img class="provider-logo img-responsive" style="width:100% \9;" alt="TACC" title="Log in with TACC" src="{% static 'designsafe/apps/auth/logos/tacc.svg'%}"> </a> </p> <p> - <a href="{% url 'designsafe_auth:agave_oauth' %}?{{ request.META.QUERY_STRING }}">Log in using your TACC Account.</a> + <a href="{% url 'designsafe_auth:tapis_oauth' %}?{{ request.META.QUERY_STRING }}">Log in using your TACC Account.</a> </p> <a href="{% url 'designsafe_accounts:register' %}?{{ request.META.QUERY_STRING }}">Register for a TACC account</a> • diff --git a/designsafe/apps/auth/unit_test.py b/designsafe/apps/auth/unit_test.py new file mode 100644 index 0000000000..ad86c7b049 --- /dev/null +++ b/designsafe/apps/auth/unit_test.py @@ -0,0 +1,98 @@ +from django.test import TransactionTestCase, override_settings +from django.contrib.auth import get_user_model +from mock import patch, MagicMock +from portal.apps.auth.backends import TapisOAuthBackend +from requests import Response +from portal.apps.auth.views import launch_setup_checks +import pytest + +pytestmark = pytest.mark.django_db + + +def test_launch_setup_checks(mocker, regular_user, settings): + mocker.patch("portal.apps.auth.views.new_user_setup_check") + mock_execute = mocker.patch("portal.apps.auth.views.execute_setup_steps") + regular_user.profile.setup_complete = False + launch_setup_checks(regular_user) + mock_execute.apply_async.assert_called_with(args=["username"]) + + +class TestTapisOAuthBackend(TransactionTestCase): + def setUp(self): + super(TestTapisOAuthBackend, self).setUp() + self.backend = TapisOAuthBackend() + self.mock_response = MagicMock(autospec=Response) + self.mock_requests_patcher = patch( + "portal.apps.auth.backends.requests.get", return_value=self.mock_response + ) + self.mock_requests = self.mock_requests_patcher.start() + + self.mock_user_data_patcher = patch( + "portal.apps.auth.backends.get_user_data", + return_value={ + "username": "testuser", + "firstName": "test", + "lastName": "user", + "email": "new@email.com", + }, + ) + self.mock_user_data = self.mock_user_data_patcher.start() + + def tearDown(self): + super(TestTapisOAuthBackend, self).tearDown() + self.mock_requests_patcher.stop() + self.mock_user_data_patcher.stop() + + def test_bad_backend_params(self): + # Test backend authenticate with no params + result = self.backend.authenticate() + self.assertIsNone(result) + # Test TapisOAuthBackend if params do not indicate tapis + result = self.backend.authenticate(backend="not_tapis") + self.assertIsNone(result) + + def test_bad_response_status(self): + # Test that backend failure responses are handled + + # Mock different return values for the backend response + self.mock_response.json.return_value = {} + result = self.backend.authenticate(backend="tapis", token="1234") + self.assertIsNone(result) + self.mock_response.json.return_value = {"status": "failure"} + result = self.backend.authenticate(backend="tapis", token="1234") + self.assertIsNone(result) + + @override_settings(PORTAL_USER_ACCOUNT_SETUP_STEPS=[]) + def test_new_user(self): + # Test that a new user is created and returned + self.mock_response.json.return_value = { + "status": "success", + "result": {"username": "testuser"}, + } + result = self.backend.authenticate(backend="tapis", token="1234") + self.assertEqual(result.username, "testuser") + + @override_settings(PORTAL_USER_ACCOUNT_SETUP_STEPS=[]) + def test_update_existing_user(self): + # Test that an existing user's information is + # updated with from info from the Tapis backend response + + # Create a pre-existing user with the same username + user = get_user_model().objects.create_user( + username="testuser", + first_name="test", + last_name="user", + email="old@email.com", + ) + self.mock_response.json.return_value = { + "status": "success", + "result": { + "username": "testuser", + }, + } + result = self.backend.authenticate(backend="tapis", token="1234") + # Result user object should be the same + self.assertEqual(result, user) + # Existing user object should be updated + user = get_user_model().objects.get(username="testuser") + self.assertEqual(user.email, "new@email.com") diff --git a/designsafe/apps/auth/urls.py b/designsafe/apps/auth/urls.py index 003639c78d..cc6a5ef832 100644 --- a/designsafe/apps/auth/urls.py +++ b/designsafe/apps/auth/urls.py @@ -1,36 +1,41 @@ -from django.urls import re_path as url -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +""" +.. module:: portal.apps.auth.urls + :synopsis: Auth URLs +""" + +from django.urls import path from designsafe.apps.auth import views +app_name = "portal_auth" urlpatterns = [ - url(r'^$', views.login_options, name='login'), - url(r'^logged-out/$', views.logged_out, name='logout'), - url(r'^agave/$', views.agave_oauth, name='agave_oauth'), - url(r'^agave/callback/$', views.agave_oauth_callback, name='agave_oauth_callback'), - url(r'^agave/session-error/$', views.agave_session_error, name='agave_session_error'), + path("logged-out/", views.logged_out, name="logout"), + path("tapis/", views.tapis_oauth, name="tapis_oauth"), + path("tapis/callback/", views.tapis_oauth_callback, name="tapis_oauth_callback"), ] -def menu_items(**kwargs): - if 'type' in kwargs and kwargs['type'] == 'account': - return [ - { - 'label': _('Login'), - 'url': reverse('login'), - 'children': [], - 'visible': False - }, - { - 'label': _('Login'), - 'url': reverse('designsafe_auth:login'), - 'children': [], - 'visible': False - }, - { - 'label': _('Agave'), - 'url': reverse('designsafe_auth:agave_session_error'), - 'children': [], - 'visible': False - } - ] +# from django.urls import reverse +# from django.utils.translation import gettext_lazy as _ + +# def menu_items(**kwargs): +# if 'type' in kwargs and kwargs['type'] == 'account': +# return [ +# { +# 'label': _('Login'), +# 'url': reverse('login'), +# 'children': [], +# 'visible': False +# }, +# { +# 'label': _('Login'), +# 'url': reverse('designsafe_auth:login'), +# 'children': [], +# 'visible': False +# }, +# { +# 'label': _('Agave'), +# 'url': reverse('designsafe_auth:agave_session_error'), +# 'children': [], +# 'visible': False +# } +# ] diff --git a/designsafe/apps/auth/views.py b/designsafe/apps/auth/views.py index 92e0813d53..5714c81de0 100644 --- a/designsafe/apps/auth/views.py +++ b/designsafe/apps/auth/views.py @@ -1,209 +1,174 @@ +""" +Auth views. +""" + +import logging +import time +import secrets +import requests from django.conf import settings from django.contrib import messages from django.contrib.auth import authenticate, login from django.urls import reverse -from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseRedirect, HttpResponseBadRequest from django.shortcuts import render -import secrets - -from .models import AgaveOAuthToken, AgaveServiceStatus -from agavepy.agave import Agave -from designsafe.apps.auth.tasks import check_or_create_agave_home_dir, new_user_alert -import logging -import os -import requests -import time -from requests import HTTPError - +from .models import TapisOAuthToken +# TODOV3: Onboarding +# from tapipy.errors import BaseTapyException +# from designsafe.apps.auth.tasks import check_or_create_agave_home_dir +# from portal.apps.onboarding.execute import execute_setup_steps, new_user_setup_check +# from portal.apps.search.tasks import index_allocations logger = logging.getLogger(__name__) +METRICS = logging.getLogger(f"metrics.{__name__}") def logged_out(request): - return render(request, 'designsafe/apps/auth/logged_out.html') - - -def login_options(request): - if request.user.is_authenticated: - messages.info(request, 'You are already logged in!') - return HttpResponseRedirect('/') - - message = False - - try: - agave_status = AgaveServiceStatus() - ds_oauth_svc_id = getattr(settings, 'AGAVE_DESIGNSAFE_OAUTH_STATUS_ID', - '56bb6d92a216b873280008fd') - designsafe_status = next((s for s in agave_status.status - if s['id'] == ds_oauth_svc_id)) - if designsafe_status and 'status_code' in designsafe_status: - if designsafe_status['status_code'] == 400: - message = { - 'class': 'warning', - 'text': 'DesignSafe API Services are experiencing a ' - 'Partial Service Disruption. Some services ' - 'may be unavailable.' - } - elif designsafe_status['status_code'] == 500: - message = { - 'class': 'danger', - 'text': 'DesignSafe API Services are experiencing a ' - 'Service Disruption. Some services may be ' - 'unavailable.' - } - except Exception as e: - logger.warn('Unable to check AgaveServiceStatus') - logger.warn(e) - agave_status = None - designsafe_status = None - - if not message: - return agave_oauth(request) - else: - context = { - 'message': message, - 'agave_status': agave_status, - 'designsafe_status': designsafe_status, - } - return render(request, 'designsafe/apps/auth/login.html', context) + """Render logged out page upon logout""" + return render(request, "designsafe/apps/auth/logged_out.html") -def agave_oauth(request): - tenant_base_url = getattr(settings, 'AGAVE_TENANT_BASEURL') - client_key = getattr(settings, 'AGAVE_CLIENT_KEY') +def _get_auth_state(): + return secrets.token_hex(24) + +def tapis_oauth(request): + """First step for Tapis OAuth workflow.""" session = request.session - session['auth_state'] = secrets.token_hex(24) - next_page = request.GET.get('next') + session["auth_state"] = _get_auth_state() + next_page = request.GET.get("next") if next_page: - session['next'] = next_page - # Check for HTTP_X_DJANGO_PROXY custom header - django_proxy = request.META.get('HTTP_X_DJANGO_PROXY', 'false') == 'true' - if django_proxy or request.is_secure(): - protocol = 'https' + session["next"] = next_page + + if request.is_secure(): + protocol = "https" else: - protocol = 'http' - redirect_uri = '{}://{}{}'.format( - protocol, - request.get_host(), - reverse('designsafe_auth:agave_oauth_callback') - ) + protocol = "http" + + redirect_uri = f"{protocol}://{request.get_host()}{reverse('designsafe_auth:tapis_oauth_callback')}" + + tenant_base_url = getattr(settings, "TAPIS_TENANT_BASEURL") + client_id = getattr(settings, "TAPIS_CLIENT_ID") + + METRICS.info(f"user:{request.user.username} starting oauth redirect login") + + # Authorization code request authorization_url = ( - '%s/authorize?' - 'client_id=%s&' - 'response_type=code&' - 'redirect_uri=%s&' - 'state=%s' % ( - tenant_base_url, - client_key, - redirect_uri, - session['auth_state'], - ) + f"{tenant_base_url}/v3/oauth2/authorize?" + f"client_id={client_id}&" + f"redirect_uri={redirect_uri}&" + "response_type=code&" + f"state={session['auth_state']}" ) + return HttpResponseRedirect(authorization_url) -def agave_oauth_callback(request): - """ - http://agaveapi.co/documentation/authorization-guide/#authorization_code_flow - """ - state = request.GET.get('state') +# TODOV3: Onboarding +# def launch_setup_checks(user): +# """Perform any onboarding checks or non-onboarding steps that may spawn celery tasks""" + +# # Check onboarding settings +# # new_user_setup_check(user) +# # if not user.profile.setup_complete: +# # logger.info("Executing onboarding setup steps for %s", user.username) +# # execute_setup_steps.apply_async(args=[user.username]) +# # else: +# # logger.info( +# # "Already onboarded, running non-onboarding steps (e.g. update cached " +# # "allocation information) for %s", +# # user.username, +# # ) +# # index_allocations.apply_async(args=[user.username]) + +# # TODOV3: Onboarding -> Move home dir creation to onboarding step +# client = user.tapis_oauth.client +# try: +# client.files.list( +# systemId=settings.AGAVE_STORAGE_SYSTEM, filePath=user.username +# ) +# except BaseTapyException as e: +# if e.response.status_code == 404: +# check_or_create_agave_home_dir.apply_async( +# args=(user.username, settings.AGAVE_STORAGE_SYSTEM), queue="files" +# ) + +# try: +# client.files.list( +# systemId=settings.AGAVE_WORKING_SYSTEM, filePath=user.username +# ) +# except BaseTapyException as e: +# if e.response.status_code == 404: +# check_or_create_agave_home_dir.apply_async( +# args=(user.username, settings.AGAVE_WORKING_SYSTEM), queue="files" +# ) + + +def tapis_oauth_callback(request): + """Tapis OAuth callback handler.""" + state = request.GET.get("state") + + if request.session["auth_state"] != state: + msg = f"OAuth Authorization State mismatch: auth_state={request.session['auth_state']} does not match returned state={state}" - if request.session['auth_state'] != state: - msg = ( - 'OAuth Authorization State mismatch!? auth_state=%s ' - 'does not match returned state=%s' % ( - request.session['auth_state'], state - ) - ) logger.warning(msg) - return HttpResponseBadRequest('Authorization State Failed') + return HttpResponseBadRequest("Authorization State Failed") - if 'code' in request.GET: + if "code" in request.GET: # obtain a token for the user - # Check for HTTP_X_DJANGO_PROXY custom header - request.META.get('HTTP_X_DJANGO_PROXY', 'false') == 'true' - django_proxy = request.META.get('HTTP_X_DJANGO_PROXY', 'false') - if django_proxy or request.is_secure(): - protocol = 'https' + if request.is_secure(): + protocol = "https" else: - protocol = 'http' - redirect_uri = '{}://{}{}'.format( - protocol, - request.get_host(), - reverse('designsafe_auth:agave_oauth_callback') - ) - code = request.GET['code'] - tenant_base_url = getattr(settings, 'AGAVE_TENANT_BASEURL') - client_key = getattr(settings, 'AGAVE_CLIENT_KEY') - client_sec = getattr(settings, 'AGAVE_CLIENT_SECRET') + protocol = "http" + redirect_uri = f"{protocol}://{request.get_host()}{reverse('designsafe_auth:tapis_oauth_callback')}" + code = request.GET["code"] + body = { - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': redirect_uri, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + } + response = requests.post( + f"{settings.TAPIS_TENANT_BASEURL}/v3/oauth2/tokens", + data=body, + auth=(settings.TAPIS_CLIENT_ID, settings.TAPIS_CLIENT_KEY), + timeout=30, + ) + response_json = response.json() + token_data = { + "created": int(time.time()), + "access_token": response_json["result"]["access_token"]["access_token"], + "refresh_token": response_json["result"]["refresh_token"]["refresh_token"], + "expires_in": response_json["result"]["access_token"]["expires_in"], } - # TODO update to token call in agavepy - response = requests.post('%s/token' % tenant_base_url, - data=body, - auth=(client_key, client_sec)) - token_data = response.json() - token_data['created'] = int(time.time()) + # log user in - user = authenticate(backend='agave', token=token_data['access_token']) + user = authenticate(backend="tapis", token=token_data["access_token"]) if user: - try: - token = user.agave_oauth - token.update(**token_data) - except ObjectDoesNotExist: - token = AgaveOAuthToken(**token_data) - token.user = user - new_user_alert.apply_async(args=(user.username,)) - token.save() + TapisOAuthToken.objects.update_or_create(user=user, defaults={**token_data}) login(request, user) - - ag = Agave(api_server=settings.AGAVE_TENANT_BASEURL, - token=settings.AGAVE_SUPER_TOKEN) - try: - ag.files.list(systemId=settings.AGAVE_STORAGE_SYSTEM, - filePath=user.username) - except HTTPError as e: - if e.response.status_code == 404: - check_or_create_agave_home_dir.apply_async(args=(user.username, settings.AGAVE_STORAGE_SYSTEM),queue='files') - - try: - ag.files.list(systemId=settings.AGAVE_WORKING_SYSTEM, - filePath=user.username) - except HTTPError as e: - if e.response.status_code == 404: - check_or_create_agave_home_dir.apply_async(args=(user.username, settings.AGAVE_WORKING_SYSTEM),queue='files') - + # TODOV3: Onboarding + # launch_setup_checks(user) else: messages.error( request, - 'Authentication failed. Please try again. If this problem ' - 'persists please submit a support ticket.' + "Authentication failed. Please try again. If this problem " + "persists please submit a support ticket.", ) - return HttpResponseRedirect(reverse('designsafe_auth:login')) + return HttpResponseRedirect(reverse("logout")) else: - if 'error' in request.GET: - error = request.GET['error'] - logger.warning('Authorization failed: %s' % error) - messages.error( - request, 'Authentication failed! Did you forget your password? ' - '<a href="%s">Click here</a> to reset your password.' % - reverse('designsafe_accounts:password_reset')) - return HttpResponseRedirect(reverse('designsafe_auth:login')) - if 'next' in request.session: - next_uri = request.session.pop('next') - return HttpResponseRedirect(next_uri) - else: - # return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) - return HttpResponseRedirect(reverse('designsafe_dashboard:index')) + if "error" in request.GET: + error = request.GET["error"] + logger.warning("Authorization failed: %s", error) + + return HttpResponseRedirect(reverse("logout")) + redirect = getattr(settings, "LOGIN_REDIRECT_URL", "/") + if "next" in request.session: + redirect += "?next=" + request.session.pop("next") -def agave_session_error(request): - return render(request, 'designsafe/apps/auth/agave_session_error.html') + response = HttpResponseRedirect(redirect) + return response diff --git a/designsafe/apps/auth/views_unit_test.py b/designsafe/apps/auth/views_unit_test.py new file mode 100644 index 0000000000..c09d592dd1 --- /dev/null +++ b/designsafe/apps/auth/views_unit_test.py @@ -0,0 +1,95 @@ +"""DesignSafe Auth Tapis OAuth flow view tests""" +import pytest +from django.conf import settings +from django.urls import reverse + +# TODOV3: Onboarding Tests +# from portal.apps.auth.views import launch_setup_checks + +TEST_STATE = "ABCDEFG123456" + +pytestmark = pytest.mark.django_db + + +def test_auth_tapis(client, mocker): + """Test auth flow redirect""" + mocker.patch("designsafe.apps.auth.views._get_auth_state", return_value=TEST_STATE) + + response = client.get("/auth/tapis/", follow=False) + + tapis_authorize = ( + f"{settings.TAPIS_TENANT_BASEURL}/v3/oauth2/authorize" + f"?client_id=test&redirect_uri=http://testserver/auth/tapis/callback/&response_type=code&state={TEST_STATE}" + ) + + assert response.status_code == 302 + assert response.url == tapis_authorize + assert client.session["auth_state"] == TEST_STATE + + +def test_tapis_callback(client, mocker, regular_user, tapis_tokens_create_mock): + """Test successful Tapis callback""" + mock_authenticate = mocker.patch("designsafe.apps.auth.views.authenticate") + mock_tapis_token_post = mocker.patch("designsafe.apps.auth.views.requests.post") + # TODOV3: Onboarding Tests + # mock_launch_setup_checks = mocker.patch( + # "designsafe.apps.auth.views.launch_setup_checks" + # ) + + # add auth to session + session = client.session + session["auth_state"] = TEST_STATE + session.save() + + mock_tapis_token_post.return_value.json.return_value = tapis_tokens_create_mock + mock_tapis_token_post.return_value.status_code = 200 + mock_authenticate.return_value = regular_user + + response = client.get( + f"/auth/tapis/callback/?state={TEST_STATE}&code=83163624a0bc41c4a376e0acb16a62f9" + ) + assert response.status_code == 302 + assert response.url == settings.LOGIN_REDIRECT_URL + # TODOV3: Onboarding Tests + # assert mock_launch_setup_checks.call_count == 1 + + +def test_tapis_callback_no_code(client): + """Test Tapis callback with no auth code""" + # add auth to session + session = client.session + session["auth_state"] = TEST_STATE + session.save() + + response = client.get(f"/auth/tapis/callback/?state={TEST_STATE}") + assert response.status_code == 302 + assert response.url == reverse("logout") + + +def test_tapis_callback_mismatched_state(client): + """Test Tapis callback with mismatched state""" + # add auth to session + session = client.session + session["auth_state"] = "TEST_STATE" + session.save() + response = client.get("/auth/tapis/callback/?state=bar") + assert response.status_code == 400 + + +# TODOV3: Onboarding Tests +# def test_launch_setup_checks(regular_user, mocker): +# mock_execute_setup_steps = mocker.patch( +# "portal.apps.auth.views.execute_setup_steps" +# ) +# launch_setup_checks(regular_user) +# mock_execute_setup_steps.apply_async.assert_called_with( +# args=[regular_user.username] +# ) + + +# TODOV3: Onboarding Tests +# def test_launch_setup_checks_already_onboarded(regular_user, mocker): +# regular_user.profile.setup_complete = True +# mock_index_allocations = mocker.patch("portal.apps.auth.views.index_allocations") +# launch_setup_checks(regular_user) +# mock_index_allocations.apply_async.assert_called_with(args=[regular_user.username]) diff --git a/designsafe/conftest.py b/designsafe/conftest.py index 3b327245b1..1cb2309d9d 100644 --- a/designsafe/conftest.py +++ b/designsafe/conftest.py @@ -1,44 +1,46 @@ +"""Base User pytest fixtures""" + import pytest -from django.conf import settings -from designsafe.apps.auth.models import AgaveOAuthToken +from designsafe.apps.auth.models import TapisOAuthToken + @pytest.fixture -def mock_agave_client(mocker): - yield mocker.patch('designsafe.apps.auth.models.AgaveOAuthToken.client', autospec=True) +def mock_tapis_client(mocker): + """Tapis client fixture""" + yield mocker.patch( + "designsafe.apps.auth.models.TapisOAuthToken.client", autospec=True + ) @pytest.fixture -def regular_user(django_user_model, mock_agave_client): - django_user_model.objects.create_user(username="username", - password="password", - first_name="Firstname", - last_name="Lastname", - email="user@user.com") - django_user_model.objects.create_user(username="username2", - password="password2", - first_name="Firstname2", - last_name="Lastname2", - email="user@user.com2") +def regular_user(django_user_model, mock_tapis_client): + """Normal User fixture""" + django_user_model.objects.create_user( + username="username", + password="password", + first_name="Firstname", + last_name="Lastname", + email="user@user.com", + ) user = django_user_model.objects.get(username="username") - token = AgaveOAuthToken.objects.create( + TapisOAuthToken.objects.create( user=user, - token_type="bearer", - scope="default", access_token="1234fsf", refresh_token="123123123", expires_in=14400, - created=1523633447) - token.save() + created=1523633447, + ) yield user @pytest.fixture def project_admin_user(django_user_model): - django_user_model.objects.create_user(username="test_prjadmin", - password="password", - first_name="Project", - last_name="Admin", + django_user_model.objects.create_user( + username="test_prjadmin", + password="password", + first_name="Project", + last_name="Admin", ) user = django_user_model.objects.get(username="test_prjadmin") yield user diff --git a/designsafe/settings/common_settings.py b/designsafe/settings/common_settings.py index af2b46e187..ec2917098d 100644 --- a/designsafe/settings/common_settings.py +++ b/designsafe/settings/common_settings.py @@ -138,7 +138,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'designsafe.apps.token_access.middleware.TokenAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware', + 'designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -524,6 +524,15 @@ AGAVE_USER_STORE_ID = os.environ.get('AGAVE_USER_STORE_ID', 'TACC') AGAVE_USE_SANDBOX = os.environ.get('AGAVE_USE_SANDBOX', 'False').lower() == 'true' +# Tapis Client Configuration +PORTAL_ADMIN_USERNAME = os.environ.get('PORTAL_ADMIN_USERNAME') +TAPIS_TENANT_BASEURL = os.environ.get('TAPIS_TENANT_BASEURL') +TAPIS_CLIENT_ID = os.environ.get('TAPIS_CLIENT_ID') +TAPIS_CLIENT_KEY = os.environ.get('TAPIS_CLIENT_KEY') +TAPIS_ADMIN_JWT = os.environ.get('TAPIS_ADMIN_JWT') + +KEY_SERVICE_TOKEN = os.environ.get('KEY_SERVICE_TOKEN') + DS_ADMIN_USERNAME = os.environ.get('DS_ADMIN_USERNAME') DS_ADMIN_PASSWORD = os.environ.get('DS_ADMIN_PASSWORD') diff --git a/designsafe/settings/test_settings.py b/designsafe/settings/test_settings.py index e88bd5e0be..496e2d8fce 100644 --- a/designsafe/settings/test_settings.py +++ b/designsafe/settings/test_settings.py @@ -131,7 +131,7 @@ 'django.contrib.sessions.middleware.SessionMiddleware', 'designsafe.apps.token_access.middleware.TokenAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware', + 'designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -528,7 +528,7 @@ # No token refreshes during testing MIDDLEWARE= [c for c in MIDDLEWARE if c != - 'designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware'] + 'designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware'] STATIC_ROOT = os.path.join(BASE_DIR, 'static') MEDIA_ROOT = os.path.join(BASE_DIR, '.media') diff --git a/designsafe/templates/includes/header.html b/designsafe/templates/includes/header.html index 4ef08ee78f..b3eef01240 100644 --- a/designsafe/templates/includes/header.html +++ b/designsafe/templates/includes/header.html @@ -37,15 +37,7 @@ </li> </ul> {% if user.is_authenticated %} - <span style="vertical-align:middle"> - {% if not agave_ready %} - - <a class="text-danger" title="API Session Not Available. Click for details." - href="{% url 'designsafe_auth:agave_session_error' %}"><i role="none" class="fa fa-exclamation-triangle"></i><span - class="sr-only">API Session Not Available. Click for details.</span></a> - {% endif %} - </span> - <notification-badge></notification-badge> + <notification-badge></notification-badge> <div class="btn-group"> <a href="{% url 'designsafe_dashboard:index' %}" class="btn btn-link-alt">Welcome, {{ user.first_name }}!</a> <button type="button" class="btn btn-link-alt dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" diff --git a/designsafe/urls.py b/designsafe/urls.py index 0c7e5adb86..c4918d6639 100644 --- a/designsafe/urls.py +++ b/designsafe/urls.py @@ -29,7 +29,6 @@ from django.views.generic import RedirectView, TemplateView from django.urls import reverse, path from django.http import HttpResponse, HttpResponseRedirect -from designsafe.apps.auth.views import login_options as des_login_options from django.contrib.auth.views import LogoutView as des_logout from designsafe.views import project_version as des_version, redirect_old_nees from impersonate import views as impersonate_views @@ -148,7 +147,6 @@ # auth url(r'^auth/', include(('designsafe.apps.auth.urls', 'designsafe.apps.auth'), namespace='designsafe_auth')), - url(r'^login/$', des_login_options, name='login'), url(r'^logout/$', des_logout.as_view(), name='logout'), # help diff --git a/poetry.lock b/poetry.lock index b530c7ac6b..57c63b4fdf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,38 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. - -[[package]] -name = "agavepy" -version = "1.0.0a12" -description = "SDK for TACC Tapis (formerly Agave)" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "agavepy-1.0.0a12-py2.py3-none-any.whl", hash = "sha256:ad096612c07a05d7f16b0e0e49d75dacfbccd3681e82c1b5f92c2c1b733b4209"}, - {file = "agavepy-1.0.0a12.tar.gz", hash = "sha256:60717e2749d1e4116a10b0aed509f691f56ebea8a1321f29f621ff1ddd4a9d1e"}, -] - -[package.dependencies] -arrow = ">=0.15.5" -attrdict = ">=2.0.0" -cloudpickle = ">=1.3.0" -curlify = ">=2.2.1" -future = ">=0.18.2" -Jinja2 = ">=2.11.1" -petname = ">=2.6" -py = ">=1.8.1" -python-dateutil = ">=2.8.1" -python-dotenv = ">=0.11.0" -requests = ">=2.23.0" -requests-toolbelt = ">=0.9.1" -six = ">=1.12.0" -websocket-client = ">=0.57.0" +# This file is automatically @generated by Poetry 1.5.0 and should not be changed by hand. [[package]] name = "amqp" version = "5.1.1" description = "Low-level AMQP client for Python (fork of amqplib)." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -47,7 +18,6 @@ vine = ">=5.0.0" name = "annotated-types" version = "0.6.0" description = "Reusable constraint types to use with typing.Annotated" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -59,7 +29,6 @@ files = [ name = "appnope" version = "0.1.3" description = "Disable App Nap on macOS >= 10.9" -category = "main" optional = false python-versions = "*" files = [ @@ -67,31 +36,10 @@ files = [ {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, ] -[[package]] -name = "arrow" -version = "1.3.0" -description = "Better dates & times for Python" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" - -[package.extras] -doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (>=1.0.0,<2.0.0)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (>=3.0.0,<4.0.0)"] - [[package]] name = "asgiref" version = "3.7.2" description = "ASGI specs, helper code, and adapters" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -106,7 +54,6 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "astroid" version = "2.15.8" description = "An abstract syntax tree for Python with inference support." -category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -122,7 +69,6 @@ wrapt = {version = ">=1.14,<2", markers = "python_version >= \"3.11\""} name = "asttokens" version = "2.4.1" description = "Annotate AST trees with source code positions" -category = "main" optional = false python-versions = "*" files = [ @@ -141,7 +87,6 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -149,11 +94,20 @@ files = [ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + [[package]] name = "attrdict" version = "2.0.1" description = "A dict with attribute-style access" -category = "main" optional = false python-versions = "*" files = [] @@ -172,7 +126,6 @@ resolved_reference = "83b779ee82d5b0e33be695d398162b8f2430ff33" name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -191,7 +144,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "autobahn" version = "23.6.2" description = "WebSocket client & server library, WAMP real-time framework" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -220,7 +172,6 @@ xbr = ["base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "click (>=8. name = "automat" version = "22.10.0" description = "Self-service finite-state machines for the programmer on the go." -category = "main" optional = false python-versions = "*" files = [ @@ -239,7 +190,6 @@ visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "main" optional = false python-versions = "*" files = [ @@ -251,7 +201,6 @@ files = [ name = "beautifulsoup4" version = "4.12.2" description = "Screen-scraping library" -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -270,7 +219,6 @@ lxml = ["lxml"] name = "billiard" version = "4.1.0" description = "Python multiprocessing fork with improvements and bugfixes" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -282,7 +230,6 @@ files = [ name = "black" version = "23.10.1" description = "The uncompromising code formatter." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -323,7 +270,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "boxsdk" version = "2.14.0" description = "Official Box Python SDK" -category = "main" optional = false python-versions = "*" files = [ @@ -348,7 +294,6 @@ test = ["bottle", "coverage (<5.0)", "jsonpatch", "mock (>=2.0.0,<4.0.0)", "pyco name = "cachetools" version = "5.3.2" description = "Extensible memoizing collections and decorators" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -360,7 +305,6 @@ files = [ name = "celery" version = "5.3.4" description = "Distributed Task Queue." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -416,7 +360,6 @@ zstd = ["zstandard (==0.21.0)"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -428,7 +371,6 @@ files = [ name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -493,7 +435,6 @@ pycparser = "*" name = "channels" version = "4.0.0" description = "Brings async, event-driven capabilities to Django 3.2 and up." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -513,7 +454,6 @@ tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", " name = "channels-redis" version = "4.1.0" description = "Redis-backed ASGI channel layer implementation" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -535,7 +475,6 @@ tests = ["async-timeout", "cryptography (>=1.3.0)", "pytest", "pytest-asyncio", name = "charset-normalizer" version = "3.3.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -635,7 +574,6 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -650,7 +588,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "click-didyoumean" version = "0.3.0" description = "Enables git-like *did-you-mean* feature in click" -category = "main" optional = false python-versions = ">=3.6.2,<4.0.0" files = [ @@ -665,7 +602,6 @@ click = ">=7" name = "click-plugins" version = "1.1.1" description = "An extension module for click to enable registering CLI commands via setuptools entry-points." -category = "main" optional = false python-versions = "*" files = [ @@ -683,7 +619,6 @@ dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] name = "click-repl" version = "0.3.0" description = "REPL plugin for Click" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -702,7 +637,6 @@ testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] name = "cloudpickle" version = "3.0.0" description = "Pickler class to extend the standard pickle.Pickler functionality" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -714,7 +648,6 @@ files = [ name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -726,7 +659,6 @@ files = [ name = "constantly" version = "15.1.0" description = "Symbolic constants in Python" -category = "main" optional = false python-versions = "*" files = [ @@ -738,7 +670,6 @@ files = [ name = "coverage" version = "7.3.2" description = "Code coverage measurement for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -803,7 +734,6 @@ toml = ["tomli"] name = "cryptography" version = "41.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -849,7 +779,6 @@ test-randomorder = ["pytest-randomly"] name = "cssselect2" version = "0.7.0" description = "CSS selectors for Python ElementTree" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -865,25 +794,10 @@ webencodings = "*" doc = ["sphinx", "sphinx_rtd_theme"] test = ["flake8", "isort", "pytest"] -[[package]] -name = "curlify" -version = "2.2.1" -description = "Library to convert python requests object to curl command." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "curlify-2.2.1.tar.gz", hash = "sha256:0d3f02e7235faf952de8ef45ef469845196d30632d5838bcd5aee217726ddd6d"}, -] - -[package.dependencies] -requests = "*" - [[package]] name = "daphne" version = "4.0.0" description = "Django ASGI (HTTP/WebSocket) server" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -903,7 +817,6 @@ tests = ["django", "hypothesis", "pytest", "pytest-asyncio"] name = "debugpy" version = "1.8.0" description = "An implementation of the Debug Adapter Protocol for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -931,7 +844,6 @@ files = [ name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -943,7 +855,6 @@ files = [ name = "dill" version = "0.3.7" description = "serialize all of Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -958,7 +869,6 @@ graph = ["objgraph (>=1.7.2)"] name = "django" version = "4.2.6" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -979,7 +889,6 @@ bcrypt = ["bcrypt"] name = "django-appconf" version = "1.0.5" description = "A helper class for handling configuration defaults of packaged apps gracefully." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -994,7 +903,6 @@ django = "*" name = "django-bootstrap3" version = "23.4" description = "Bootstrap 3 for Django" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1010,7 +918,6 @@ django = ">=3.2" name = "django-classy-tags" version = "4.1.0" description = "Class based template tags for Django" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1025,7 +932,6 @@ django = ">=3.2" name = "django-cms" version = "3.11.4" description = "Lean enterprise content management powered by Django." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1046,7 +952,6 @@ packaging = "*" name = "django-filer" version = "2.2.6" description = "A file management application for django that makes handling of files and images a breeze." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1065,7 +970,6 @@ Unidecode = ">=0.04,<1.2" name = "django-formtools" version = "2.2" description = "A set of high-level abstractions for Django forms" -category = "main" optional = false python-versions = "*" files = [ @@ -1080,7 +984,6 @@ Django = ">=1.11" name = "django-haystack" version = "3.2.1" description = "Pluggable search for Django." -category = "main" optional = false python-versions = "*" files = [ @@ -1097,7 +1000,6 @@ elasticsearch = ["elasticsearch (>=5,<8)"] name = "django-impersonate" version = "1.9.1" description = "Django app to allow superusers to impersonate other users." -category = "main" optional = false python-versions = "*" files = [ @@ -1108,7 +1010,6 @@ files = [ name = "django-ipware" version = "1.2.0" description = "A Django utility application that returns client's real IP address" -category = "main" optional = false python-versions = "*" files = [ @@ -1119,7 +1020,6 @@ files = [ name = "django-js-asset" version = "2.1.0" description = "script tag with additional attributes for django.forms.Media" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1137,7 +1037,6 @@ tests = ["coverage"] name = "django-mptt" version = "0.15.0" description = "Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances." -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -1155,7 +1054,6 @@ tests = ["coverage[toml]", "mock-django"] name = "django-polymorphic" version = "3.1.0" description = "Seamless polymorphic inheritance for Django models" -category = "main" optional = false python-versions = "*" files = [ @@ -1170,7 +1068,6 @@ Django = ">=2.1" name = "django-recaptcha" version = "3.0.0" description = "Django recaptcha form field/widget app." -category = "main" optional = false python-versions = "*" files = [ @@ -1185,7 +1082,6 @@ django = "*" name = "django-recaptcha2" version = "1.4.1" description = "Django reCaptcha v2 field/widget" -category = "main" optional = false python-versions = "*" files = [] @@ -1204,7 +1100,6 @@ resolved_reference = "1b7942af0032f1e6ba368e0028c48e3c7cfa0588" name = "django-sekizai" version = "2.0.0" description = "Django Sekizai" -category = "main" optional = false python-versions = "*" files = [ @@ -1220,7 +1115,6 @@ django-classy-tags = ">=1" name = "django-select2" version = "6.3.1" description = "Select2 option fields for Django." -category = "main" optional = false python-versions = "*" files = [ @@ -1235,7 +1129,6 @@ django-appconf = ">=0.6.0" name = "django-termsandconditions" version = "2.0.12" description = "Django app that enables users to accept terms and conditions of a site." -category = "main" optional = false python-versions = ">=3.7.2,<4.0" files = [ @@ -1250,7 +1143,6 @@ Django = ">2.2" name = "django-treebeard" version = "4.7" description = "Efficient tree implementations for Django" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1265,7 +1157,6 @@ Django = ">=3.2" name = "djangocms-admin-style" version = "3.2.6" description = "Adds pretty CSS styles for the django CMS admin interface." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1280,7 +1171,6 @@ Django = "*" name = "djangocms-attributes-field" version = "3.0.0" description = "Adds attributes to Django models." -category = "main" optional = false python-versions = "*" files = [ @@ -1295,7 +1185,6 @@ django-cms = ">=3.7" name = "djangocms-cascade" version = "0.16.3" description = "Collection of extendible plugins for django-CMS to create and edit widgets in a simple manner" -category = "main" optional = false python-versions = "*" files = [] @@ -1317,7 +1206,6 @@ resolved_reference = "9e9f9e3088a0fcfdc1b27ad7e08b68390aa6a570" name = "djangocms-file" version = "3.0.0" description = "Adds file plugin to django CMS" -category = "main" optional = false python-versions = "*" files = [ @@ -1334,7 +1222,6 @@ djangocms-attributes-field = ">=1" name = "djangocms-forms-maintained" version = "202206141440" description = "The easiest and most flexible Django CMS Form builder w/ ReCaptcha v2 support!" -category = "main" optional = false python-versions = "*" files = [] @@ -1361,7 +1248,6 @@ resolved_reference = "63ead9288c2ea65139124698bffc0ad01d182afa" name = "djangocms-googlemap" version = "2.0.0" description = "Adds Google Maps plugins to django CMS." -category = "main" optional = false python-versions = "*" files = [ @@ -1377,7 +1263,6 @@ django-filer = ">=1.7" name = "djangocms-picture" version = "3.0.0" description = "Adds an image plugin to django CMS" -category = "main" optional = false python-versions = "*" files = [ @@ -1395,7 +1280,6 @@ easy-thumbnails = "*" name = "djangocms-snippet" version = "3.0.0" description = "Adds snippet plugin to django CMS." -category = "main" optional = false python-versions = "*" files = [ @@ -1410,7 +1294,6 @@ django-cms = ">=3.7" name = "djangocms-style" version = "3.0.0" description = "Adds style plugin to django CMS" -category = "main" optional = false python-versions = "*" files = [ @@ -1426,7 +1309,6 @@ djangocms-attributes-field = ">=1" name = "djangocms-text-ckeditor" version = "5.1.4" description = "Text Plugin for django CMS with CKEditor support" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1444,7 +1326,6 @@ Pillow = "*" name = "djangocms-video" version = "3.0.0" description = "Adds video plugin to django CMS." -category = "main" optional = false python-versions = "*" files = [ @@ -1461,7 +1342,6 @@ djangocms-attributes-field = ">=1" name = "dropbox" version = "10.6.0" description = "Official Dropbox API Client" -category = "main" optional = false python-versions = "*" files = [ @@ -1478,7 +1358,6 @@ six = ">=1.12.0" name = "easy-thumbnails" version = "2.8.5" description = "Easy thumbnails for Django" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1499,7 +1378,6 @@ svg = ["reportlab", "svglib"] name = "elasticsearch" version = "7.17.9" description = "Python client for Elasticsearch" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" files = [ @@ -1521,7 +1399,6 @@ requests = ["requests (>=2.4.0,<3.0.0)"] name = "elasticsearch-dsl" version = "7.4.1" description = "Python client for Elasticsearch" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1541,7 +1418,6 @@ develop = ["coverage (<5.0.0)", "mock", "pytest (>=3.0.0)", "pytest-cov", "pytes name = "et-xmlfile" version = "1.1.0" description = "An implementation of lxml.xmlfile for the standard library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1553,7 +1429,6 @@ files = [ name = "executing" version = "2.0.0" description = "Get the currently executing AST node of a frame, and other information" -category = "main" optional = false python-versions = "*" files = [ @@ -1568,7 +1443,6 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth name = "exifread" version = "2.3.2" description = "Read Exif metadata from tiff and jpeg files." -category = "main" optional = false python-versions = "*" files = [ @@ -1576,22 +1450,10 @@ files = [ {file = "ExifRead-2.3.2.tar.gz", hash = "sha256:a0f74af5040168d3883bbc980efe26d06c89f026dc86ba28eb34107662d51766"}, ] -[[package]] -name = "future" -version = "0.18.3" -description = "Clean single-source support for Python 3 and 2" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, -] - [[package]] name = "google-api-core" version = "2.12.0" description = "Google API client core library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1614,7 +1476,6 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] name = "google-api-python-client" version = "2.105.0" description = "Google API Client Library for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1623,7 +1484,7 @@ files = [ ] [package.dependencies] -google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0.dev0" +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" google-auth = ">=1.19.0,<3.0.0.dev0" google-auth-httplib2 = ">=0.1.0" httplib2 = ">=0.15.0,<1.dev0" @@ -1633,7 +1494,6 @@ uritemplate = ">=3.0.1,<5" name = "google-auth" version = "2.23.3" description = "Google Authentication Library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1657,7 +1517,6 @@ requests = ["requests (>=2.20.0,<3.0.0.dev0)"] name = "google-auth-httplib2" version = "0.1.1" description = "Google Authentication Library: httplib2 transport" -category = "main" optional = false python-versions = "*" files = [ @@ -1673,7 +1532,6 @@ httplib2 = ">=0.19.0" name = "google-auth-oauthlib" version = "0.4.6" description = "Google Authentication Library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1692,7 +1550,6 @@ tool = ["click (>=6.0.0)"] name = "googleapis-common-protos" version = "1.61.0" description = "Common protobufs used in Google APIs" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1710,7 +1567,6 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] name = "hashids" version = "1.3.1" description = "Implements the hashids algorithm in python. For more information, visit http://hashids.org/" -category = "main" optional = false python-versions = ">=2.7" files = [ @@ -1725,7 +1581,6 @@ test = ["pytest (>=2.1.0)"] name = "html5lib" version = "1.1" description = "HTML parser based on the WHATWG HTML specification" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1747,7 +1602,6 @@ lxml = ["lxml"] name = "httplib2" version = "0.22.0" description = "A comprehensive HTTP client library." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1762,7 +1616,6 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 name = "hyperlink" version = "21.0.0" description = "A featureful, immutable, and correct URL for Python." -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1777,7 +1630,6 @@ idna = ">=2.5" name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1789,7 +1641,6 @@ files = [ name = "importlib-resources" version = "5.13.0" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1805,7 +1656,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", name = "incremental" version = "22.10.0" description = "\"A small library that versions your Python projects.\"" -category = "main" optional = false python-versions = "*" files = [ @@ -1821,7 +1671,6 @@ scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1833,7 +1682,6 @@ files = [ name = "ipdb" version = "0.13.13" description = "IPython-enabled pdb" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1849,7 +1697,6 @@ ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} name = "ipython" version = "8.16.1" description = "IPython: Productive Interactive Computing" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -1884,11 +1731,24 @@ qtconsole = ["qtconsole"] test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = "*" +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[package.dependencies] +six = "*" + [[package]] name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." -category = "main" optional = false python-versions = ">=3.8.0" files = [ @@ -1906,7 +1766,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jedi" version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1922,29 +1781,10 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - [[package]] name = "jsonfield" version = "3.1.0" description = "A reusable Django field that allows you to store validated JSON in your model." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1959,7 +1799,6 @@ Django = ">=2.2" name = "jsonpickle" version = "3.0.2" description = "Python library for serializing any arbitrary object graph into JSON" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1972,11 +1811,46 @@ docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"] testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"] testing-libs = ["simplejson", "ujson"] +[[package]] +name = "jsonschema" +version = "4.17.3" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, +] + +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-spec" +version = "0.1.6" +description = "JSONSchema Spec with object-oriented paths" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "jsonschema_spec-0.1.6-py3-none-any.whl", hash = "sha256:f2206d18c89d1824c1f775ba14ed039743b41a9167bd2c5bdb774b66b3ca0bbf"}, + {file = "jsonschema_spec-0.1.6.tar.gz", hash = "sha256:90215863b56e212086641956b20127ccbf6d8a3a38343dad01d6a74d19482f76"}, +] + +[package.dependencies] +jsonschema = ">=4.0.0,<4.18.0" +pathable = ">=0.4.1,<0.5.0" +PyYAML = ">=5.1" +requests = ">=2.31.0,<3.0.0" + [[package]] name = "kombu" version = "5.3.2" description = "Messaging library for Python." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2009,7 +1883,6 @@ zookeeper = ["kazoo (>=2.8.0)"] name = "lazy-object-proxy" version = "1.9.0" description = "A fast and thorough lazy object proxy." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2055,7 +1928,6 @@ files = [ name = "lxml" version = "4.9.3" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ @@ -2161,69 +2033,77 @@ source = ["Cython (>=0.29.35)"] [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] name = "matplotlib-inline" version = "0.1.6" description = "Inline Matplotlib backend for Jupyter" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2238,7 +2118,6 @@ traitlets = "*" name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2250,7 +2129,6 @@ files = [ name = "mock" version = "4.0.3" description = "Rolling backport of unittest.mock for all Pythons" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2263,11 +2141,21 @@ build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest (<5.4)", "pytest-cov"] +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + [[package]] name = "msgpack" version = "1.0.7" description = "MessagePack serializer" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2333,7 +2221,6 @@ files = [ name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2345,7 +2232,6 @@ files = [ name = "networkx" version = "3.2.1" description = "Python package for creating and manipulating graphs and networks" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2364,7 +2250,6 @@ test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] name = "oauth2client" version = "4.1.3" description = "OAuth 2.0 client library" -category = "main" optional = false python-versions = "*" files = [ @@ -2383,7 +2268,6 @@ six = ">=1.6.1" name = "oauthlib" version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2400,18 +2284,84 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] name = "olefile" version = "0.46" description = "Python package to parse, read and write Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office)" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "olefile-0.46.zip", hash = "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"}, ] +[[package]] +name = "openapi-core" +version = "0.16.0" +description = "client-side and server-side support for the OpenAPI Specification v3" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "openapi-core-0.16.0.tar.gz", hash = "sha256:5db8fa034e5c262de865cab5f2344995c52f1ba0386182c0be584d02f0282c6a"}, + {file = "openapi_core-0.16.0-py3-none-any.whl", hash = "sha256:4331f528f5a74c7a3963f37b2ad73c54e3dd477276354fd6b7188d2352fd7e8e"}, +] + +[package.dependencies] +isodate = "*" +jsonschema-spec = ">=0.1.1,<0.2.0" +more-itertools = "*" +openapi-schema-validator = ">=0.3.0,<0.4.0" +openapi-spec-validator = ">=0.5.0,<0.6.0" +parse = "*" +pathable = ">=0.4.0,<0.5.0" +typing-extensions = ">=4.3.0,<5.0.0" +werkzeug = "*" + +[package.extras] +django = ["django (>=3.0)"] +falcon = ["falcon (>=3.0)"] +flask = ["flask"] +requests = ["requests"] + +[[package]] +name = "openapi-schema-validator" +version = "0.3.4" +description = "OpenAPI schema validation for Python" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "openapi-schema-validator-0.3.4.tar.gz", hash = "sha256:7cf27585dd7970b7257cefe48e1a3a10d4e34421831bdb472d96967433bc27bd"}, + {file = "openapi_schema_validator-0.3.4-py3-none-any.whl", hash = "sha256:34fbd14b7501abe25e64d7b4624a9db02cde1a578d285b3da6f34b290cdf0b3a"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +jsonschema = ">=4.0.0,<5.0.0" + +[package.extras] +isodate = ["isodate"] +rfc3339-validator = ["rfc3339-validator"] +strict-rfc3339 = ["strict-rfc3339"] + +[[package]] +name = "openapi-spec-validator" +version = "0.5.4" +description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "openapi_spec_validator-0.5.4-py3-none-any.whl", hash = "sha256:96be4258fdccc89d3da094738e19d56b94956914b93a22de795b9dd220cb4c7c"}, + {file = "openapi_spec_validator-0.5.4.tar.gz", hash = "sha256:68654e81cc56c71392dba31bf55d11e1c03c99458bebcb0018959a7134e104da"}, +] + +[package.dependencies] +jsonschema = ">=4.0.0,<5.0.0" +jsonschema-spec = ">=0.1.1,<0.2.0" +lazy-object-proxy = ">=1.7.1,<2.0.0" +openapi-schema-validator = ">=0.3.2,<0.5" + +[package.extras] +requests = ["requests"] + [[package]] name = "openpyxl" version = "3.1.2" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2426,7 +2376,6 @@ et-xmlfile = "*" name = "opf-fido" version = "1.4.1" description = "Format Identification for Digital Objects (FIDO)." -category = "main" optional = false python-versions = "*" files = [ @@ -2447,7 +2396,6 @@ testing = ["pytest"] name = "packaging" version = "23.2" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2455,11 +2403,21 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "parse" +version = "1.20.1" +description = "parse() is the opposite of format()" +optional = false +python-versions = "*" +files = [ + {file = "parse-1.20.1-py2.py3-none-any.whl", hash = "sha256:76ddd5214255ae711db4c512be636151fbabaa948c6f30115aecc440422ca82c"}, + {file = "parse-1.20.1.tar.gz", hash = "sha256:09002ca350ad42e76629995f71f7b518670bcf93548bdde3684fd55d2be51975"}, +] + [[package]] name = "parso" version = "0.8.3" description = "A Python Parser" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2471,11 +2429,21 @@ files = [ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] +[[package]] +name = "pathable" +version = "0.4.3" +description = "Object-oriented paths" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14"}, + {file = "pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab"}, +] + [[package]] name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2483,22 +2451,10 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] -[[package]] -name = "petname" -version = "2.6" -description = "Generate human-readable, random object names" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "petname-2.6.tar.gz", hash = "sha256:981c31ef772356a373640d1bb7c67c102e0159eda14578c67a1c99d5b34c9e4c"}, -] - [[package]] name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." -category = "main" optional = false python-versions = "*" files = [ @@ -2513,7 +2469,6 @@ ptyprocess = ">=0.5" name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "main" optional = false python-versions = "*" files = [ @@ -2525,7 +2480,6 @@ files = [ name = "pillow" version = "10.1.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2593,7 +2547,6 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa name = "platformdirs" version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2609,7 +2562,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2625,7 +2577,6 @@ testing = ["pytest", "pytest-benchmark"] name = "ply" version = "3.11" description = "Python Lex & Yacc" -category = "main" optional = false python-versions = "*" files = [ @@ -2637,7 +2588,6 @@ files = [ name = "prompt-toolkit" version = "3.0.39" description = "Library for building powerful interactive command lines in Python" -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -2652,7 +2602,6 @@ wcwidth = "*" name = "protobuf" version = "4.24.4" description = "" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2675,7 +2624,6 @@ files = [ name = "psycopg" version = "3.1.12" description = "PostgreSQL database adapter for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2699,7 +2647,6 @@ test = ["anyio (>=3.6.2,<4.0)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6 name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "main" optional = false python-versions = "*" files = [ @@ -2711,7 +2658,6 @@ files = [ name = "pure-eval" version = "0.2.2" description = "Safely evaluate AST nodes without side effects" -category = "main" optional = false python-versions = "*" files = [ @@ -2722,23 +2668,10 @@ files = [ [package.extras] tests = ["pytest"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - [[package]] name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2750,7 +2683,6 @@ files = [ name = "pyasn1-modules" version = "0.3.0" description = "A collection of ASN.1-based protocols modules" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2765,7 +2697,6 @@ pyasn1 = ">=0.4.6,<0.6.0" name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2777,7 +2708,6 @@ files = [ name = "pydantic" version = "2.5.0" description = "Data validation using Python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2797,7 +2727,6 @@ email = ["email-validator (>=2.0.0)"] name = "pydantic-core" version = "2.14.1" description = "" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2911,7 +2840,6 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2926,7 +2854,6 @@ plugins = ["importlib-metadata"] name = "pyjwt" version = "1.7.1" description = "JSON Web Token implementation in Python" -category = "main" optional = false python-versions = "*" files = [ @@ -2943,7 +2870,6 @@ test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner name = "pylint" version = "2.17.7" description = "python code static checker" -category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -2968,7 +2894,6 @@ testutils = ["gitpython (>3)"] name = "pylint-django" version = "2.5.5" description = "A Pylint plugin to help Pylint understand the Django web framework" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -2987,7 +2912,6 @@ with-django = ["Django (>=2.2)"] name = "pylint-plugin-utils" version = "0.8.2" description = "Utilities and helpers for writing Pylint plugins" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -3002,7 +2926,6 @@ pylint = ">=1.7" name = "pymemcache" version = "4.0.0" description = "A comprehensive, fast, pure Python memcached client" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3014,7 +2937,6 @@ files = [ name = "pymongo" version = "3.13.0" description = "Python driver for MongoDB <http://www.mongodb.org>" -category = "main" optional = false python-versions = "*" files = [ @@ -3143,7 +3065,6 @@ zstd = ["zstandard"] name = "pyopenssl" version = "23.3.0" description = "Python wrapper module around the OpenSSL library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3162,7 +3083,6 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "pyparsing" version = "3.1.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" optional = false python-versions = ">=3.6.8" files = [ @@ -3173,11 +3093,51 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyrsistent" +version = "0.20.0" +description = "Persistent/Functional/Immutable data structures" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b"}, + {file = "pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f"}, + {file = "pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7"}, + {file = "pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224"}, + {file = "pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656"}, + {file = "pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee"}, + {file = "pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d"}, + {file = "pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174"}, + {file = "pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d"}, + {file = "pyrsistent-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86"}, + {file = "pyrsistent-0.20.0-cp38-cp38-win32.whl", hash = "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423"}, + {file = "pyrsistent-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d"}, + {file = "pyrsistent-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca"}, + {file = "pyrsistent-0.20.0-cp39-cp39-win32.whl", hash = "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f"}, + {file = "pyrsistent-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf"}, + {file = "pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b"}, + {file = "pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4"}, +] + [[package]] name = "pytas" version = "1.7.0" description = "Python package for TAS integration" -category = "main" optional = false python-versions = "*" files = [] @@ -3196,7 +3156,6 @@ resolved_reference = "1e2e4e85b895cc381677a4c9d8e1fa7d8c7b86bc" name = "pytest" version = "7.4.3" description = "pytest: simple powerful testing with Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3217,7 +3176,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.14.0" description = "Pytest support for asyncio." -category = "main" optional = false python-versions = ">= 3.5" files = [ @@ -3235,7 +3193,6 @@ testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] name = "pytest-cov" version = "2.12.1" description = "Pytest plugin for measuring coverage." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3255,7 +3212,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-django" version = "4.5.2" description = "A Django plugin for pytest." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3274,7 +3230,6 @@ testing = ["Django", "django-configurations (>=2.0)"] name = "pytest-mock" version = "3.12.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3292,7 +3247,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -3303,26 +3257,10 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "python-dotenv" -version = "1.0.0" -description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - [[package]] name = "python-magic" version = "0.4.27" description = "File type identification using libmagic" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3334,7 +3272,6 @@ files = [ name = "pytz" version = "2023.3.post1" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -3346,7 +3283,6 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3355,6 +3291,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -3362,8 +3299,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -3380,6 +3325,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -3387,6 +3333,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -3396,7 +3343,6 @@ files = [ name = "redis" version = "5.0.1" description = "Python client for Redis database and key-value store" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3415,7 +3361,6 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "reportlab" version = "4.0.6" description = "The Reportlab Toolkit" -category = "main" optional = false python-versions = ">=3.7,<4" files = [ @@ -3435,7 +3380,6 @@ renderpm = ["rl-renderPM (>=4.0.3,<4.1)"] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3457,7 +3401,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "requests-mock" version = "1.11.0" description = "Mock out responses from the requests package" -category = "main" optional = false python-versions = "*" files = [ @@ -3477,7 +3420,6 @@ test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "tes name = "requests-oauthlib" version = "1.3.1" description = "OAuthlib authentication support for Requests." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3496,7 +3438,6 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] name = "requests-toolbelt" version = "0.10.1" description = "A utility belt for advanced users of python-requests" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3511,7 +3452,6 @@ requests = ">=2.0.1,<3.0.0" name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -3526,7 +3466,6 @@ pyasn1 = ">=0.1.3" name = "rt" version = "1.0.13" description = "Python interface to Request Tracker API" -category = "main" optional = false python-versions = "*" files = [ @@ -3541,7 +3480,6 @@ six = "*" name = "service-identity" version = "23.1.0" description = "Service identity verification for pyOpenSSL & cryptography." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3566,7 +3504,6 @@ tests = ["coverage[toml] (>=5.0.2)", "pytest"] name = "setuptools" version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3583,7 +3520,6 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3595,7 +3531,6 @@ files = [ name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3607,7 +3542,6 @@ files = [ name = "sqlparse" version = "0.4.4" description = "A non-validating SQL parser." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3624,7 +3558,6 @@ test = ["pytest", "pytest-cov"] name = "stack-data" version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" -category = "main" optional = false python-versions = "*" files = [ @@ -3644,7 +3577,6 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] name = "stone" version = "3.3.1" description = "Stone is an interface description language (IDL) for APIs." -category = "main" optional = false python-versions = "*" files = [ @@ -3661,7 +3593,6 @@ six = ">=1.12.0" name = "svglib" version = "1.5.1" description = "A pure-Python library for reading and converting SVG" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3678,7 +3609,6 @@ tinycss2 = ">=0.6.0" name = "tablib" version = "3.5.0" description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3700,11 +3630,37 @@ xls = ["xlrd", "xlwt"] xlsx = ["openpyxl (>=2.6.0)"] yaml = ["pyyaml"] +[[package]] +name = "tapipy" +version = "1.6.1" +description = "Python lib for interacting with an instance of the Tapis API Framework" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "tapipy-1.6.1-py3-none-any.whl", hash = "sha256:4898ed0ba11a56fc4405d405e3a8f8447af8e08c26a403ff0af1c870ad9a41eb"}, + {file = "tapipy-1.6.1.tar.gz", hash = "sha256:2d5a23acca2be59f14a75ae56a746a3959ee56d50d6c8edfd39f3932b448a51c"}, +] + +[package.dependencies] +atomicwrites = ">=1.4.0,<2.0.0" +certifi = ">=2020.11.8" +cloudpickle = ">=1.6.0" +cryptography = ">=3.3.2" +jsonschema = ">=3.2.0" +openapi_core = "0.16.0" +openapi_spec_validator = ">=0.5.0,<0.6.0" +PyJWT = ">=1.7.1" +python_dateutil = ">=2.5.3,<3.0.0" +pyyaml = ">=5.4" +requests = ">=2.20.0,<3.0.0" +setuptools = ">=21.0.0" +six = ">=1.10,<2.0" +urllib3 = ">=1.26.5,<2.0.0" + [[package]] name = "tinycss2" version = "1.2.1" description = "A tiny CSS parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3723,7 +3679,6 @@ test = ["flake8", "isort", "pytest"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3735,7 +3690,6 @@ files = [ name = "tomlkit" version = "0.12.1" description = "Style preserving TOML library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3747,7 +3701,6 @@ files = [ name = "traitlets" version = "5.12.0" description = "Traitlets Python configuration system" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3763,7 +3716,6 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.6.0)", "pre-commit", "pytest (>=7.0, name = "twisted" version = "23.8.0" description = "An asynchronous networking framework written in Python" -category = "main" optional = false python-versions = ">=3.7.1" files = [ @@ -3804,7 +3756,6 @@ windows-platform = ["pywin32 (!=226)", "pywin32 (!=226)", "twisted[all-non-platf name = "twisted-iocpsupport" version = "1.0.4" description = "An extension for use in the twisted I/O Completion Ports reactor." -category = "main" optional = false python-versions = "*" files = [ @@ -3833,7 +3784,6 @@ files = [ name = "txaio" version = "23.1.1" description = "Compatibility API between asyncio/Twisted/Trollius" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3846,23 +3796,10 @@ all = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] dev = ["pep8 (>=1.6.2)", "pyenchant (>=1.6.6)", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "sphinx (>=1.2.3)", "sphinx-rtd-theme (>=0.1.9)", "sphinxcontrib-spelling (>=2.1.2)", "tox (>=2.1.1)", "tox-gh-actions (>=2.2.0)", "twine (>=1.6.5)", "wheel"] twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] -[[package]] -name = "types-python-dateutil" -version = "2.8.19.14" -description = "Typing stubs for python-dateutil" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, - {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, -] - [[package]] name = "typing-extensions" version = "4.8.0" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3874,7 +3811,6 @@ files = [ name = "tzdata" version = "2023.3" description = "Provider of IANA time zone data" -category = "main" optional = false python-versions = ">=2" files = [ @@ -3886,7 +3822,6 @@ files = [ name = "unidecode" version = "1.1.2" description = "ASCII transliterations of Unicode text" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3898,7 +3833,6 @@ files = [ name = "uritemplate" version = "4.1.1" description = "Implementation of RFC 6570 URI Templates" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3910,7 +3844,6 @@ files = [ name = "urllib3" version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -3927,7 +3860,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "uwsgi" version = "2.0.22" description = "The uWSGI server" -category = "main" optional = false python-versions = "*" files = [ @@ -3938,7 +3870,6 @@ files = [ name = "uwsgitop" version = "0.11" description = "uWSGI top-like interface" -category = "main" optional = false python-versions = "*" files = [ @@ -3949,7 +3880,6 @@ files = [ name = "vine" version = "5.0.0" description = "Promises, promises, promises." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3961,7 +3891,6 @@ files = [ name = "wcwidth" version = "0.2.8" description = "Measures the displayed width of unicode strings in a terminal" -category = "main" optional = false python-versions = "*" files = [ @@ -3973,7 +3902,6 @@ files = [ name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" -category = "main" optional = false python-versions = "*" files = [ @@ -3982,27 +3910,26 @@ files = [ ] [[package]] -name = "websocket-client" -version = "1.6.4" -description = "WebSocket client for Python with low level API options" -category = "main" +name = "werkzeug" +version = "3.0.1" +description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"}, - {file = "websocket_client-1.6.4-py3-none-any.whl", hash = "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24"}, + {file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, + {file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, ] +[package.dependencies] +MarkupSafe = ">=2.1.1" + [package.extras] -docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] +watchdog = ["watchdog (>=2.3)"] [[package]] name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -4087,7 +4014,6 @@ files = [ name = "zope-interface" version = "6.1" description = "Interfaces for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -4140,4 +4066,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "520a4dd93287162675b6fb3d8706fc0034342db00718f0345e115313b3bd5bde" +content-hash = "afa925fb8ac88226c3b5c8be707883fd2ddaf484aa82a1d9f70c2ca07b6b25ce" diff --git a/pyproject.toml b/pyproject.toml index 6ef6a610da..76b92b06f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ authors = ["DesignSafe-CI <designsafe-ci@tacc.utexas.edu>"] [tool.poetry.dependencies] python = "^3.11" -agavepy = "1.0.0a12" pytas = {git = "https://bitbucket.org/taccaci/pytas.git", tag = "v1.7.0"} attrdict = { git = "https://github.com/DesignSafe-CI/AttrDict", rev = "83b779ee82d5b0e33be695d398162b8f2430ff33" } Django = "^4.2" @@ -75,6 +74,7 @@ django-select2 = "6.3.1" djangocms-admin-style = "~3.2.6" pydantic = "^2.5.0" networkx = "^3.2.1" +tapipy = "^1.6.1" [build-system] requires = ["poetry-core>=1.0.0"] From 2771edc96f9fc972fe344b623facc5d264129993 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 15:21:00 -0600 Subject: [PATCH 02/20] add docstrings to test methods --- designsafe/apps/auth/unit_test.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/designsafe/apps/auth/unit_test.py b/designsafe/apps/auth/unit_test.py index ad86c7b049..7d84b24f8b 100644 --- a/designsafe/apps/auth/unit_test.py +++ b/designsafe/apps/auth/unit_test.py @@ -1,20 +1,23 @@ + +import pytest from django.test import TransactionTestCase, override_settings from django.contrib.auth import get_user_model from mock import patch, MagicMock -from portal.apps.auth.backends import TapisOAuthBackend from requests import Response -from portal.apps.auth.views import launch_setup_checks -import pytest +from designsafe.apps.auth.backends import TapisOAuthBackend +# from designsafe.apps.auth.views import launch_setup_checks +from tapipy.tapis import TapisResult + pytestmark = pytest.mark.django_db -def test_launch_setup_checks(mocker, regular_user, settings): - mocker.patch("portal.apps.auth.views.new_user_setup_check") - mock_execute = mocker.patch("portal.apps.auth.views.execute_setup_steps") - regular_user.profile.setup_complete = False - launch_setup_checks(regular_user) - mock_execute.apply_async.assert_called_with(args=["username"]) +# def test_launch_setup_checks(mocker, regular_user, settings): +# mocker.patch("designsafe.apps.auth.views.new_user_setup_check") +# mock_execute = mocker.patch("designsafe.apps.auth.views.execute_setup_steps") +# regular_user.profile.setup_complete = False +# launch_setup_checks(regular_user) +# mock_execute.apply_async.assert_called_with(args=["username"]) class TestTapisOAuthBackend(TransactionTestCase): @@ -23,12 +26,12 @@ def setUp(self): self.backend = TapisOAuthBackend() self.mock_response = MagicMock(autospec=Response) self.mock_requests_patcher = patch( - "portal.apps.auth.backends.requests.get", return_value=self.mock_response + "designsafe.apps.auth.backends.Tapis.authenticator.get_userinfo", return_value=self.mock_response ) self.mock_requests = self.mock_requests_patcher.start() self.mock_user_data_patcher = patch( - "portal.apps.auth.backends.get_user_data", + "designsafe.apps.auth.backends.get_user_data", return_value={ "username": "testuser", "firstName": "test", @@ -64,7 +67,7 @@ def test_bad_response_status(self): @override_settings(PORTAL_USER_ACCOUNT_SETUP_STEPS=[]) def test_new_user(self): - # Test that a new user is created and returned + """Test that a new user is created and returned""" self.mock_response.json.return_value = { "status": "success", "result": {"username": "testuser"}, @@ -74,8 +77,7 @@ def test_new_user(self): @override_settings(PORTAL_USER_ACCOUNT_SETUP_STEPS=[]) def test_update_existing_user(self): - # Test that an existing user's information is - # updated with from info from the Tapis backend response + """Test that an existing user's information is updated with from info from the Tapis backend response""" # Create a pre-existing user with the same username user = get_user_model().objects.create_user( From 378c7cd123a9ec675ec90898721d4597f0f8cba2 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 15:21:21 -0600 Subject: [PATCH 03/20] update service account to use Tapis --- designsafe/apps/api/agave/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/designsafe/apps/api/agave/__init__.py b/designsafe/apps/api/agave/__init__.py index 8e05b4d037..bff5fddd9d 100644 --- a/designsafe/apps/api/agave/__init__.py +++ b/designsafe/apps/api/agave/__init__.py @@ -5,13 +5,11 @@ """ import logging import requests -from agavepy.agave import Agave, load_resource +from tapipy.tapis import Tapis from django.conf import settings logger = logging.getLogger(__name__) -AGAVE_RESOURCES = load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) - def get_service_account_client(): """Return service account agave client. @@ -23,15 +21,17 @@ def get_service_account_client(): There might be some issues because of permissionas, but it might be a bit safer.""" - return Agave(api_server=settings.AGAVE_TENANT_BASEURL, - token=settings.AGAVE_SUPER_TOKEN, - resources=AGAVE_RESOURCES) + return Tapis( + base_url=settings.TAPIS_TENANT_BASEURL, + access_token=settings.TAPIS_ADMIN_JWT) + +# TODOV3: Remove sandbox account code def get_sandbox_service_account_client(): """Return sandbox service account""" - return Agave(api_server=settings.AGAVE_SANDBOX_TENANT_BASEURL, - token=settings.AGAVE_SANDBOX_SUPER_TOKEN, - resources=AGAVE_RESOURCES) + return Tapis( + base_url=settings.TAPIS_TENANT_BASEURL, + access_token=settings.TAPIS_ADMIN_JWT) def service_account(): """Return prod or sandbox service client depending on setting.AGAVE_USE_SANDBOX""" From 73806dc18a01d88eb6f42f03229c81d9399cbf22 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 15:22:15 -0600 Subject: [PATCH 04/20] remove future imports --- designsafe/apps/api/search/searchmanager/cms.py | 3 +-- designsafe/apps/data/models/elasticsearch.py | 11 +++++------ designsafe/apps/projects/managers/base.py | 3 +-- designsafe/apps/projects/managers/datacite.py | 1 - designsafe/apps/projects/models/elasticsearch.py | 11 ++++------- designsafe/apps/workspace/models/elasticsearch.py | 8 ++++---- designsafe/libs/elasticsearch/docs.py | 2 +- designsafe/libs/elasticsearch/docs/base.py | 10 +++++----- designsafe/libs/elasticsearch/docs/files.py | 14 +++++++------- .../libs/elasticsearch/docs/publication_legacy.py | 10 +++++----- designsafe/libs/elasticsearch/docs/publications.py | 6 +++--- designsafe/libs/elasticsearch/indices.py | 2 +- designsafe/libs/elasticsearch/utils.py | 8 ++++---- 13 files changed, 41 insertions(+), 48 deletions(-) diff --git a/designsafe/apps/api/search/searchmanager/cms.py b/designsafe/apps/api/search/searchmanager/cms.py index 515c5b18ad..410c85745d 100644 --- a/designsafe/apps/api/search/searchmanager/cms.py +++ b/designsafe/apps/api/search/searchmanager/cms.py @@ -5,12 +5,11 @@ import logging -from future.utils import python_2_unicode_compatible from elasticsearch_dsl import Q, Index from django.conf import settings from designsafe.apps.api.search.searchmanager.base import BaseSearchManager -@python_2_unicode_compatible + class CMSSearchManager(BaseSearchManager): """ Search manager handling CMS data. """ diff --git a/designsafe/apps/data/models/elasticsearch.py b/designsafe/apps/data/models/elasticsearch.py index fa17ebbb26..c9eec0af5b 100644 --- a/designsafe/apps/data/models/elasticsearch.py +++ b/designsafe/apps/data/models/elasticsearch.py @@ -1,5 +1,4 @@ -from future.utils import python_2_unicode_compatible import logging import json import datetime @@ -152,7 +151,7 @@ class Index: class Meta: dynamic = MetaField('strict') -@python_2_unicode_compatible + class IndexedPublication(Document): revision = Long() revisionText = Text(analyzer='english') @@ -374,7 +373,7 @@ def from_id(cls, project_id, revision=None, using='default'): except Exception as e: raise e if res.hits.total.value > 1: - id_filter = Q('term', **{'_id': res[0].meta.id}) + id_filter = Q('term', **{'_id': res[0].meta.id}) # Delete all files indexed with the same system/path, except the first result delete_query = id_filter & ~id_filter cls.search(using=using).filter(delete_query).delete() @@ -399,7 +398,7 @@ def max_revision(cls, project_id, using='default'): class Index: name = settings.ES_INDICES['publications']['alias'] -@python_2_unicode_compatible + class IndexedCMSPage(Document): body = Text(analyzer='english') description = Text(analyzer='english') @@ -418,7 +417,7 @@ class Index: class Meta: dynamic = MetaField('strict') -@python_2_unicode_compatible + class IndexedPublicationLegacy(Document): startDate = Date() endDate = Date() @@ -513,7 +512,7 @@ def from_id(cls, project_id): except Exception as e: raise e if res.hits.total.value > 1: - id_filter = Q('term', **{'_id': res[0].meta.id}) + id_filter = Q('term', **{'_id': res[0].meta.id}) # Delete all files indexed with the same system/path, except the first result delete_query = id_filter & ~id_filter cls.search().filter(delete_query).delete() diff --git a/designsafe/apps/projects/managers/base.py b/designsafe/apps/projects/managers/base.py index 71dc5d72dd..e5074a7ad1 100644 --- a/designsafe/apps/projects/managers/base.py +++ b/designsafe/apps/projects/managers/base.py @@ -6,14 +6,13 @@ from __future__ import unicode_literals, absolute_import import logging import json -from future.utils import python_2_unicode_compatible from designsafe.apps.projects.models.utils import lookup_model as project_lookup_model LOG = logging.getLogger(__name__) -@python_2_unicode_compatible + class ProjectsManager(object): """Base projects manager.""" diff --git a/designsafe/apps/projects/managers/datacite.py b/designsafe/apps/projects/managers/datacite.py index 444fb485ce..611bd18268 100644 --- a/designsafe/apps/projects/managers/datacite.py +++ b/designsafe/apps/projects/managers/datacite.py @@ -7,7 +7,6 @@ from __future__ import unicode_literals, absolute_import import json import logging -# from future.utils import python_2_unicode_compatible from django.conf import settings import requests from requests import HTTPError diff --git a/designsafe/apps/projects/models/elasticsearch.py b/designsafe/apps/projects/models/elasticsearch.py index 1a0e7faf51..1c77416e4c 100644 --- a/designsafe/apps/projects/models/elasticsearch.py +++ b/designsafe/apps/projects/models/elasticsearch.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals, absolute_import -from future.utils import python_2_unicode_compatible import logging import json from django.conf import settings @@ -17,7 +16,6 @@ logger = logging.getLogger(__name__) #pylint: enable=invalid-name -@python_2_unicode_compatible class IndexedProject(Document): uuid = Text(fields={'_exact': Keyword()}) schemaId = Text(fields={'_exact': Keyword()}) @@ -59,11 +57,11 @@ class IndexedProject(Document): 'path': Text(fields={'_exact': Keyword()}) }, multi=True), - + # 'nhEventStart': Date(), # 'nhEventEnd': Date(), 'nhTypes': Text(fields={'_exact': Keyword()}), - 'nhType': Text(fields={'_exact': Keyword()}), + 'nhType': Text(fields={'_exact': Keyword()}), 'nhTypeOther': Text(fields={'_exact': Keyword()}), 'nhEvent': Text(fields={'_exact': Keyword()}), 'nhLocation': Text(fields={'_exact': Keyword()}), @@ -111,12 +109,11 @@ class IndexedProject(Document): }) class Index: - name = settings.ES_INDICES['projects']['alias'] + name = settings.ES_INDICES['projects']['alias'] class Meta: dynamic = MetaField('strict') -@python_2_unicode_compatible class IndexedEntity(Document): uuid = Text(fields={'_exact': Keyword()}) schemaId = Text(fields={'_exact': Keyword()}) @@ -142,6 +139,6 @@ class IndexedEntity(Document): class Index: name = settings.ES_INDICES['project_entities']['alias'] - + class Meta: dynamic = MetaField('strict') diff --git a/designsafe/apps/workspace/models/elasticsearch.py b/designsafe/apps/workspace/models/elasticsearch.py index a802e15f9f..f590c0989e 100644 --- a/designsafe/apps/workspace/models/elasticsearch.py +++ b/designsafe/apps/workspace/models/elasticsearch.py @@ -1,5 +1,5 @@ -from future.utils import python_2_unicode_compatible + import logging import json from django.conf import settings @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) #pylint: enable=invalid-name -@python_2_unicode_compatible + class IndexedApp(DocType): uuid = Text(fields={'_exact': Keyword()}) schemaId = Text(fields={'_exact': Keyword()}) @@ -42,7 +42,7 @@ class IndexedApp(DocType): }) class Index: - name = settings.ES_INDICES['project_entities']['alias'] - + name = settings.ES_INDICES['project_entities']['alias'] + class Meta: dynamic = MetaField('strict') diff --git a/designsafe/libs/elasticsearch/docs.py b/designsafe/libs/elasticsearch/docs.py index 2dad2365d0..13e426ce41 100644 --- a/designsafe/libs/elasticsearch/docs.py +++ b/designsafe/libs/elasticsearch/docs.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES different doc types. """ -from future.utils import python_2_unicode_compatible + import logging import json from django.conf import settings diff --git a/designsafe/libs/elasticsearch/docs/base.py b/designsafe/libs/elasticsearch/docs/base.py index c50d85f8f2..1b422da64a 100644 --- a/designsafe/libs/elasticsearch/docs/base.py +++ b/designsafe/libs/elasticsearch/docs/base.py @@ -1,8 +1,8 @@ -from future.utils import python_2_unicode_compatible + import logging -@python_2_unicode_compatible + class BaseESResource(object): """Base class used to represent an Elastic Search resource. @@ -14,7 +14,7 @@ class BaseESResource(object): """ def __init__(self, wrapped_doc=None, **kwargs): self._wrap(wrapped_doc, **kwargs) - + def to_dict(self): """Return wrapped doc as dict""" return self._wrapped.to_dict() @@ -32,7 +32,7 @@ def __getattr__(self, name): """ _wrapped = object.__getattribute__(self, '_wrapped') if _wrapped and hasattr(_wrapped, name): - return getattr(_wrapped, name) + return getattr(_wrapped, name) else: return object.__getattribute__(self, name) @@ -43,4 +43,4 @@ def __setattr__(self, name, value): return else: object.__setattr__(self, name, value) - return \ No newline at end of file + return diff --git a/designsafe/libs/elasticsearch/docs/files.py b/designsafe/libs/elasticsearch/docs/files.py index ffdb04e193..c61e585cea 100644 --- a/designsafe/libs/elasticsearch/docs/files.py +++ b/designsafe/libs/elasticsearch/docs/files.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES ``files`` doc type. """ -from future.utils import python_2_unicode_compatible + import logging import os from django.conf import settings @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) #pylint: enable=invalid-name -@python_2_unicode_compatible + class BaseESFile(BaseESResource): """Wrapper class for Elastic Search indexed file. @@ -61,25 +61,25 @@ def _index_cls(cls, reindex): def children(self, limit=100): """ - Yield all children (i.e. documents whose basePath matches self.path) by + Yield all children (i.e. documents whose basePath matches self.path) by paginating with the search_after api. """ res, search_after = self._index_cls(self._reindex).children( self.username, self.system, - self.path, + self.path, limit=limit) for doc in res: yield BaseESFile(self.username, wrapped_doc=doc) while not len(res) < limit: # If the number or results doesn't match the limit, we're done paginating. - # Retrieve the sort key from the last element then use + # Retrieve the sort key from the last element then use # search_after to get the next page of results res, search_after = self._index_cls(self._reindex).children( self.username, self.system, - self.path, + self.path, limit=limit, search_after=search_after) for doc in res: @@ -101,4 +101,4 @@ def delete(self): for child in children: if child.path != self.path: child.delete() - self._wrapped.delete() \ No newline at end of file + self._wrapped.delete() diff --git a/designsafe/libs/elasticsearch/docs/publication_legacy.py b/designsafe/libs/elasticsearch/docs/publication_legacy.py index 1255100ff2..80185aa833 100644 --- a/designsafe/libs/elasticsearch/docs/publication_legacy.py +++ b/designsafe/libs/elasticsearch/docs/publication_legacy.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES ``files`` doc type. """ -from future.utils import python_2_unicode_compatible + import logging import os import zipfile @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) #pylint: enable=invalid-name -@python_2_unicode_compatible + class BaseESPublicationLegacy(BaseESResource): """Wrapper class for Elastic Search indexed NEES publication. @@ -67,10 +67,10 @@ def to_file(self): publication_dict = self.to_dict() project_dict = {} - for key in ['deleted', 'description', 'endDate', 'facility', 'name', + for key in ['deleted', 'description', 'endDate', 'facility', 'name', 'organization', 'pis', 'project', 'projectPath', 'publications', 'startDate', 'system', 'title', 'sponsor']: - + if key in publication_dict: project_dict[key] = publication_dict[key] @@ -97,5 +97,5 @@ def to_file(self): 'experiments': experiments, 'project': project_dict }} - + return dict_obj diff --git a/designsafe/libs/elasticsearch/docs/publications.py b/designsafe/libs/elasticsearch/docs/publications.py index 396eb1a32d..e822f11487 100644 --- a/designsafe/libs/elasticsearch/docs/publications.py +++ b/designsafe/libs/elasticsearch/docs/publications.py @@ -5,7 +5,7 @@ """ import logging -from future.utils import python_2_unicode_compatible + from designsafe.apps.data.models.elasticsearch import IndexedPublication from designsafe.libs.elasticsearch.docs.base import BaseESResource from designsafe.libs.elasticsearch.exceptions import DocumentNotFound @@ -16,7 +16,7 @@ # pylint: enable=invalid-name -@python_2_unicode_compatible + class BaseESPublication(BaseESResource): """Wrapper class for Elastic Search indexed publication. @@ -157,7 +157,7 @@ def to_file(self): except: dict_obj['meta']['piLabel'] = '({pi})'.format(pi=pi) return dict_obj - + def entity_keys(self, publishable=False): """Type specific keys for publication""" diff --git a/designsafe/libs/elasticsearch/indices.py b/designsafe/libs/elasticsearch/indices.py index 8e51a1ba3d..2ae93abe64 100644 --- a/designsafe/libs/elasticsearch/indices.py +++ b/designsafe/libs/elasticsearch/indices.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES different doc types. """ -from future.utils import python_2_unicode_compatible + import logging import json import six diff --git a/designsafe/libs/elasticsearch/utils.py b/designsafe/libs/elasticsearch/utils.py index c97412663f..a1838022b7 100644 --- a/designsafe/libs/elasticsearch/utils.py +++ b/designsafe/libs/elasticsearch/utils.py @@ -1,4 +1,4 @@ -from future.utils import python_2_unicode_compatible + import urllib.request, urllib.parse, urllib.error from elasticsearch import Elasticsearch import logging @@ -165,7 +165,7 @@ def iterate_level(client, system, path, limit=100): break # pylint: disable=too-many-locals -@python_2_unicode_compatible + def walk_levels(client, system, path, bottom_up=False, ignore_hidden=False, paths_to_ignore=None): """Walk a pth in an Agave storgae system. @@ -298,14 +298,14 @@ def index_level(path, folders, files, systemId, reindex=False): logger.debug(children_paths) delete_recursive(hit.system, hit.path) -@python_2_unicode_compatible + def repair_path(name, path): if not path.endswith(name): path = path + '/' + name path = path.strip('/') return '/{path}'.format(path=path) -@python_2_unicode_compatible + def repair_paths(limit=1000): from designsafe.apps.data.models.elasticsearch import IndexedFile from elasticsearch import Elasticsearch From e82d8d5b0b00713d01ae3c08f75d6625b2b42d6f Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 15:28:23 -0600 Subject: [PATCH 05/20] uncomment agave imports --- designsafe/apps/accounts/tasks.py | 16 ++++++++-------- .../apps/api/notifications/views/webhooks.py | 2 +- designsafe/apps/api/tests.py | 2 +- designsafe/apps/applications/views.py | 4 ++-- designsafe/apps/auth/tasks.py | 4 ++-- designsafe/apps/data/models/agave/files.py | 2 +- designsafe/apps/data/views/mixins.py | 4 ++-- designsafe/apps/notifications/views.py | 2 +- designsafe/apps/search/views.py | 4 ++-- designsafe/apps/workspace/tasks.py | 2 +- designsafe/apps/workspace/views.py | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/designsafe/apps/accounts/tasks.py b/designsafe/apps/accounts/tasks.py index 040b5d10e8..1ed33b62f5 100644 --- a/designsafe/apps/accounts/tasks.py +++ b/designsafe/apps/accounts/tasks.py @@ -2,7 +2,7 @@ import io import logging from django.conf import settings -from agavepy.agave import Agave, AgaveException +# from agavepy.agave import Agave, AgaveException from celery import shared_task from requests import HTTPError from django.contrib.auth import get_user_model @@ -17,8 +17,8 @@ @shared_task(default_retry_delay=1*30, max_retries=3) def create_report(username, list_name): """ - This task runs a celery task that creates a report of all DesignSafe users. - It pulls data from both TAS and the Django user model, writes them to a CSV, and + This task runs a celery task that creates a report of all DesignSafe users. + It pulls data from both TAS and the Django user model, writes them to a CSV, and imports the CSV to the top-level of the user's My Data directory. """ @@ -47,20 +47,20 @@ def create_report(username, list_name): try: user_profile = TASUser(username=user) designsafe_user = get_user_model().objects.get(username=user) - + if hasattr(designsafe_user, "profile"): #making nh_interests QuerySet into list interests = designsafe_user.profile.nh_interests.all().values('description') nh_interests = [interest['description'] for interest in interests] - + #making research_activities QuerySet into list activities = designsafe_user.profile.research_activities.all().values('description') research_activities = [activity['description'] for activity in activities] # order of items as required by user writer.writerow([user_profile.lastName if user_profile.lastName else user_profile.lastName, - user_profile.firstName if user_profile.firstName else user_profile.firstName, + user_profile.firstName if user_profile.firstName else user_profile.firstName, user_profile.email, user_profile.phone, user_profile.institution, @@ -92,10 +92,10 @@ def create_report(username, list_name): systemId=settings.AGAVE_STORAGE_SYSTEM, fileToUpload=csv_file ) - + csv_file.close() except (HTTPError, AgaveException): logger.exception('Failed to create user report.', extra={'user': username, - 'systemId': settings.AGAVE_STORAGE_SYSTEM}) \ No newline at end of file + 'systemId': settings.AGAVE_STORAGE_SYSTEM}) diff --git a/designsafe/apps/api/notifications/views/webhooks.py b/designsafe/apps/api/notifications/views/webhooks.py index 3dd9470757..3f922d98fe 100644 --- a/designsafe/apps/api/notifications/views/webhooks.py +++ b/designsafe/apps/api/notifications/views/webhooks.py @@ -11,7 +11,7 @@ from celery import shared_task from requests import ConnectionError, HTTPError -from agavepy.agave import Agave, AgaveException +# from agavepy.agave import Agave, AgaveException from designsafe.apps.api.notifications.models import Notification diff --git a/designsafe/apps/api/tests.py b/designsafe/apps/api/tests.py index 01df8f33fb..38a7168eb7 100644 --- a/designsafe/apps/api/tests.py +++ b/designsafe/apps/api/tests.py @@ -4,7 +4,7 @@ from designsafe.apps.projects.models.agave.experimental import ExperimentalProject, ModelConfig, FileModel -from agavepy.agave import Agave +# from agavepy.agave import Agave import mock import json diff --git a/designsafe/apps/applications/views.py b/designsafe/apps/applications/views.py index c82237bf6b..455e30d07d 100644 --- a/designsafe/apps/applications/views.py +++ b/designsafe/apps/applications/views.py @@ -1,4 +1,4 @@ -from agavepy.agave import Agave, AgaveException, load_resource +# from agavepy.agave import Agave, AgaveException, load_resource from designsafe.apps.licenses.models import LICENSE_TYPES, get_license_info from designsafe.apps.notifications.views import get_number_unread_notifications from designsafe.libs.common.decorators import profile as profile_fn @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) metrics = logging.getLogger('metrics') -AGAVE_RESOURCES = load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) +# AGAVE_RESOURCES = load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) @login_required diff --git a/designsafe/apps/auth/tasks.py b/designsafe/apps/auth/tasks.py index 99f07e4738..1d32771a63 100644 --- a/designsafe/apps/auth/tasks.py +++ b/designsafe/apps/auth/tasks.py @@ -2,7 +2,7 @@ import requests from django.conf import settings from django.core.mail import send_mail -from agavepy.agave import Agave, AgaveException +# from agavepy.agave import Agave, AgaveException from designsafe.apps.api.tasks import agave_indexer from designsafe.apps.api.notifications.models import Notification from celery import shared_task @@ -97,7 +97,7 @@ def new_user_alert(username): 'Name: ' + user.first_name + ' ' + user.last_name + '\n' + 'Id: ' + str(user.id) + '\n', settings.DEFAULT_FROM_EMAIL, settings.NEW_ACCOUNT_ALERT_EMAILS.split(','),) - + tram_headers = {"tram-services-key": settings.TRAM_SERVICES_KEY} tram_body = {"project_id": settings.TRAM_PROJECT_ID, "email": user.email} diff --git a/designsafe/apps/data/models/agave/files.py b/designsafe/apps/data/models/agave/files.py index 235fb834f0..d6c6abde9a 100644 --- a/designsafe/apps/data/models/agave/files.py +++ b/designsafe/apps/data/models/agave/files.py @@ -9,7 +9,7 @@ from designsafe.apps.data.models.agave.base import BaseAgaveResource from designsafe.apps.data.models.agave.metadata import BaseMetadataResource, BaseMetadataPermissionResource from designsafe.apps.data.models.agave.systems import roles as system_roles_list -from agavepy.agave import AgaveException +# from agavepy.agave import AgaveException # from agavepy.async import AgaveAsyncResponse, TimeoutError, Error from designsafe.apps.api import tasks diff --git a/designsafe/apps/data/views/mixins.py b/designsafe/apps/data/views/mixins.py index b8dc484dbf..6cd5322c0d 100644 --- a/designsafe/apps/data/views/mixins.py +++ b/designsafe/apps/data/views/mixins.py @@ -3,7 +3,7 @@ from django.contrib.auth.decorators import login_required from django.core.serializers.json import DjangoJSONEncoder from django.http import HttpResponse, StreamingHttpResponse -from agavepy.agave import Agave, AgaveException +# from agavepy.agave import Agave, AgaveException from django.contrib.auth import get_user_model from django.conf import settings import datetime @@ -17,7 +17,7 @@ class JSONResponseMixin(object): """ - View mixin to return a JSON response. + View mixin to return a JSON response. We're building one so we can put any extra code in here. """ diff --git a/designsafe/apps/notifications/views.py b/designsafe/apps/notifications/views.py index e08a20ded0..380992f87f 100644 --- a/designsafe/apps/notifications/views.py +++ b/designsafe/apps/notifications/views.py @@ -7,7 +7,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.contrib.auth import get_user_model -from agavepy.agave import AgaveException +# from agavepy.agave import AgaveException from requests import HTTPError from designsafe.apps.api.notifications.models import Notification from designsafe.apps.notifications.models import Notification as LegacyNotification diff --git a/designsafe/apps/search/views.py b/designsafe/apps/search/views.py index 1ceb1e1eb8..e020284299 100644 --- a/designsafe/apps/search/views.py +++ b/designsafe/apps/search/views.py @@ -3,7 +3,7 @@ from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST -from agavepy.agave import AgaveException +# from agavepy.agave import AgaveException from requests import HTTPError from django.contrib.auth import get_user_model @@ -18,4 +18,4 @@ def index(request): logger.debug('search index') - return render(request, 'designsafe/apps/search/index.html') \ No newline at end of file + return render(request, 'designsafe/apps/search/index.html') diff --git a/designsafe/apps/workspace/tasks.py b/designsafe/apps/workspace/tasks.py index f3b3319eaf..c1644c75fa 100644 --- a/designsafe/apps/workspace/tasks.py +++ b/designsafe/apps/workspace/tasks.py @@ -9,7 +9,7 @@ from designsafe.apps.api.agave import impersonate_service_account from designsafe.apps.api.notifications.models import Notification from django.db import transaction -from agavepy.agave import AgaveException +# from agavepy.agave import AgaveException from celery import shared_task from requests import ConnectionError, HTTPError import logging diff --git a/designsafe/apps/workspace/views.py b/designsafe/apps/workspace/views.py index 20325b89cf..0b6b09dd11 100644 --- a/designsafe/apps/workspace/views.py +++ b/designsafe/apps/workspace/views.py @@ -1,4 +1,4 @@ -from agavepy.agave import AgaveException, Agave +# from agavepy.agave import AgaveException, Agave from django.shortcuts import render, redirect from django.conf import settings from django.contrib.auth.decorators import login_required From 3f53b2702a13b012408f5cb3744c3d85e53d9f23 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 15:29:15 -0600 Subject: [PATCH 06/20] add TapisOAuthToken model migration --- ..._tapisoauthtoken_delete_agaveoauthtoken.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py diff --git a/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py new file mode 100644 index 0000000000..18ffb59c36 --- /dev/null +++ b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.6 on 2024-02-28 21:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('designsafe_auth', '0002_auto_20160209_0427'), + ] + + operations = [ + migrations.CreateModel( + name='TapisOAuthToken', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('access_token', models.CharField(max_length=2048)), + ('refresh_token', models.CharField(max_length=2048)), + ('expires_in', models.BigIntegerField()), + ('created', models.BigIntegerField()), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='tapis_oauth', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.DeleteModel( + name='AgaveOAuthToken', + ), + ] From f1d29ffbbb378ba9680a1e83b5e99d7769b8bbbb Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 15:43:04 -0600 Subject: [PATCH 07/20] fix bugs --- .docs/source/designsafe.apps.auth.rst | 8 -------- designsafe/apps/auth/backends.py | 10 +++++----- designsafe/apps/auth/urls.py | 3 ++- designsafe/settings/common_settings.py | 3 +-- designsafe/settings/test_settings.py | 1 - 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/.docs/source/designsafe.apps.auth.rst b/.docs/source/designsafe.apps.auth.rst index 0d872b14a2..ccdbdd0f2a 100644 --- a/.docs/source/designsafe.apps.auth.rst +++ b/.docs/source/designsafe.apps.auth.rst @@ -28,14 +28,6 @@ designsafe.apps.auth.backends module :undoc-members: :show-inheritance: -designsafe.apps.auth.context_processors module ----------------------------------------------- - -.. automodule:: designsafe.apps.auth.context_processors - :members: - :undoc-members: - :show-inheritance: - designsafe.apps.auth.middleware module -------------------------------------- diff --git a/designsafe/apps/auth/backends.py b/designsafe/apps/auth/backends.py index 8e933c8966..aef3c89fa9 100644 --- a/designsafe/apps/auth/backends.py +++ b/designsafe/apps/auth/backends.py @@ -8,8 +8,8 @@ from tapipy.tapis import Tapis from tapipy.errors import BaseTapyException from designsafe.apps.accounts.models import DesignSafeProfile, NotificationPreferences -from designsafe.apps.users.utils import get_user_data -from desingsafe.apps.auth.models.TapisOAuthToken import get_masked_token +from designsafe.apps.api.users.utils import get_user_data +from designsafe.apps.auth.models import TapisOAuthToken from django.contrib.auth.signals import user_logged_out from django.contrib import messages from django.core.exceptions import ValidationError @@ -36,7 +36,7 @@ def on_user_logged_out(sender, request, user, **kwargs): login_provider = "TACC" logger.info( - "Revoking tapis token: %s", get_masked_token(user.tapis_oauth.access_token) + "Revoking tapis token: %s", TapisOAuthToken().get_masked_token(user.tapis_oauth.access_token) ) backend = TapisOAuthBackend() TapisOAuthBackend.revoke(backend, user.tapis_oauth.access_token) @@ -139,7 +139,7 @@ def authenticate(self, *args, **kwargs): token = kwargs["token"] logger.info( - 'Attempting login via Tapis with token "%s"' % get_masked_token(token) + 'Attempting login via Tapis with token "%s"' % TapisOAuthToken().get_masked_token(token) ) client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL, access_token=token) @@ -188,7 +188,7 @@ def authenticate(self, *args, **kwargs): def revoke(self, token): self.logger.info( - "Attempting to revoke Tapis token %s" % get_masked_token(token) + "Attempting to revoke Tapis token %s" % TapisOAuthToken().get_masked_token(token) ) client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL, access_token=token) diff --git a/designsafe/apps/auth/urls.py b/designsafe/apps/auth/urls.py index cc6a5ef832..a14d41f958 100644 --- a/designsafe/apps/auth/urls.py +++ b/designsafe/apps/auth/urls.py @@ -6,8 +6,9 @@ from django.urls import path from designsafe.apps.auth import views -app_name = "portal_auth" +app_name = "designsafe_auth" urlpatterns = [ + path('login/', views.tapis_oauth, name="login"), path("logged-out/", views.logged_out, name="logout"), path("tapis/", views.tapis_oauth, name="tapis_oauth"), path("tapis/callback/", views.tapis_oauth_callback, name="tapis_oauth_callback"), diff --git a/designsafe/settings/common_settings.py b/designsafe/settings/common_settings.py index ec2917098d..b40189154e 100644 --- a/designsafe/settings/common_settings.py +++ b/designsafe/settings/common_settings.py @@ -115,7 +115,7 @@ ) AUTHENTICATION_BACKENDS = ( - 'designsafe.apps.auth.backends.AgaveOAuthBackend', + 'designsafe.apps.auth.backends.TapisOAuthBackend', 'designsafe.apps.auth.backends.TASBackend', 'django.contrib.auth.backends.ModelBackend', ) @@ -177,7 +177,6 @@ 'designsafe.context_processors.site_verification', 'designsafe.context_processors.debug', 'designsafe.context_processors.messages', - 'designsafe.apps.auth.context_processors.auth', 'designsafe.apps.cms_plugins.context_processors.cms_section', ], }, diff --git a/designsafe/settings/test_settings.py b/designsafe/settings/test_settings.py index 496e2d8fce..373b5a32d1 100644 --- a/designsafe/settings/test_settings.py +++ b/designsafe/settings/test_settings.py @@ -167,7 +167,6 @@ 'designsafe.context_processors.site_verification', 'designsafe.context_processors.debug', 'designsafe.context_processors.messages', - 'designsafe.apps.auth.context_processors.auth', 'designsafe.apps.cms_plugins.context_processors.cms_section', ], }, From f2a988d7d6b4ab874336c114788919255754e184 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 15:48:06 -0600 Subject: [PATCH 08/20] remove attrdict for agavepy --- poetry.lock | 20 +------------------- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 57c63b4fdf..02e72643b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -104,24 +104,6 @@ files = [ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] -[[package]] -name = "attrdict" -version = "2.0.1" -description = "A dict with attribute-style access" -optional = false -python-versions = "*" -files = [] -develop = false - -[package.dependencies] -six = "*" - -[package.source] -type = "git" -url = "https://github.com/DesignSafe-CI/AttrDict" -reference = "83b779ee82d5b0e33be695d398162b8f2430ff33" -resolved_reference = "83b779ee82d5b0e33be695d398162b8f2430ff33" - [[package]] name = "attrs" version = "23.1.0" @@ -4066,4 +4048,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "afa925fb8ac88226c3b5c8be707883fd2ddaf484aa82a1d9f70c2ca07b6b25ce" +content-hash = "307e3b629eb7cf6da0b854f396084fa0ff4feda5c1d8b34666d32f4b71ffece2" diff --git a/pyproject.toml b/pyproject.toml index 76b92b06f4..7bb5da495b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ authors = ["DesignSafe-CI <designsafe-ci@tacc.utexas.edu>"] [tool.poetry.dependencies] python = "^3.11" pytas = {git = "https://bitbucket.org/taccaci/pytas.git", tag = "v1.7.0"} -attrdict = { git = "https://github.com/DesignSafe-CI/AttrDict", rev = "83b779ee82d5b0e33be695d398162b8f2430ff33" } Django = "^4.2" daphne = "^4.0.0" debugpy = "^1.8.0" From 41ac9a10165fd572337ee6369b569a6128147a2d Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Wed, 28 Feb 2024 16:40:39 -0600 Subject: [PATCH 09/20] pylint: ignore test files --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index cbf324a0ef..ac05af4669 100644 --- a/.pylintrc +++ b/.pylintrc @@ -52,7 +52,7 @@ ignore=CVS,tests.py # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\\' represents the directory delimiter on Windows systems, # it can't be used as an escape character. -ignore-paths=^.*migrations/.*$,^.*_tests/.*$ +ignore-paths=^.*migrations/.*$,^.*_tests/.*$,^.*_unit_test.*$ # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores From 3d32c937d94effc99045f3af9b12e0806bd0f1c9 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Thu, 29 Feb 2024 13:59:06 -0600 Subject: [PATCH 10/20] fix ignore unit test regex --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index ac05af4669..492dab158b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -52,7 +52,7 @@ ignore=CVS,tests.py # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\\' represents the directory delimiter on Windows systems, # it can't be used as an escape character. -ignore-paths=^.*migrations/.*$,^.*_tests/.*$,^.*_unit_test.*$ +ignore-paths=^.*migrations/.*$,^.*_tests/.*$,^.*unit_test.*$ # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores From 4783ced742fb96bd2e61cf15cdbb2cb427f6c334 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Thu, 29 Feb 2024 14:43:26 -0600 Subject: [PATCH 11/20] fix login url --- designsafe/apps/auth/urls.py | 2 +- designsafe/urls.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/designsafe/apps/auth/urls.py b/designsafe/apps/auth/urls.py index a14d41f958..678ee5642f 100644 --- a/designsafe/apps/auth/urls.py +++ b/designsafe/apps/auth/urls.py @@ -8,7 +8,7 @@ app_name = "designsafe_auth" urlpatterns = [ - path('login/', views.tapis_oauth, name="login"), + path('/', views.tapis_oauth, name="login"), path("logged-out/", views.logged_out, name="logout"), path("tapis/", views.tapis_oauth, name="tapis_oauth"), path("tapis/callback/", views.tapis_oauth_callback, name="tapis_oauth_callback"), diff --git a/designsafe/urls.py b/designsafe/urls.py index c4918d6639..e2086c8de0 100644 --- a/designsafe/urls.py +++ b/designsafe/urls.py @@ -28,7 +28,8 @@ from django.contrib import admin from django.views.generic import RedirectView, TemplateView from django.urls import reverse, path -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponseRedirect +from designsafe.apps.auth.views import tapis_oauth as login from django.contrib.auth.views import LogoutView as des_logout from designsafe.views import project_version as des_version, redirect_old_nees from impersonate import views as impersonate_views @@ -147,6 +148,7 @@ # auth url(r'^auth/', include(('designsafe.apps.auth.urls', 'designsafe.apps.auth'), namespace='designsafe_auth')), + url(r'^login/$', login, name='login'), url(r'^logout/$', des_logout.as_view(), name='logout'), # help From e296436af7baa9cca685eec1cc3da07b4a24e217 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Thu, 29 Feb 2024 14:52:09 -0600 Subject: [PATCH 12/20] formatting; remove AgaveOAuthToken references --- designsafe/apps/api/notifications/tests.py | 136 +++++++++++------- ..._tapisoauthtoken_delete_agaveoauthtoken.py | 33 +++-- designsafe/apps/auth/unit_test.py | 7 +- designsafe/apps/auth/views_unit_test.py | 1 + designsafe/apps/workspace/tests.py | 71 ++++----- 5 files changed, 150 insertions(+), 98 deletions(-) diff --git a/designsafe/apps/api/notifications/tests.py b/designsafe/apps/api/notifications/tests.py index cf93dfc8fc..cf692feaba 100644 --- a/designsafe/apps/api/notifications/tests.py +++ b/designsafe/apps/api/notifications/tests.py @@ -1,15 +1,12 @@ -import requests import json import os from django.test import TestCase from django.test import Client from django.contrib.auth import get_user_model from django.db.models.signals import post_save -from mock import Mock, patch -from designsafe.apps.auth.models import AgaveOAuthToken +from mock import patch from urllib.parse import urlencode from unittest import skip -from django.dispatch import receiver from django.urls import reverse from designsafe.apps.api.notifications.models import Notification from .receivers import send_notification_ws @@ -19,42 +16,41 @@ logger = logging.getLogger(__name__) -FILEDIR_PENDING = os.path.join(os.path.dirname(__file__), './json/pending.json') -FILEDIR_SUBMITTING = os.path.join(os.path.dirname(__file__), './json/submitting.json') -FILEDIR_PENDING2 = os.path.join(os.path.dirname(__file__), './json/pending2.json') +FILEDIR_PENDING = os.path.join(os.path.dirname(__file__), "./json/pending.json") +FILEDIR_SUBMITTING = os.path.join(os.path.dirname(__file__), "./json/submitting.json") +FILEDIR_PENDING2 = os.path.join(os.path.dirname(__file__), "./json/pending2.json") webhook_body_pending = json.dumps(json.load(open(FILEDIR_PENDING))) webhook_body_pending2 = json.dumps(json.load(open(FILEDIR_PENDING2))) webhook_body_submitting = json.dumps(json.load(open(FILEDIR_SUBMITTING))) - # Create your tests here. @skip("Need to mock websocket call to redis") class NotificationsTestCase(TestCase): - fixtures = ['user-data.json', 'agave-oauth-token-data.json'] + fixtures = ["user-data.json", "agave-oauth-token-data.json"] def setUp(self): - self.wh_url = reverse('designsafe_api:jobs_wh_handler') + self.wh_url = reverse("designsafe_api:jobs_wh_handler") user = get_user_model().objects.get(pk=2) - user.set_password('password') + user.set_password("password") user.save() self.user = user self.client = Client() - with open('designsafe/apps/api/fixtures/agave-model-config-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-model-config-meta.json") as f: model_config_meta = json.load(f) self.model_config_meta = model_config_meta - with open('designsafe/apps/api/fixtures/agave-file-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-file-meta.json") as f: file_meta = json.load(f) self.file_meta = file_meta - with open('designsafe/apps/api/fixtures/agave-experiment-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-experiment-meta.json") as f: experiment_meta = json.load(f) self.experiment_meta = experiment_meta - with open('designsafe/apps/api/fixtures/agave-project-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-project-meta.json") as f: project_meta = json.load(f) self.project_meta = project_meta @@ -62,42 +58,58 @@ def test_current_user_is_ds_user(self): """ just making sure the db setup worked. """ - self.assertEqual(self.user.username, 'ds_user') + self.assertEqual(self.user.username, "ds_user") def test_submitting_webhook_returns_200_and_creates_notification(self): - r = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') + r = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) self.assertEqual(r.status_code, 200) n = Notification.objects.last() - status_from_notification = n.to_dict()['extra']['status'] - self.assertEqual(status_from_notification, 'PENDING') + status_from_notification = n.to_dict()["extra"]["status"] + self.assertEqual(status_from_notification, "PENDING") def test_2_webhooks_same_status_same_jobId_should_give_1_notification(self): - r = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') - - #assert that sending the same status twice doesn't trigger a second notification. - r2 = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') + r = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) + + # assert that sending the same status twice doesn't trigger a second notification. + r2 = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) self.assertEqual(Notification.objects.count(), 1) def test_2_webhooks_different_status_same_jobId_should_give_2_notifications(self): - r1 = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') + r1 = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) - r2 = self.client.post(self.wh_url, webhook_body_submitting, content_type='application/json') + r2 = self.client.post( + self.wh_url, webhook_body_submitting, content_type="application/json" + ) self.assertEqual(Notification.objects.count(), 2) def test_2_webhooks_same_status_different_jobId_should_give_2_notifications(self): - r = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') - r2 = self.client.post(self.wh_url, webhook_body_pending2, content_type='application/json') + r = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) + r2 = self.client.post( + self.wh_url, webhook_body_pending2, content_type="application/json" + ) self.assertEqual(Notification.objects.count(), 2) class TestWebhookViews(TestCase): - fixtures = ['user-data', 'agave-oauth-token-data'] + fixtures = ["user-data", "agave-oauth-token-data"] def setUp(self): - self.wh_url = reverse('designsafe_api:jobs_wh_handler') - self.mock_agave_patcher = patch('designsafe.apps.auth.models.AgaveOAuthToken.client', autospec=True) + self.wh_url = reverse("designsafe_api:jobs_wh_handler") + self.mock_agave_patcher = patch( + "designsafe.apps.auth.models.TapisOAuthToken.client", autospec=True + ) self.mock_agave = self.mock_agave_patcher.start() self.client.force_login(get_user_model().objects.get(username="ds_user")) @@ -109,7 +121,7 @@ def setUp(self): "port": "1234", "address": "http://designsafe-exec-01.tacc.utexas.edu:1234", "job_uuid": "3373312947011719656-242ac11b-0001-007", - "owner": "ds_user" + "owner": "ds_user", } self.vnc_event = { @@ -117,7 +129,7 @@ def setUp(self): "host": "stampede2.tacc.utexas.edu", "port": "2234", "password": "3373312947011719656-242ac11b-0001-007", - "owner": "ds_user" + "owner": "ds_user", } self.agave_job_running = {"owner": "ds_user", "status": "RUNNING"} @@ -125,62 +137,80 @@ def setUp(self): def tearDown(self): self.mock_agave_patcher.stop() - post_save.connect(send_notification_ws, sender=Notification, dispatch_uid="notification_msg") + post_save.connect( + send_notification_ws, sender=Notification, dispatch_uid="notification_msg" + ) def test_unsupported_event_type(self): - response = self.client.post(reverse('interactive_wh_handler'), - urlencode({'event_type': 'DUMMY'}), - content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode({"event_type": "DUMMY"}), + content_type="application/x-www-form-urlencoded", + ) self.assertTrue(response.status_code == 400) def test_webhook_job_post(self): - job_event = json.load(open(os.path.join(os.path.dirname(__file__), 'json/submitting.json'))) + job_event = json.load( + open(os.path.join(os.path.dirname(__file__), "json/submitting.json")) + ) - response = self.client.post(self.wh_url, json.dumps(job_event), content_type='application/json') + response = self.client.post( + self.wh_url, json.dumps(job_event), content_type="application/json" + ) self.assertEqual(response.status_code, 200) n = Notification.objects.last() - n_status = n.to_dict()['extra']['status'] - self.assertEqual(n_status, job_event['status']) + n_status = n.to_dict()["extra"]["status"] + self.assertEqual(n_status, job_event["status"]) def test_webhook_vnc_post(self): self.mock_agave.jobs.get.return_value = self.agave_job_running link_from_event = "https://tap.tacc.utexas.edu/noVNC/?host=stampede2.tacc.utexas.edu&port=2234&autoconnect=true&encrypt=true&resize=scale&password=3373312947011719656-242ac11b-0001-007" - response = self.client.post(reverse('interactive_wh_handler'), urlencode(self.vnc_event), content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.vnc_event), + content_type="application/x-www-form-urlencoded", + ) self.assertEqual(response.status_code, 200) self.assertTrue(self.mock_agave.meta.addMetadata.called) self.assertEqual(Notification.objects.count(), 1) n = Notification.objects.last() - action_link = n.to_dict()['action_link'] + action_link = n.to_dict()["action_link"] self.assertEqual(action_link, link_from_event) - self.assertEqual(n.operation, 'web_link') + self.assertEqual(n.operation, "web_link") def test_webhook_web_post(self): self.mock_agave.jobs.get.return_value = self.agave_job_running link_from_event = "http://designsafe-exec-01.tacc.utexas.edu:1234" - response = self.client.post(reverse('interactive_wh_handler'), urlencode(self.web_event), content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.web_event), + content_type="application/x-www-form-urlencoded", + ) self.assertEqual(response.status_code, 200) self.assertTrue(self.mock_agave.meta.addMetadata.called) self.assertEqual(Notification.objects.count(), 1) n = Notification.objects.last() - action_link = n.to_dict()['action_link'] + action_link = n.to_dict()["action_link"] self.assertEqual(action_link, link_from_event) - self.assertEqual(n.operation, 'web_link') + self.assertEqual(n.operation, "web_link") def test_webhook_vnc_post_no_matching_job(self): self.mock_agave.jobs.get.return_value = self.agave_job_failed - response = self.client.post(reverse('interactive_wh_handler'), - urlencode(self.vnc_event), - content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.vnc_event), + content_type="application/x-www-form-urlencoded", + ) # no matching running job so it fails self.assertEqual(response.status_code, 400) self.assertEqual(Notification.objects.count(), 0) @@ -188,9 +218,11 @@ def test_webhook_vnc_post_no_matching_job(self): def test_webhook_web_post_no_matching_job(self): self.mock_agave.jobs.get.return_value = self.agave_job_failed - response = self.client.post(reverse('interactive_wh_handler'), - urlencode(self.web_event), - content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.web_event), + content_type="application/x-www-form-urlencoded", + ) # no matching running job so it fails self.assertEqual(response.status_code, 400) self.assertEqual(Notification.objects.count(), 0) diff --git a/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py index 18ffb59c36..186e7754a2 100644 --- a/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py +++ b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py @@ -9,22 +9,37 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('designsafe_auth', '0002_auto_20160209_0427'), + ("designsafe_auth", "0002_auto_20160209_0427"), ] operations = [ migrations.CreateModel( - name='TapisOAuthToken', + name="TapisOAuthToken", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('access_token', models.CharField(max_length=2048)), - ('refresh_token', models.CharField(max_length=2048)), - ('expires_in', models.BigIntegerField()), - ('created', models.BigIntegerField()), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='tapis_oauth', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("access_token", models.CharField(max_length=2048)), + ("refresh_token", models.CharField(max_length=2048)), + ("expires_in", models.BigIntegerField()), + ("created", models.BigIntegerField()), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="tapis_oauth", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.DeleteModel( - name='AgaveOAuthToken', + name="AgaveOAuthToken", ), ] diff --git a/designsafe/apps/auth/unit_test.py b/designsafe/apps/auth/unit_test.py index 7d84b24f8b..9fde813a4c 100644 --- a/designsafe/apps/auth/unit_test.py +++ b/designsafe/apps/auth/unit_test.py @@ -1,12 +1,12 @@ - import pytest from django.test import TransactionTestCase, override_settings from django.contrib.auth import get_user_model from mock import patch, MagicMock from requests import Response from designsafe.apps.auth.backends import TapisOAuthBackend + # from designsafe.apps.auth.views import launch_setup_checks -from tapipy.tapis import TapisResult +# from tapipy.tapis import TapisResult pytestmark = pytest.mark.django_db @@ -26,7 +26,8 @@ def setUp(self): self.backend = TapisOAuthBackend() self.mock_response = MagicMock(autospec=Response) self.mock_requests_patcher = patch( - "designsafe.apps.auth.backends.Tapis.authenticator.get_userinfo", return_value=self.mock_response + "designsafe.apps.auth.backends.Tapis.authenticator.get_userinfo", + return_value=self.mock_response, ) self.mock_requests = self.mock_requests_patcher.start() diff --git a/designsafe/apps/auth/views_unit_test.py b/designsafe/apps/auth/views_unit_test.py index c09d592dd1..fa08c9fc97 100644 --- a/designsafe/apps/auth/views_unit_test.py +++ b/designsafe/apps/auth/views_unit_test.py @@ -1,4 +1,5 @@ """DesignSafe Auth Tapis OAuth flow view tests""" + import pytest from django.conf import settings from django.urls import reverse diff --git a/designsafe/apps/workspace/tests.py b/designsafe/apps/workspace/tests.py index c1261d9a1c..1e8d7088e5 100644 --- a/designsafe/apps/workspace/tests.py +++ b/designsafe/apps/workspace/tests.py @@ -1,5 +1,4 @@ import json -import os from mock import patch from django.test import TestCase from .models.app_descriptions import AppDescription @@ -9,32 +8,38 @@ class AppDescriptionModelTest(TestCase): - fixtures = ['user-data', 'agave-oauth-token-data'] + fixtures = ["user-data", "agave-oauth-token-data"] def setUp(self): user = get_user_model().objects.get(pk=2) - user.set_password('user/password') + user.set_password("user/password") user.save() def test_string_representation(self): - descriptionModel = AppDescription(appid='TestApp0.1', appdescription='Test description') + descriptionModel = AppDescription( + appid="TestApp0.1", appdescription="Test description" + ) self.assertEqual(str(descriptionModel), descriptionModel.appid) def test_get_app_description(self): - AppDescription.objects.create(appid='TestApp0.1', appdescription='Test description') - self.client.login(username='ds_user', password='user/password') - url = reverse('designsafe_workspace:call_api', args=('description',)) - response = self.client.get(url, {'app_id': 'TestApp0.1'}) - self.assertContains(response, 'TestApp0.1') + AppDescription.objects.create( + appid="TestApp0.1", appdescription="Test description" + ) + self.client.login(username="ds_user", password="user/password") + url = reverse("designsafe_workspace:call_api", args=("description",)) + response = self.client.get(url, {"app_id": "TestApp0.1"}) + self.assertContains(response, "TestApp0.1") class TestAppsApiViews(TestCase): - fixtures = ['user-data', 'agave-oauth-token-data'] + fixtures = ["user-data", "agave-oauth-token-data"] @classmethod def setUpClass(cls): super(TestAppsApiViews, cls).setUpClass() - cls.mock_client_patcher = patch('designsafe.apps.auth.models.AgaveOAuthToken.client') + cls.mock_client_patcher = patch( + "designsafe.apps.auth.models.TapisOAuthToken.client" + ) cls.mock_client = cls.mock_client_patcher.start() @classmethod @@ -44,26 +49,20 @@ def tearDownClass(cls): def setUp(self): user = get_user_model().objects.get(pk=2) - user.set_password('user/password') + user.set_password("user/password") user.save() def test_apps_list(self): - self.client.login(username='ds_user', password='user/password') + self.client.login(username="ds_user", password="user/password") apps = [ - { - "id": "app-one", - "executionSystem": "stampede2" - }, - { - "id": "app-two", - "executionSystem": "stampede2" - } + {"id": "app-one", "executionSystem": "stampede2"}, + {"id": "app-two", "executionSystem": "stampede2"}, ] - #need to do a return_value on the mock_client because - #the calling signature is something like client = Agave(**kwargs).apps.list() + # need to do a return_value on the mock_client because + # the calling signature is something like client = Agave(**kwargs).apps.list() self.mock_client.apps.list.return_value = apps - url = reverse('designsafe_workspace:call_api', args=('apps',)) + url = reverse("designsafe_workspace:call_api", args=("apps",)) response = self.client.get(url, follow=True) data = response.json() # If the request is sent successfully, then I expect a response to be returned. @@ -72,37 +71,41 @@ def test_apps_list(self): self.assertTrue(data == apps) def test_job_submit_notifications(self): - with open('designsafe/apps/workspace/fixtures/job-submission.json') as f: + with open("designsafe/apps/workspace/fixtures/job-submission.json") as f: job_data = json.load(f) self.mock_client.jobs.submit.return_value = {"status": "ok"} - self.client.login(username='ds_user', password='user/password') + self.client.login(username="ds_user", password="user/password") - url = reverse('designsafe_workspace:call_api', args=('jobs',)) - response = self.client.post(url, json.dumps(job_data), content_type="application/json") + url = reverse("designsafe_workspace:call_api", args=("jobs",)) + response = self.client.post( + url, json.dumps(job_data), content_type="application/json" + ) data = response.json() self.assertTrue(self.mock_client.jobs.submit.called) - self.assertEqual(data['status'], 'ok') + self.assertEqual(data["status"], "ok") self.assertEqual(response.status_code, 200) def test_job_submit_parse_urls(self): - with open('designsafe/apps/workspace/fixtures/job-submission.json') as f: + with open("designsafe/apps/workspace/fixtures/job-submission.json") as f: job_data = json.load(f) # the spaces should get quoted out job_data["inputs"]["workingDirectory"] = "agave://test.system/name with spaces" self.mock_client.jobs.submit.return_value = {"status": "ok"} - self.client.login(username='ds_user', password='user/password') + self.client.login(username="ds_user", password="user/password") - url = reverse('designsafe_workspace:call_api', args=('jobs',)) - response = self.client.post(url, json.dumps(job_data), content_type="application/json") + url = reverse("designsafe_workspace:call_api", args=("jobs",)) + response = self.client.post( + url, json.dumps(job_data), content_type="application/json" + ) self.assertEqual(response.status_code, 200) args, kwargs = self.mock_client.jobs.submit.call_args body = kwargs["body"] input = body["inputs"]["workingDirectory"] - #the spaces should have been quoted + # the spaces should have been quoted self.assertTrue("%20" in input) def test_licensed_apps(self): From e49c23d697ed9d2d9739c8d90082210def138f9e Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Thu, 29 Feb 2024 14:58:01 -0600 Subject: [PATCH 13/20] add test settings for tapisv3 --- designsafe/settings/test_settings.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/designsafe/settings/test_settings.py b/designsafe/settings/test_settings.py index 373b5a32d1..1789ae1495 100644 --- a/designsafe/settings/test_settings.py +++ b/designsafe/settings/test_settings.py @@ -540,6 +540,15 @@ AGAVE_SUPER_TOKEN = 'example_com_client_token' AGAVE_STORAGE_SYSTEM = 'storage.example.com' +# Tapis Client Configuration +PORTAL_ADMIN_USERNAME = '' +TAPIS_TENANT_BASEURL = 'https://designsafe.tapis.io' +TAPIS_CLIENT_ID = 'client_id' +TAPIS_CLIENT_KEY = 'client_key' +TAPIS_ADMIN_JWT = 'admin_jwt' + +KEY_SERVICE_TOKEN = '' + MIGRATION_MODULES = { 'data': None, 'designsafe_data': None, From 3eac1b803dd92c35efeade3cf6c152456879047b Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Thu, 29 Feb 2024 18:15:40 -0600 Subject: [PATCH 14/20] fix auth/views_unit_test.py --- .flake8 | 2 ++ designsafe/apps/auth/backends.py | 4 ++-- designsafe/apps/auth/middleware.py | 6 +++++- designsafe/apps/auth/views.py | 2 +- designsafe/apps/auth/views_unit_test.py | 2 +- designsafe/conftest.py | 16 ++++++++++++++++ 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.flake8 b/.flake8 index d5604d56e3..c3e40888df 100644 --- a/.flake8 +++ b/.flake8 @@ -5,3 +5,5 @@ ignore = E501, H101 exclude = __pycache__, tests.py, migrations + +extend-ignore = W503 diff --git a/designsafe/apps/auth/backends.py b/designsafe/apps/auth/backends.py index aef3c89fa9..b668a94b3b 100644 --- a/designsafe/apps/auth/backends.py +++ b/designsafe/apps/auth/backends.py @@ -187,10 +187,10 @@ def authenticate(self, *args, **kwargs): return user def revoke(self, token): - self.logger.info( + logger.info( "Attempting to revoke Tapis token %s" % TapisOAuthToken().get_masked_token(token) ) client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL, access_token=token) response = client.authenticator.revoke_token(token=token) - self.logger.info("revoke response is %s" % response) + logger.info("revoke response is %s" % response) diff --git a/designsafe/apps/auth/middleware.py b/designsafe/apps/auth/middleware.py index 877657d0e1..f013fe701d 100644 --- a/designsafe/apps/auth/middleware.py +++ b/designsafe/apps/auth/middleware.py @@ -20,7 +20,11 @@ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - if request.path != reverse("logout") and request.user.is_authenticated: + if ( + request.path != reverse("logout") + and request.path != reverse("login") + and request.user.is_authenticated + ): self.process_request(request) response = self.get_response(request) diff --git a/designsafe/apps/auth/views.py b/designsafe/apps/auth/views.py index 5714c81de0..0fb8afb44e 100644 --- a/designsafe/apps/auth/views.py +++ b/designsafe/apps/auth/views.py @@ -51,7 +51,7 @@ def tapis_oauth(request): tenant_base_url = getattr(settings, "TAPIS_TENANT_BASEURL") client_id = getattr(settings, "TAPIS_CLIENT_ID") - METRICS.info(f"user:{request.user.username} starting oauth redirect login") + METRICS.debug(f"user:{request.user.username} starting oauth redirect login") # Authorization code request authorization_url = ( diff --git a/designsafe/apps/auth/views_unit_test.py b/designsafe/apps/auth/views_unit_test.py index fa08c9fc97..46e77017d9 100644 --- a/designsafe/apps/auth/views_unit_test.py +++ b/designsafe/apps/auth/views_unit_test.py @@ -20,7 +20,7 @@ def test_auth_tapis(client, mocker): tapis_authorize = ( f"{settings.TAPIS_TENANT_BASEURL}/v3/oauth2/authorize" - f"?client_id=test&redirect_uri=http://testserver/auth/tapis/callback/&response_type=code&state={TEST_STATE}" + f"?client_id=client_id&redirect_uri=http://testserver/auth/tapis/callback/&response_type=code&state={TEST_STATE}" ) assert response.status_code == 302 diff --git a/designsafe/conftest.py b/designsafe/conftest.py index 1cb2309d9d..aaa811fb3c 100644 --- a/designsafe/conftest.py +++ b/designsafe/conftest.py @@ -1,6 +1,9 @@ """Base User pytest fixtures""" import pytest +import os +import json +from django.conf import settings from designsafe.apps.auth.models import TapisOAuthToken @@ -50,3 +53,16 @@ def project_admin_user(django_user_model): def authenticated_user(client, regular_user): client.force_login(regular_user) yield regular_user + + +@pytest.fixture +def tapis_tokens_create_mock(): + yield json.load( + open( + os.path.join( + settings.BASE_DIR, + "designsafe/fixtures/tapis/auth/create-tokens-response.json", + ), + "r", + ) + ) From d5cb4da1614ab80d50498e533dd30ae7e3bb361e Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Fri, 1 Mar 2024 12:18:44 -0600 Subject: [PATCH 15/20] fix backends unit test --- designsafe/apps/api/users/utils.py | 34 ++---- designsafe/apps/auth/backends_unit_test.py | 97 +++++++++++++++++ designsafe/apps/auth/unit_test.py | 101 ------------------ .../tapis/auth/create-tokens-response.json | 20 ++++ 4 files changed, 128 insertions(+), 124 deletions(-) create mode 100644 designsafe/apps/auth/backends_unit_test.py delete mode 100644 designsafe/apps/auth/unit_test.py create mode 100644 designsafe/fixtures/tapis/auth/create-tokens-response.json diff --git a/designsafe/apps/api/users/utils.py b/designsafe/apps/api/users/utils.py index 0eb576a8e3..bd51e92e52 100644 --- a/designsafe/apps/api/users/utils.py +++ b/designsafe/apps/api/users/utils.py @@ -1,30 +1,18 @@ import logging from pytas.http import TASClient from django.db.models import Q -from django.conf import settings logger = logging.getLogger(__name__) -def get_tas_client(): - """Return a TAS Client with pytas""" - return TASClient( - baseURL=settings.TAS_URL, - credentials={ - 'username': settings.TAS_CLIENT_KEY, - 'password': settings.TAS_CLIENT_SECRET - } - ) - - def get_user_data(username): """Returns user contact information : returns: user_data : rtype: dict """ - tas_client = get_tas_client() + tas_client = TASClient() user_data = tas_client.get_user(username=username) return user_data @@ -32,13 +20,13 @@ def get_user_data(username): def list_to_model_queries(q_comps): query = None if len(q_comps) > 2: - query = Q(first_name__icontains = ' '.join(q_comps[:1])) - query |= Q(first_name__icontains = ' '.join(q_comps[:2])) - query |= Q(last_name__icontains = ' '.join(q_comps[1:])) - query |= Q(last_name__icontains = ' '.join(q_comps[2:])) + query = Q(first_name__icontains=" ".join(q_comps[:1])) + query |= Q(first_name__icontains=" ".join(q_comps[:2])) + query |= Q(last_name__icontains=" ".join(q_comps[1:])) + query |= Q(last_name__icontains=" ".join(q_comps[2:])) else: - query = Q(first_name__icontains = q_comps[0]) - query |= Q(last_name__icontains = q_comps[1]) + query = Q(first_name__icontains=q_comps[0]) + query |= Q(last_name__icontains=q_comps[1]) return query @@ -47,12 +35,12 @@ def q_to_model_queries(q): return None query = None - if ' ' in q: + if " " in q: q_comps = q.split() query = list_to_model_queries(q_comps) else: - query = Q(email__icontains = q) - query |= Q(first_name__icontains = q) - query |= Q(last_name__icontains = q) + query = Q(email__icontains=q) + query |= Q(first_name__icontains=q) + query |= Q(last_name__icontains=q) return query diff --git a/designsafe/apps/auth/backends_unit_test.py b/designsafe/apps/auth/backends_unit_test.py new file mode 100644 index 0000000000..f02d3e4680 --- /dev/null +++ b/designsafe/apps/auth/backends_unit_test.py @@ -0,0 +1,97 @@ +import pytest +from django.contrib.auth import get_user_model +from mock import Mock +from designsafe.apps.auth.backends import TapisOAuthBackend +from tapipy.tapis import TapisResult +from tapipy.errors import BaseTapyException + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def user_data_mock(mocker): + mock_user_data = mocker.patch( + "designsafe.apps.auth.backends.get_user_data", + return_value={ + "username": "testuser", + "firstName": "test", + "lastName": "user", + "email": "new@email.com", + }, + ) + return mock_user_data + + +@pytest.fixture() +def tapis_mock(mocker): + tapis_patcher = mocker.patch("designsafe.apps.auth.backends.Tapis") + mock_tapis = Mock() + mock_tapis.authenticator.get_userinfo.return_value = TapisResult( + username="testuser" + ) + tapis_patcher.return_value = mock_tapis + yield tapis_patcher + + +@pytest.fixture() +def update_institution_from_tas_mock(mocker): + yield mocker.patch("designsafe.apps.auth.backends.update_institution_from_tas") + + +# def test_launch_setup_checks(mocker, regular_user, settings): +# mocker.patch("designsafe.apps.auth.views.new_user_setup_check") +# mock_execute = mocker.patch("designsafe.apps.auth.views.execute_setup_steps") +# regular_user.profile.setup_complete = False +# launch_setup_checks(regular_user) +# mock_execute.apply_async.assert_called_with(args=["username"]) + + +def test_bad_backend_params(tapis_mock): + # Test backend authenticate with no backend params + backend = TapisOAuthBackend() + result = backend.authenticate() + assert result is None + + # Test TapisOAuthBackend if params do not indicate tapis + result = backend.authenticate(backend="not_tapis") + assert result is None + + +def test_bad_response_status( + tapis_mock, user_data_mock, update_institution_from_tas_mock +): + """Test that backend failure responses are handled""" + backend = TapisOAuthBackend() + mock_tapis = Mock() + mock_tapis.authenticator.get_userinfo.side_effect = BaseTapyException + tapis_mock.return_value = mock_tapis + result = backend.authenticate(backend="tapis", token="1234") + assert result is None + + +def test_new_user(tapis_mock, user_data_mock, update_institution_from_tas_mock): + """Test that a new user is created and returned""" + backend = TapisOAuthBackend() + result = backend.authenticate(backend="tapis", token="1234") + assert result.username == "testuser" + + +def test_update_existing_user( + tapis_mock, user_data_mock, update_institution_from_tas_mock +): + """Test that an existing user's information is updated with from info from the Tapis backend response""" + backend = TapisOAuthBackend() + + # Create a pre-existing user with the same username + user = get_user_model().objects.create_user( + username="testuser", + first_name="test", + last_name="user", + email="old@email.com", + ) + result = backend.authenticate(backend="tapis", token="1234") + # Result user object should be the same + assert result == user + # Existing user object should be updated + user = get_user_model().objects.get(username="testuser") + assert user.email == "new@email.com" diff --git a/designsafe/apps/auth/unit_test.py b/designsafe/apps/auth/unit_test.py deleted file mode 100644 index 9fde813a4c..0000000000 --- a/designsafe/apps/auth/unit_test.py +++ /dev/null @@ -1,101 +0,0 @@ -import pytest -from django.test import TransactionTestCase, override_settings -from django.contrib.auth import get_user_model -from mock import patch, MagicMock -from requests import Response -from designsafe.apps.auth.backends import TapisOAuthBackend - -# from designsafe.apps.auth.views import launch_setup_checks -# from tapipy.tapis import TapisResult - - -pytestmark = pytest.mark.django_db - - -# def test_launch_setup_checks(mocker, regular_user, settings): -# mocker.patch("designsafe.apps.auth.views.new_user_setup_check") -# mock_execute = mocker.patch("designsafe.apps.auth.views.execute_setup_steps") -# regular_user.profile.setup_complete = False -# launch_setup_checks(regular_user) -# mock_execute.apply_async.assert_called_with(args=["username"]) - - -class TestTapisOAuthBackend(TransactionTestCase): - def setUp(self): - super(TestTapisOAuthBackend, self).setUp() - self.backend = TapisOAuthBackend() - self.mock_response = MagicMock(autospec=Response) - self.mock_requests_patcher = patch( - "designsafe.apps.auth.backends.Tapis.authenticator.get_userinfo", - return_value=self.mock_response, - ) - self.mock_requests = self.mock_requests_patcher.start() - - self.mock_user_data_patcher = patch( - "designsafe.apps.auth.backends.get_user_data", - return_value={ - "username": "testuser", - "firstName": "test", - "lastName": "user", - "email": "new@email.com", - }, - ) - self.mock_user_data = self.mock_user_data_patcher.start() - - def tearDown(self): - super(TestTapisOAuthBackend, self).tearDown() - self.mock_requests_patcher.stop() - self.mock_user_data_patcher.stop() - - def test_bad_backend_params(self): - # Test backend authenticate with no params - result = self.backend.authenticate() - self.assertIsNone(result) - # Test TapisOAuthBackend if params do not indicate tapis - result = self.backend.authenticate(backend="not_tapis") - self.assertIsNone(result) - - def test_bad_response_status(self): - # Test that backend failure responses are handled - - # Mock different return values for the backend response - self.mock_response.json.return_value = {} - result = self.backend.authenticate(backend="tapis", token="1234") - self.assertIsNone(result) - self.mock_response.json.return_value = {"status": "failure"} - result = self.backend.authenticate(backend="tapis", token="1234") - self.assertIsNone(result) - - @override_settings(PORTAL_USER_ACCOUNT_SETUP_STEPS=[]) - def test_new_user(self): - """Test that a new user is created and returned""" - self.mock_response.json.return_value = { - "status": "success", - "result": {"username": "testuser"}, - } - result = self.backend.authenticate(backend="tapis", token="1234") - self.assertEqual(result.username, "testuser") - - @override_settings(PORTAL_USER_ACCOUNT_SETUP_STEPS=[]) - def test_update_existing_user(self): - """Test that an existing user's information is updated with from info from the Tapis backend response""" - - # Create a pre-existing user with the same username - user = get_user_model().objects.create_user( - username="testuser", - first_name="test", - last_name="user", - email="old@email.com", - ) - self.mock_response.json.return_value = { - "status": "success", - "result": { - "username": "testuser", - }, - } - result = self.backend.authenticate(backend="tapis", token="1234") - # Result user object should be the same - self.assertEqual(result, user) - # Existing user object should be updated - user = get_user_model().objects.get(username="testuser") - self.assertEqual(user.email, "new@email.com") diff --git a/designsafe/fixtures/tapis/auth/create-tokens-response.json b/designsafe/fixtures/tapis/auth/create-tokens-response.json new file mode 100644 index 0000000000..00191a6106 --- /dev/null +++ b/designsafe/fixtures/tapis/auth/create-tokens-response.json @@ -0,0 +1,20 @@ +{ + "message": "Token created successfully.", + "metadata": {}, + "result": { + "access_token": { + "access_token": "eyJhbGci---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------", + "expires_at": "2024-03-01T03:47:18.611914+00:00", + "expires_in": 14400, + "jti": "108792e6-2a77-41ad-964c-f289cc2198f7" + }, + "refresh_token": { + "expires_at": "2025-02-28T23:47:18.711146+00:00", + "expires_in": 31536000, + "jti": "69992b30-3b3b-477a-ba22-3bd2a8203791", + "refresh_token": "eyJhbGci---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------" + } + }, + "status": "success", + "version": "dev" +} From 58ab3e7ff0be0c394e13ae8ce43e019618a7714c Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Fri, 1 Mar 2024 12:22:05 -0600 Subject: [PATCH 16/20] formatting --- .../migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py | 1 - 1 file changed, 1 deletion(-) diff --git a/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py index 186e7754a2..29a3f33c85 100644 --- a/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py +++ b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("designsafe_auth", "0002_auto_20160209_0427"), From 697e11f875941cd0721ae09162becc0ec7e79384 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Mon, 4 Mar 2024 13:54:20 -0600 Subject: [PATCH 17/20] task/DES-2654: Tapis v2/v3 apps model (#1158) * v3 apps model * make appId required field * add AppBundle model * updated model admin * update models and add migration * rework app tray models * squash appitem and appbundle * un-squash migrations * remove faulty app description migration * Use an inline admin form to manage app variants within bundles (#1164) Co-authored-by: Sal Tijerina <r.sal.tijerina@gmail.com> * model adjustments * model adjustments; add category pop migration --------- Co-authored-by: Jake Rosenberg <jrosenberg@tacc.utexas.edu> --- .flake8 | 3 +- .pylintrc | 2 + designsafe/apps/workspace/admin.py | 95 ++++++++ .../apps/workspace/migrations/0001_initial.py | 4 +- .../migrations/0002_auto_20200423_1940.py | 2 +- ...try_apptraycategory_appvariant_and_more.py | 228 ++++++++++++++++++ .../migrations/0004_initial_app_categories.py | 35 +++ designsafe/apps/workspace/models/__init__.py | 2 +- .../apps/workspace/models/app_entries.py | 173 +++++++++++++ designsafe/static/styles/cms-form-styles.css | 4 + 10 files changed, 543 insertions(+), 5 deletions(-) create mode 100644 designsafe/apps/workspace/migrations/0003_applistingentry_apptraycategory_appvariant_and_more.py create mode 100644 designsafe/apps/workspace/migrations/0004_initial_app_categories.py create mode 100644 designsafe/apps/workspace/models/app_entries.py create mode 100644 designsafe/static/styles/cms-form-styles.css diff --git a/.flake8 b/.flake8 index c3e40888df..3c81c28f5d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,8 @@ [flake8] # E501: line is too long. # H101: Use TODO(NAME) -ignore = E501, H101 +# W503: line break before binary operator. Ingore as black will break this rule. +ignore = E501, H101, W503 exclude = __pycache__, tests.py, migrations diff --git a/.pylintrc b/.pylintrc index 492dab158b..5a4da2b364 100644 --- a/.pylintrc +++ b/.pylintrc @@ -430,9 +430,11 @@ disable=raw-checker-failed, useless-suppression, deprecated-pragma, use-symbolic-message-instead, + line-too-long, duplicate-code, logging-fstring-interpolation + # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where diff --git a/designsafe/apps/workspace/admin.py b/designsafe/apps/workspace/admin.py index f7721229ca..b95c773de0 100644 --- a/designsafe/apps/workspace/admin.py +++ b/designsafe/apps/workspace/admin.py @@ -1,4 +1,99 @@ +"""Admin layout for Tools & Applications workspace models. +""" + from django.contrib import admin +from django.db import models +from django.forms import CheckboxSelectMultiple from designsafe.apps.workspace.models.app_descriptions import AppDescription +from designsafe.apps.workspace.models.app_entries import ( + AppListingEntry, + AppVariant, + AppTrayCategory, +) admin.site.register(AppDescription) +admin.site.register(AppTrayCategory) + + +class AppVariantInline(admin.StackedInline): + """Admin layout for app variants.""" + + extra = 0 + model = AppVariant + fk_name = "bundle" + + def get_fieldsets(self, request, obj=None): + return [ + ( + "Tapis App information", + { + "fields": ( + "app_type", + "app_id", + "version", + "license_type", + ) + }, + ), + ( + "Display information", + { + "fields": ( + "label", + "enabled", + ) + }, + ), + ( + "HTML App Body for app_type: HTML", + { + "classes": ["collapse"], + "fields": ["html"], + }, + ), + ] + + +@admin.register(AppListingEntry) +class AppTrayEntryAdmin(admin.ModelAdmin): + """Admin layout for AppTrayEntry items.""" + + class Media: + css = {"all": ("styles/cms-form-styles.css",)} + + inlines = [AppVariantInline] + + def get_fieldsets(self, request, obj=None): + default_fieldset = [ + ( + "Portal Display Options", + { + "fields": [ + "category", + "label", + "icon", + "enabled", + ], + }, + ), + ] + + cms_fieldset = [ + ( + "CMS Display Options", + { + "fields": [ + "href", + "popular", + "not_bundled", + "related_apps", + ], + }, + ), + ] + + return default_fieldset + cms_fieldset + + formfield_overrides = { + models.ManyToManyField: {"widget": CheckboxSelectMultiple}, + } diff --git a/designsafe/apps/workspace/migrations/0001_initial.py b/designsafe/apps/workspace/migrations/0001_initial.py index ce5d324624..7f3fc4f043 100644 --- a/designsafe/apps/workspace/migrations/0001_initial.py +++ b/designsafe/apps/workspace/migrations/0001_initial.py @@ -16,8 +16,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AppDescription', fields=[ - ('appId', models.CharField(max_length=120, primary_key=True, serialize=False, unique=True)), - ('appDescription', models.TextField(help_text=b'App dropdown description text for apps that have a dropdown.')), + ('appid', models.CharField(max_length=120, primary_key=True, serialize=False, unique=True)), + ('appdescription', models.TextField(help_text=b'App dropdown description text for apps that have a dropdown.')), ], ), ] diff --git a/designsafe/apps/workspace/migrations/0002_auto_20200423_1940.py b/designsafe/apps/workspace/migrations/0002_auto_20200423_1940.py index a717cf3763..4e23933c39 100644 --- a/designsafe/apps/workspace/migrations/0002_auto_20200423_1940.py +++ b/designsafe/apps/workspace/migrations/0002_auto_20200423_1940.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): operations = [ migrations.AlterField( model_name='appdescription', - name='appDescription', + name='appdescription', field=models.TextField(help_text='App dropdown description text for apps that have a dropdown.'), ), ] diff --git a/designsafe/apps/workspace/migrations/0003_applistingentry_apptraycategory_appvariant_and_more.py b/designsafe/apps/workspace/migrations/0003_applistingentry_apptraycategory_appvariant_and_more.py new file mode 100644 index 0000000000..6994cca463 --- /dev/null +++ b/designsafe/apps/workspace/migrations/0003_applistingentry_apptraycategory_appvariant_and_more.py @@ -0,0 +1,228 @@ +# Generated by Django 4.2.6 on 2024-03-01 20:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("workspace", "0002_auto_20200423_1940"), + ] + + operations = [ + migrations.CreateModel( + name="AppListingEntry", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "label", + models.CharField( + help_text="The display name of this bundle in the Apps Tray. Not used if this entry is a single app ID.", + max_length=64, + ), + ), + ( + "icon", + models.CharField( + blank=True, + choices=[ + ("adcirc", "ADCIRC"), + ("ansys", "Ansys"), + ("blender", "Blender"), + ("clawpack", "Clawpack"), + ("compress", "Compress"), + ("dakota", "Dakota"), + ("extract", "Extract"), + ("hazmapper", "Hazmapper"), + ("jupyter", "Jupyter"), + ("ls-dyna", "LS-DYNA"), + ("matlab", "MATLAB"), + ("ngl", "NGL"), + ("openfoam", "OpenFOAM"), + ("opensees", "OpenSees"), + ("paraview", "Paraview"), + ("qgis", "QGIS"), + ("rwhale", "rWHALE"), + ("stko", "STKO"), + ("swbatch", "swbatch"), + ("visit", "VisIt"), + ], + help_text="The icon associated with this app.", + max_length=64, + ), + ), + ( + "enabled", + models.BooleanField( + default=True, help_text="App bundle visibility in app tray." + ), + ), + ( + "popular", + models.BooleanField( + default=False, + help_text="Mark as popular on tools & apps overview.", + ), + ), + ( + "not_bundled", + models.BooleanField( + default=False, + help_text="Select if this entry represents a single app ID and not a bundle.", + ), + ), + ( + "href", + models.CharField( + blank=True, + help_text="Link to overview page for this app.", + max_length=128, + ), + ), + ], + options={ + "verbose_name_plural": "App Listing Entries", + }, + ), + migrations.CreateModel( + name="AppTrayCategory", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "category", + models.CharField( + help_text="A category for the app tray.", + max_length=64, + unique=True, + ), + ), + ( + "priority", + models.IntegerField( + default=0, + help_text="Category priority, where higher number tabs appear before lower ones.", + ), + ), + ], + options={ + "verbose_name_plural": "App Categories", + }, + ), + migrations.CreateModel( + name="AppVariant", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "app_id", + models.CharField( + help_text="The id of this app or app bundle. The id appears in the unique url path to the app.", + max_length=64, + ), + ), + ( + "app_type", + models.CharField( + choices=[ + ("tapis", "Tapis App"), + ("html", "HTML or External app"), + ], + default="tapis", + help_text="Application type.", + max_length=10, + ), + ), + ( + "label", + models.CharField( + blank=True, + help_text="The display name of this app in the Apps Tray. If not defined, uses notes.label from app definition.", + max_length=64, + ), + ), + ( + "license_type", + models.CharField( + choices=[("OS", "Open Source"), ("LS", "Licensed")], + default="OS", + help_text="License Type.", + max_length=2, + ), + ), + ( + "html", + models.TextField( + blank=True, + default="", + help_text="HTML definition to display when app is loaded.", + ), + ), + ( + "version", + models.CharField( + blank=True, + default="", + help_text="The version number of the app. The app id + version denotes a unique app.", + max_length=64, + ), + ), + ( + "enabled", + models.BooleanField( + default=True, help_text="App variant visibility in app tray." + ), + ), + ( + "bundle", + models.ForeignKey( + blank=True, + help_text="Bundle that the app belongs to.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="workspace.applistingentry", + ), + ), + ], + ), + migrations.AddField( + model_name="applistingentry", + name="category", + field=models.ForeignKey( + help_text="The category for this app entry.", + on_delete=django.db.models.deletion.CASCADE, + to="workspace.apptraycategory", + ), + ), + migrations.AddField( + model_name="applistingentry", + name="related_apps", + field=models.ManyToManyField( + blank=True, + help_text="Related apps that will display on app overview page.", + to="workspace.applistingentry", + ), + ), + ] diff --git a/designsafe/apps/workspace/migrations/0004_initial_app_categories.py b/designsafe/apps/workspace/migrations/0004_initial_app_categories.py new file mode 100644 index 0000000000..72667dbcb7 --- /dev/null +++ b/designsafe/apps/workspace/migrations/0004_initial_app_categories.py @@ -0,0 +1,35 @@ +from django.db import migrations + + +APP_CATEGORIES = [ + ("5", "Simulation"), + ("4", "Sim Center Tools"), + ("3", "Visualization"), + ("2", "Analysis"), + ("1", "Hazard Apps"), + ("0", "Utilities"), +] + + +def populate_categories(apps, schema_editor): + AppTrayCategory = apps.get_model("workspace", "AppTrayCategory") + for priority, category in APP_CATEGORIES: + AppTrayCategory.objects.create( + priority=priority, + category=category, + ) + + +def reverse_func(apps, schema_editor): + AppTrayCategory = apps.get_model("workspace", "AppTrayCategory") + AppTrayCategory.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("workspace", "0003_applistingentry_apptraycategory_appvariant_and_more"), + ] + + operations = [ + migrations.RunPython(populate_categories, reverse_func), + ] diff --git a/designsafe/apps/workspace/models/__init__.py b/designsafe/apps/workspace/models/__init__.py index bed7aa34d0..9402842dc2 100644 --- a/designsafe/apps/workspace/models/__init__.py +++ b/designsafe/apps/workspace/models/__init__.py @@ -1,4 +1,4 @@ -from django.db import models + from django.dispatch import receiver from designsafe.apps.signals.signals import generic_event from designsafe.apps.notifications.models import Notification diff --git a/designsafe/apps/workspace/models/app_entries.py b/designsafe/apps/workspace/models/app_entries.py new file mode 100644 index 0000000000..ec3d559593 --- /dev/null +++ b/designsafe/apps/workspace/models/app_entries.py @@ -0,0 +1,173 @@ +"""Models for the Tools & Applications workspace. +""" + +from django.db import models + +APP_ICONS = [ + ("adcirc", "ADCIRC"), + ("ansys", "Ansys"), + ("blender", "Blender"), + ("clawpack", "Clawpack"), + ("compress", "Compress"), + ("dakota", "Dakota"), + ("extract", "Extract"), + ("hazmapper", "Hazmapper"), + ("jupyter", "Jupyter"), + ("ls-dyna", "LS-DYNA"), + ("matlab", "MATLAB"), + ("ngl", "NGL"), + ("openfoam", "OpenFOAM"), + ("opensees", "OpenSees"), + ("paraview", "Paraview"), + ("qgis", "QGIS"), + ("rwhale", "rWHALE"), + ("stko", "STKO"), + ("swbatch", "swbatch"), + ("visit", "VisIt"), +] + +LICENSE_TYPES = [("OS", "Open Source"), ("LS", "Licensed")] + + +class AppTrayCategory(models.Model): + """Categories in which AppTrayEntry items are organized.""" + + category = models.CharField( + help_text="A category for the app tray.", max_length=64, unique=True + ) + priority = models.IntegerField( + help_text="Category priority, where higher number tabs appear before lower ones.", + default=0, + ) + + def __str__(self): + return f"{self.category}" + + class Meta: + verbose_name_plural = "App Categories" + + +class AppListingEntry(models.Model): + """Entries for the Tools & Applications workspace, including Tapis, HTML, and Bundled apps. + + ENTRY_TYPES: + A Tapis App is corresponds to a valid app id registered in the Tapis tenant. + + An HTML or External app is typically an HTML body with a link to an external resource. + + An App Listing Entry (bundle) is both: + 1) a card on the apps CMS layout page that links to an overview page, and + 2) a binned app in the apps workspace, where each app variant is a dropdown item. + + Note: If an App Listing Entry has only one variant, it will be treated as a single app, and not a bundle. + """ + + # Basic display options + category = models.ForeignKey( + AppTrayCategory, + help_text="The category for this app entry.", + on_delete=models.CASCADE, + ) + label = models.CharField( + help_text="The display name of this bundle in the Apps Tray. Not used if this entry is a single app ID.", + max_length=64, + ) + icon = models.CharField( + help_text="The icon associated with this app.", + max_length=64, + choices=APP_ICONS, + blank=True, + ) + enabled = models.BooleanField( + help_text="App bundle visibility in app tray.", default=True + ) + + # CMS Display Options + related_apps = models.ManyToManyField( + "self", + help_text="Related apps that will display on app overview page.", + blank=True, + ) + popular = models.BooleanField( + help_text="Mark as popular on tools & apps overview.", default=False + ) + + not_bundled = models.BooleanField( + help_text="Select if this entry represents a single app ID and not a bundle.", + default=False, + ) + + href = models.CharField( + help_text="Link to overview page for this app.", max_length=128, blank=True + ) + + def __str__(self): + return ( + f"{self.category} " + f"{self.label + ': ' if self.label else ''}" + f" ({'ENABLED' if self.enabled else 'DISABLED'})" + ) + + class Meta: + verbose_name_plural = "App Listing Entries" + + +class AppVariant(models.Model): + """Model to represent a variant of an app, e.g. a software version or execution environment""" + + APP_TYPES = [ + ("tapis", "Tapis App"), + ("html", "HTML or External app"), + ] + + app_id = models.CharField( + help_text="The id of this app or app bundle. The id appears in the unique url path to the app.", + max_length=64, + ) + + app_type = models.CharField( + help_text="Application type.", + max_length=10, + choices=APP_TYPES, + default="tapis", + ) + + label = models.CharField( + help_text="The display name of this app in the Apps Tray. If not defined, uses notes.label from app definition.", + max_length=64, + blank=True, + ) + + license_type = models.CharField( + max_length=2, choices=LICENSE_TYPES, help_text="License Type.", default="OS" + ) + + bundle = models.ForeignKey( + AppListingEntry, + help_text="Bundle that the app belongs to.", + on_delete=models.CASCADE, + blank=True, + null=True, + ) + + # HTML Apps + html = models.TextField( + help_text="HTML definition to display when app is loaded.", + default="", + blank=True, + ) + + # Tapis Apps + version = models.CharField( + help_text="The version number of the app. The app id + version denotes a unique app.", + default="", + max_length=64, + blank=True, + ) + + enabled = models.BooleanField( + help_text="App variant visibility in app tray.", default=True + ) + + def __str__(self): + return f"{self.bundle.label} {self.app_id} {self.version} ({'ENABLED' if self.enabled else 'DISABLED'})" diff --git a/designsafe/static/styles/cms-form-styles.css b/designsafe/static/styles/cms-form-styles.css new file mode 100644 index 0000000000..f8800a15f6 --- /dev/null +++ b/designsafe/static/styles/cms-form-styles.css @@ -0,0 +1,4 @@ +.djangocms-admin-style .inline-deletelink { + text-indent: 0px !important; + width: fit-content !important; + } From 9850f096d6369d997fda270d4629c3adbc4d2b61 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Tue, 5 Mar 2024 14:10:41 -0600 Subject: [PATCH 18/20] fix fixture import --- .../apps/accounts/fixtures/user-data.json | 64 -------------- designsafe/apps/api/fixtures/user-data.json | 69 --------------- .../fixtures/agave-oauth-token-data.json | 28 ------ .../api/notifications/fixtures/user-data.json | 69 --------------- designsafe/apps/api/notifications/tests.py | 4 +- designsafe/apps/api/tests.py | 2 +- .../box_integration/fixtures/user-data.json | 69 --------------- .../apps/dashboard/fixtures/user-data.json | 20 ----- designsafe/apps/rapid/fixtures/user-data.json | 64 -------------- .../apps/token_access/fixtures/user-data.json | 85 ------------------- .../fixtures/agave-oauth-token-data.json | 28 ------ .../apps/workspace/fixtures/user-data.json | 46 ---------- designsafe/apps/workspace/tests.py | 4 +- designsafe/fixtures/auth.json | 35 ++++++++ designsafe/fixtures/user-data.json | 67 +++++++++++++++ designsafe/settings/test_settings.py | 4 + 16 files changed, 111 insertions(+), 547 deletions(-) delete mode 100644 designsafe/apps/accounts/fixtures/user-data.json delete mode 100644 designsafe/apps/api/fixtures/user-data.json delete mode 100644 designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json delete mode 100644 designsafe/apps/api/notifications/fixtures/user-data.json delete mode 100644 designsafe/apps/box_integration/fixtures/user-data.json delete mode 100644 designsafe/apps/dashboard/fixtures/user-data.json delete mode 100644 designsafe/apps/rapid/fixtures/user-data.json delete mode 100644 designsafe/apps/token_access/fixtures/user-data.json delete mode 100644 designsafe/apps/workspace/fixtures/agave-oauth-token-data.json delete mode 100644 designsafe/apps/workspace/fixtures/user-data.json create mode 100644 designsafe/fixtures/auth.json create mode 100644 designsafe/fixtures/user-data.json diff --git a/designsafe/apps/accounts/fixtures/user-data.json b/designsafe/apps/accounts/fixtures/user-data.json deleted file mode 100644 index d3167a1a0b..0000000000 --- a/designsafe/apps/accounts/fixtures/user-data.json +++ /dev/null @@ -1,64 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "announcements": true, - "user": 2 - }, - "model": "designsafe_accounts.notificationpreferences", - "pk": 1 - } -] diff --git a/designsafe/apps/api/fixtures/user-data.json b/designsafe/apps/api/fixtures/user-data.json deleted file mode 100644 index 42379513c1..0000000000 --- a/designsafe/apps/api/fixtures/user-data.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "user_id": "2", - "token_type": "Bearer", - "scope": "PRODUCTION", - "access_token": "fakeaccesstoken", - "refresh_token": "fakerefreshtoken", - "expires_in": "14400", - "created": "1459433273" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 - } -] diff --git a/designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json b/designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json deleted file mode 100644 index c5204eb8a8..0000000000 --- a/designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json +++ /dev/null @@ -1,28 +0,0 @@ -[ -{ - "fields": { - "created": 1461727485, - "access_token": "dc48198091d73c8933c2c0ee96afb01b", - "expires_in": 14400, - "token_type": "bearer", - "user": 1, - "scope": "default", - "refresh_token": "2f715c8eb6962a883c7cd29af7d1165" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 -}, -{ - "fields": { - "created": 1463178660, - "access_token": "7834a55e92f3f9b86dc1627bff8d43", - "expires_in": 14400, - "token_type": "bearer", - "user": 2, - "scope": "default", - "refresh_token": "dc1c5b9a5124f88147c783e35b5ca9c" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 2 -} -] diff --git a/designsafe/apps/api/notifications/fixtures/user-data.json b/designsafe/apps/api/notifications/fixtures/user-data.json deleted file mode 100644 index 42379513c1..0000000000 --- a/designsafe/apps/api/notifications/fixtures/user-data.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "user_id": "2", - "token_type": "Bearer", - "scope": "PRODUCTION", - "access_token": "fakeaccesstoken", - "refresh_token": "fakerefreshtoken", - "expires_in": "14400", - "created": "1459433273" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 - } -] diff --git a/designsafe/apps/api/notifications/tests.py b/designsafe/apps/api/notifications/tests.py index cf692feaba..74d9704a8e 100644 --- a/designsafe/apps/api/notifications/tests.py +++ b/designsafe/apps/api/notifications/tests.py @@ -28,7 +28,7 @@ # Create your tests here. @skip("Need to mock websocket call to redis") class NotificationsTestCase(TestCase): - fixtures = ["user-data.json", "agave-oauth-token-data.json"] + fixtures = ["user-data.json", "auth.json"] def setUp(self): self.wh_url = reverse("designsafe_api:jobs_wh_handler") @@ -103,7 +103,7 @@ def test_2_webhooks_same_status_different_jobId_should_give_2_notifications(self class TestWebhookViews(TestCase): - fixtures = ["user-data", "agave-oauth-token-data"] + fixtures = ["user-data", "auth"] def setUp(self): self.wh_url = reverse("designsafe_api:jobs_wh_handler") diff --git a/designsafe/apps/api/tests.py b/designsafe/apps/api/tests.py index 38a7168eb7..1ad34c673e 100644 --- a/designsafe/apps/api/tests.py +++ b/designsafe/apps/api/tests.py @@ -17,7 +17,7 @@ # Create your tests here. class ProjectDataModelsTestCase(TestCase): - fixtures = ['user-data.json', 'agave-oauth-token-data.json'] + fixtures = ['user-data.json', 'auth.json'] def setUp(self): user = get_user_model().objects.get(pk=2) diff --git a/designsafe/apps/box_integration/fixtures/user-data.json b/designsafe/apps/box_integration/fixtures/user-data.json deleted file mode 100644 index 42379513c1..0000000000 --- a/designsafe/apps/box_integration/fixtures/user-data.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "user_id": "2", - "token_type": "Bearer", - "scope": "PRODUCTION", - "access_token": "fakeaccesstoken", - "refresh_token": "fakerefreshtoken", - "expires_in": "14400", - "created": "1459433273" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 - } -] diff --git a/designsafe/apps/dashboard/fixtures/user-data.json b/designsafe/apps/dashboard/fixtures/user-data.json deleted file mode 100644 index ce1d49e299..0000000000 --- a/designsafe/apps/dashboard/fixtures/user-data.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - } -] diff --git a/designsafe/apps/rapid/fixtures/user-data.json b/designsafe/apps/rapid/fixtures/user-data.json deleted file mode 100644 index d3167a1a0b..0000000000 --- a/designsafe/apps/rapid/fixtures/user-data.json +++ /dev/null @@ -1,64 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "announcements": true, - "user": 2 - }, - "model": "designsafe_accounts.notificationpreferences", - "pk": 1 - } -] diff --git a/designsafe/apps/token_access/fixtures/user-data.json b/designsafe/apps/token_access/fixtures/user-data.json deleted file mode 100644 index dd8fc339c5..0000000000 --- a/designsafe/apps/token_access/fixtures/user-data.json +++ /dev/null @@ -1,85 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "nickname": "Test Token", - "user": 2, - "created": "2016-09-06T00:00:00.000Z" - }, - "model": "token_access.token", - "pk": "5da84493fa0037de0945631d1f9df5c00cdcac49" - }, - - { - "model": "designsafe_accounts.designsafeprofile", - "pk": 5610, - "fields": { - "user": 2, - "ethnicity": "Asian", - "gender": "Male", - "agree_to_account_limit": "2020-07-02T23:41:19.342Z", - "bio": null, - "website": null, - "orcid_id": null, - "professional_level": null, - "update_required": true, - "last_updated": "2020-07-02T23:41:19.343Z", - "nh_interests": [], - "nh_technical_domains": [], - "research_activities": [] - } - } -] \ No newline at end of file diff --git a/designsafe/apps/workspace/fixtures/agave-oauth-token-data.json b/designsafe/apps/workspace/fixtures/agave-oauth-token-data.json deleted file mode 100644 index c5204eb8a8..0000000000 --- a/designsafe/apps/workspace/fixtures/agave-oauth-token-data.json +++ /dev/null @@ -1,28 +0,0 @@ -[ -{ - "fields": { - "created": 1461727485, - "access_token": "dc48198091d73c8933c2c0ee96afb01b", - "expires_in": 14400, - "token_type": "bearer", - "user": 1, - "scope": "default", - "refresh_token": "2f715c8eb6962a883c7cd29af7d1165" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 -}, -{ - "fields": { - "created": 1463178660, - "access_token": "7834a55e92f3f9b86dc1627bff8d43", - "expires_in": 14400, - "token_type": "bearer", - "user": 2, - "scope": "default", - "refresh_token": "dc1c5b9a5124f88147c783e35b5ca9c" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 2 -} -] diff --git a/designsafe/apps/workspace/fixtures/user-data.json b/designsafe/apps/workspace/fixtures/user-data.json deleted file mode 100644 index 1d36bec677..0000000000 --- a/designsafe/apps/workspace/fixtures/user-data.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "announcements": true, - "user": 2 - }, - "model": "designsafe_accounts.notificationpreferences", - "pk": 1 - } -] diff --git a/designsafe/apps/workspace/tests.py b/designsafe/apps/workspace/tests.py index 1e8d7088e5..a808ac506d 100644 --- a/designsafe/apps/workspace/tests.py +++ b/designsafe/apps/workspace/tests.py @@ -8,7 +8,7 @@ class AppDescriptionModelTest(TestCase): - fixtures = ["user-data", "agave-oauth-token-data"] + fixtures = ["user-data", "auth"] def setUp(self): user = get_user_model().objects.get(pk=2) @@ -32,7 +32,7 @@ def test_get_app_description(self): class TestAppsApiViews(TestCase): - fixtures = ["user-data", "agave-oauth-token-data"] + fixtures = ["user-data", "auth"] @classmethod def setUpClass(cls): diff --git a/designsafe/fixtures/auth.json b/designsafe/fixtures/auth.json new file mode 100644 index 0000000000..b338bfb9e9 --- /dev/null +++ b/designsafe/fixtures/auth.json @@ -0,0 +1,35 @@ +[ + { + "model": "designsafe_auth.tapisoauthtoken", + "pk": 1, + "fields": { + "user": 1, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536692280 + } + }, + { + "model": "designsafe_auth.tapisoauthtoken", + "pk": 2, + "fields": { + "user": 2, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536700041 + } + }, + { + "model": "designsafe_auth.tapisoauthtoken", + "pk": 3, + "fields": { + "user": 3, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536700084 + } + } +] diff --git a/designsafe/fixtures/user-data.json b/designsafe/fixtures/user-data.json new file mode 100644 index 0000000000..103ad32194 --- /dev/null +++ b/designsafe/fixtures/user-data.json @@ -0,0 +1,67 @@ +[ + { + "fields": { + "username": "ds_admin", + "first_name": "DesignSafe", + "last_name": "Admin", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "admin@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" + }, + "model": "auth.user", + "pk": 1 + }, + { + "fields": { + "username": "envision", + "first_name": "DesignSafe", + "last_name": "Admin", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "admin@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" + }, + "model": "auth.user", + "pk": 3 + }, + { + "fields": { + "username": "ds_user", + "first_name": "DesignSafe", + "last_name": "User", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "user@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" + }, + "model": "auth.user", + "pk": 2 + }, + { + "fields": { + "user": 2, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536700084 + }, + "model": "designsafe_auth.tapisoauthtoken", + "pk": 1 + } + ] diff --git a/designsafe/settings/test_settings.py b/designsafe/settings/test_settings.py index 1789ae1495..beda2a591a 100644 --- a/designsafe/settings/test_settings.py +++ b/designsafe/settings/test_settings.py @@ -234,6 +234,10 @@ MEDIA_ROOT = '/srv/www/designsafe/media/' MEDIA_URL = '/media/' +FIXTURE_DIRS = [ + os.path.join(BASE_DIR, 'designsafe', 'fixtures'), +] + ##### # From 9221e6c7895960dd47908087e942c73411973c73 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Tue, 5 Mar 2024 14:31:03 -0600 Subject: [PATCH 19/20] skip tapisv2 tests; add todov3 notes; fix fixture --- designsafe/apps/accounts/tests.py | 2 +- .../api/fixtures/agave-oauth-token-data.json | 28 --- designsafe/apps/api/notifications/tests.py | 1 + designsafe/apps/api/projects/tests.py | 12 +- designsafe/apps/workspace/tests.py | 3 + designsafe/fixtures/user-data.json | 160 +++++++++++------- 6 files changed, 110 insertions(+), 96 deletions(-) delete mode 100644 designsafe/apps/api/fixtures/agave-oauth-token-data.json diff --git a/designsafe/apps/accounts/tests.py b/designsafe/apps/accounts/tests.py index 7649deafb5..a0a95b0d9e 100644 --- a/designsafe/apps/accounts/tests.py +++ b/designsafe/apps/accounts/tests.py @@ -38,7 +38,7 @@ def test_mailing_list_access(self): self.client.login(username='ds_user', password='user/password') resp = self.client.get(url) self.assertEqual(resp.status_code, 403) - self.client.logout() + user = get_user_model().objects.get(pk=2) perm = Permission.objects.get(codename='view_notification_subscribers') user.user_permissions.add(perm) diff --git a/designsafe/apps/api/fixtures/agave-oauth-token-data.json b/designsafe/apps/api/fixtures/agave-oauth-token-data.json deleted file mode 100644 index c5204eb8a8..0000000000 --- a/designsafe/apps/api/fixtures/agave-oauth-token-data.json +++ /dev/null @@ -1,28 +0,0 @@ -[ -{ - "fields": { - "created": 1461727485, - "access_token": "dc48198091d73c8933c2c0ee96afb01b", - "expires_in": 14400, - "token_type": "bearer", - "user": 1, - "scope": "default", - "refresh_token": "2f715c8eb6962a883c7cd29af7d1165" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 -}, -{ - "fields": { - "created": 1463178660, - "access_token": "7834a55e92f3f9b86dc1627bff8d43", - "expires_in": 14400, - "token_type": "bearer", - "user": 2, - "scope": "default", - "refresh_token": "dc1c5b9a5124f88147c783e35b5ca9c" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 2 -} -] diff --git a/designsafe/apps/api/notifications/tests.py b/designsafe/apps/api/notifications/tests.py index 74d9704a8e..b0faf72ef8 100644 --- a/designsafe/apps/api/notifications/tests.py +++ b/designsafe/apps/api/notifications/tests.py @@ -102,6 +102,7 @@ def test_2_webhooks_same_status_different_jobId_should_give_2_notifications(self self.assertEqual(Notification.objects.count(), 2) +@skip("TODOv3: Update webhooks with Tapisv3") class TestWebhookViews(TestCase): fixtures = ["user-data", "auth"] diff --git a/designsafe/apps/api/projects/tests.py b/designsafe/apps/api/projects/tests.py index c2c63069e7..0b844416a2 100644 --- a/designsafe/apps/api/projects/tests.py +++ b/designsafe/apps/api/projects/tests.py @@ -1,18 +1,20 @@ from designsafe.apps.api.projects.fixtures import exp_instance_meta, exp_instance_resp, exp_entity_meta, exp_entity_json import pytest +@pytest.mark.skip(reason="TODOv3: Update projects with Tapisv3") @pytest.mark.django_db -def test_project_instance_get(client, mock_agave_client, authenticated_user): - mock_agave_client.meta.getMetadata.return_value = exp_instance_meta +def test_project_instance_get(client, mock_tapis_client, authenticated_user): + mock_tapis_client.meta.getMetadata.return_value = exp_instance_meta resp = client.get('/api/projects/1052668239654088215-242ac119-0001-012/') actual = resp.json() expected = exp_instance_resp assert actual == expected +@pytest.mark.skip(reason="TODOv3: Update projects with Tapisv3") @pytest.mark.django_db -def test_project_meta_all(client, mock_agave_client, authenticated_user): - mock_agave_client.meta.getMetadata.return_value = exp_instance_meta - mock_agave_client.meta.listMetadata.return_value = exp_entity_meta +def test_project_meta_all(client, mock_tapis_client, authenticated_user): + mock_tapis_client.meta.getMetadata.return_value = exp_instance_meta + mock_tapis_client.meta.listMetadata.return_value = exp_entity_meta resp = client.get('/api/projects/1052668239654088215-242ac119-0001-012/meta/all/') actual = resp.json() expected = exp_entity_json diff --git a/designsafe/apps/workspace/tests.py b/designsafe/apps/workspace/tests.py index a808ac506d..0e09e88518 100644 --- a/designsafe/apps/workspace/tests.py +++ b/designsafe/apps/workspace/tests.py @@ -2,10 +2,12 @@ from mock import patch from django.test import TestCase from .models.app_descriptions import AppDescription +from unittest import skip from django.urls import reverse from django.contrib.auth import get_user_model +@skip("TODOv3: Update apps api with Tapisv3") class AppDescriptionModelTest(TestCase): fixtures = ["user-data", "auth"] @@ -31,6 +33,7 @@ def test_get_app_description(self): self.assertContains(response, "TestApp0.1") +@skip("TODOv3: Update apps api with Tapisv3") class TestAppsApiViews(TestCase): fixtures = ["user-data", "auth"] diff --git a/designsafe/fixtures/user-data.json b/designsafe/fixtures/user-data.json index 103ad32194..5d1cb1c5db 100644 --- a/designsafe/fixtures/user-data.json +++ b/designsafe/fixtures/user-data.json @@ -1,67 +1,103 @@ [ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 + { + "fields": { + "username": "ds_admin", + "first_name": "DesignSafe", + "last_name": "Admin", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "admin@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 + "model": "auth.user", + "pk": 1 + }, + { + "fields": { + "username": "envision", + "first_name": "DesignSafe", + "last_name": "Admin", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "admin@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 + "model": "auth.user", + "pk": 3 + }, + { + "fields": { + "username": "ds_user", + "first_name": "DesignSafe", + "last_name": "User", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "user@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" }, - { - "fields": { - "user": 2, - "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", - "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", - "expires_in": 1325391984000, - "created": 1536700084 - }, - "model": "designsafe_auth.tapisoauthtoken", - "pk": 1 + "model": "auth.user", + "pk": 2 + }, + { + "fields": { + "user": 2, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536700084 + }, + "model": "designsafe_auth.tapisoauthtoken", + "pk": 1 + }, + { + "fields": { + "nickname": "Test Token", + "user": 2, + "created": "2016-09-06T00:00:00.000Z" + }, + "model": "token_access.token", + "pk": "5da84493fa0037de0945631d1f9df5c00cdcac49" + }, + { + "model": "designsafe_accounts.designsafeprofile", + "pk": 5610, + "fields": { + "user": 2, + "ethnicity": "Asian", + "gender": "Male", + "agree_to_account_limit": "2020-07-02T23:41:19.342Z", + "bio": null, + "website": null, + "orcid_id": null, + "professional_level": null, + "update_required": true, + "last_updated": "2020-07-02T23:41:19.343Z", + "nh_interests": [], + "nh_technical_domains": [], + "research_activities": [] } - ] + }, + { + "fields": { + "announcements": true, + "user": 2 + }, + "model": "designsafe_accounts.notificationpreferences", + "pk": 1 + } +] From 094ec40f903bcc2509b1498b4e0bf1d919f977d3 Mon Sep 17 00:00:00 2001 From: Sal Tijerina <r.sal.tijerina@gmail.com> Date: Tue, 5 Mar 2024 14:33:56 -0600 Subject: [PATCH 20/20] remove unnecessary commented code --- designsafe/apps/auth/urls.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/designsafe/apps/auth/urls.py b/designsafe/apps/auth/urls.py index 678ee5642f..f52fb4ef44 100644 --- a/designsafe/apps/auth/urls.py +++ b/designsafe/apps/auth/urls.py @@ -13,30 +13,3 @@ path("tapis/", views.tapis_oauth, name="tapis_oauth"), path("tapis/callback/", views.tapis_oauth_callback, name="tapis_oauth_callback"), ] - - -# from django.urls import reverse -# from django.utils.translation import gettext_lazy as _ - -# def menu_items(**kwargs): -# if 'type' in kwargs and kwargs['type'] == 'account': -# return [ -# { -# 'label': _('Login'), -# 'url': reverse('login'), -# 'children': [], -# 'visible': False -# }, -# { -# 'label': _('Login'), -# 'url': reverse('designsafe_auth:login'), -# 'children': [], -# 'visible': False -# }, -# { -# 'label': _('Agave'), -# 'url': reverse('designsafe_auth:agave_session_error'), -# 'children': [], -# 'visible': False -# } -# ]