diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a0a10fb..5cd427de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,39 +1,35 @@ repos: -- repo: https://github.com/ambv/black - rev: 22.3.0 - hooks: - - id: black - language_version: python3 - args: - - --target-version=py310 -- repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - additional_dependencies: [Flake8-pyproject] -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-ast - - id: name-tests-test - args: ["--pytest-test-first"] -- repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. If bumping this, please also bump requirements-dev.in + rev: v0.8.2 + hooks: + # Run the linter. + - id: ruff + args: [--fix] + # Run the formatter. + - id: ruff-format + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 hooks: - - id: reorder-python-imports - name: Reorder Python imports (src, tests) - args: ["--application-directories", "src"] -- repo: https://github.com/Riverside-Healthcare/djLint + - id: detect-secrets + args: + [ + "--disable-plugin", + "HexHighEntropyString", + "--disable-plugin", + "Base64HighEntropyString", + ] + exclude: tests/keys/rsa256 + # djLint moved in from assess - to apply as a precommit hook we should make sure it works with + # apply templates + - repo: https://github.com/Riverside-Healthcare/djLint rev: v1.24.0 hooks: - id: djlint-jinja - types_or: ['html', 'jinja'] -- repo: https://github.com/Yelp/detect-secrets - rev: v1.4.0 - hooks: - - id: detect-secrets - args: ['--disable-plugin', 'HexHighEntropyString', - '--disable-plugin', 'Base64HighEntropyString'] - exclude: tests/keys/rsa256 + types_or: ["html", "jinja"] diff --git a/api/magic_links/routes.py b/api/magic_links/routes.py index 753a47c9..406d172a 100644 --- a/api/magic_links/routes.py +++ b/api/magic_links/routes.py @@ -1,17 +1,13 @@ import json from datetime import datetime -from urllib.parse import urlencode -from urllib.parse import urljoin +from urllib.parse import urlencode, urljoin -from api.session.auth_session import AuthSessionView -from config import Config -from flask import current_app -from flask import g -from flask import redirect -from flask import request -from flask import url_for +from flask import current_app, g, redirect, request, url_for from flask.views import MethodView from fsd_utils.authentication.decorators import login_requested + +from api.session.auth_session import AuthSessionView +from config import Config from models.account import AccountMethods from models.magic_link import MagicLinkMethods @@ -52,7 +48,10 @@ def use(self, link_id: str): # Check account exists account = AccountMethods.get_account(account_id=link.get("accountId")) if not account: - current_app.logger.error(f"Tried to use magic link for non-existent account_id {link.get('accountId')}") + current_app.logger.error( + "Tried to use magic link for non-existent account_id {account_id}", + extra=dict(account_id=link.get("accountId")), + ) redirect( url_for( "magic_links_bp.invalid", @@ -91,10 +90,11 @@ def use(self, link_id: str): query_string = urlencode(query_params) frontend_account_url = urljoin(Config.APPLICANT_FRONTEND_HOST, f"account?{query_string}") current_app.logger.warning( - f"The magic link with hash: '{link_hash}' has already been" - f" used but the user with account_id: '{g.account_id}' is" + "The magic link with hash: '{link_hash}' has already been" + " used but the user with account_id: '{account_id}' is" " logged in, redirecting to" - f" '{frontend_account_url}'." + " '{frontend_account_url}'.", + extra=dict(link_hash=link_hash, account_id=g.account_id, frontend_account_url=frontend_account_url), ) return redirect(frontend_account_url) return redirect( diff --git a/api/session/auth_session.py b/api/session/auth_session.py index f13bcb12..0e8f64cf 100644 --- a/api/session/auth_session.py +++ b/api/session/auth_session.py @@ -1,24 +1,16 @@ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from typing import TYPE_CHECKING import jwt +from flask import abort, current_app, make_response, redirect, request, session, url_for +from flask.views import MethodView +from fsd_utils import clear_sentry + from api.responses import error_response from api.session.exceptions import SessionCreateError from config import Config -from flask import abort -from flask import current_app -from flask import make_response -from flask import redirect -from flask import request -from flask import session -from flask import url_for -from flask.views import MethodView -from fsd_utils import clear_sentry from models.magic_link import MagicLinkMethods -from security.utils import create_token -from security.utils import decode_with_options -from security.utils import validate_token +from security.utils import create_token, decode_with_options, validate_token if TYPE_CHECKING: from models.account import Account as Account @@ -74,7 +66,9 @@ def clear_session(return_app=None, return_path=None): valid_token = decode_with_options(existing_auth_token, options={"verify_exp": False}) status = "expired_token" except jwt.PyJWTError as e: - current_app.logger.warning(f"PyJWTError: {e.__class__.__name__} - {e}") + current_app.logger.warning( + "PyJWTError: {error_name} - {error}", extra=dict(error_name=e.__class__.__name__), error=e + ) status = "invalid_token" # If validly issued token: create query params for signout url, @@ -90,9 +84,12 @@ def clear_session(return_app=None, return_path=None): if return_app: if safe_app := Config.SAFE_RETURN_APPS.get(return_app): redirect_route = safe_app.logout_endpoint - current_app.logger.info(f"Returning to {return_app} using {redirect_route}") + current_app.logger.info( + "Returning to {return_app} using {redirect_route}", + extra=dict(return_app=return_app, redirect_route=redirect_route), + ) else: - current_app.logger.warning(f"{return_app} not listed as a safe app.") + current_app.logger.warning("{return_app} not listed as a safe app.", extra=dict(return_app=return_app)) abort(400, "Unknown return app.") # Clear the cookie and redirect to signed out page @@ -184,7 +181,7 @@ def create_session_and_redirect( samesite=Config.FSD_USER_TOKEN_COOKIE_SAMESITE, httponly=Config.SESSION_COOKIE_HTTPONLY, ) - current_app.logger.info(f"User logged in to account : {account.id}") + current_app.logger.info("User logged in to account : {account_id}", extra=dict(account_id=account.id)) return response except SessionCreateError as e: error_response(404, str(e)) diff --git a/api/sso/routes.py b/api/sso/routes.py index e1820213..de9faba4 100644 --- a/api/sso/routes.py +++ b/api/sso/routes.py @@ -1,20 +1,13 @@ -import warnings -from urllib.parse import urlencode -from urllib.parse import urljoin -from urllib.parse import urlparse +from urllib.parse import urlencode, urljoin, urlparse import msal import requests -from api.session.auth_session import AuthSessionView -from config import Config -from flask import abort -from flask import current_app -from flask import make_response -from flask import redirect -from flask import request -from flask import session +from flask import abort, current_app, make_response, redirect, request, session from flask.views import MethodView from fsd_utils import clear_sentry + +from api.session.auth_session import AuthSessionView +from config import Config from models.account import AccountMethods @@ -30,7 +23,9 @@ def login(self): if return_app := request.args.get("return_app"): session["return_app"] = return_app session["return_path"] = request.args.get("return_path") - current_app.logger.debug(f"Setting return app to {return_app} for this session") + current_app.logger.debug( + "Setting return app to {return_app} for this session", extra=dict(return_app=return_app) + ) return redirect(session["flow"]["auth_uri"]), 302 @@ -104,7 +99,7 @@ def get_token(self): session["user"] = result.get("id_token_claims") self._save_cache(cache) except ValueError as e: # Usually caused by CSRF - warnings.warn(f"Value Error on get_token route: {str(e)}") + current_app.logger.warning("Value Error on get_token route: {error}", extra=dict(error=str(e))) if "user" not in session or not session["user"].get("sub"): return {"message": "No valid token"}, 404 @@ -126,9 +121,12 @@ def get_token(self): else: redirect_url = safe_app.login_url - current_app.logger.info(f"Returning to {return_app} @ {redirect_url}") + current_app.logger.info( + "Returning to {return_app} @ {redirect_url}", + extra=dict(return_app=return_app, redirect_url=redirect_url), + ) else: - current_app.logger.warning(f"{return_app} not listed as a safe app.") + current_app.logger.warning("{return_app} not listed as a safe app.", extra=dict(return_app=return_app)) abort(400, "Unknown return app.") # Create session token, set cookie and redirect diff --git a/app.py b/app.py index 83ab26e6..3356e193 100644 --- a/app.py +++ b/app.py @@ -1,36 +1,29 @@ from copy import deepcopy from os import getenv from pathlib import Path -from typing import Any -from typing import Dict -from urllib.parse import urlencode -from urllib.parse import urljoin +from typing import Any, Dict +from urllib.parse import urlencode, urljoin import connexion import prance -import static_assets -from config import Config from connexion.resolver import MethodViewResolver -from flask import Flask -from flask import request +from flask import Flask, request from flask_assets import Environment -from flask_babel import Babel -from flask_babel import gettext +from flask_babel import Babel, gettext from flask_redis import FlaskRedis from flask_session import Session from flask_talisman import Talisman -from frontend.magic_links.filters import datetime_format -from fsd_utils import init_sentry -from fsd_utils import LanguageSelector -from fsd_utils.healthchecks.checkers import FlaskRunningChecker -from fsd_utils.healthchecks.checkers import RedisChecker +from fsd_utils import LanguageSelector, init_sentry +from fsd_utils.healthchecks.checkers import FlaskRunningChecker, RedisChecker from fsd_utils.healthchecks.healthcheck import Healthcheck from fsd_utils.locale_selector.get_lang import get_lang from fsd_utils.logging import logging from fsd_utils.services.aws_extended_client import SQSExtendedClient -from jinja2 import ChoiceLoader -from jinja2 import PackageLoader -from jinja2 import PrefixLoader +from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader + +import static_assets +from config import Config +from frontend.magic_links.filters import datetime_format from models.fund import FundMethods redis_mlinks = FlaskRedis(config_prefix="REDIS_MLINKS") diff --git a/build.py b/build.py index 043ca0d1..091d6d03 100644 --- a/build.py +++ b/build.py @@ -7,7 +7,6 @@ def build_govuk_assets(static_dist_root="frontend/static/dist"): - DIST_ROOT = "./" + static_dist_root GOVUK_URL = "https://github.com/alphagov/govuk-frontend/releases/download/v4.8.0/release-v4.8.0.zip" ZIP_FILE = "./govuk_frontend.zip" diff --git a/config/envs/default.py b/config/envs/default.py index d5ecfc8e..a7181c13 100644 --- a/config/envs/default.py +++ b/config/envs/default.py @@ -1,19 +1,17 @@ """Flask configuration.""" + import base64 import logging from collections import namedtuple -from os import environ -from os import getenv +from distutils.util import strtobool +from os import environ, getenv from pathlib import Path from urllib.parse import urljoin import redis -from distutils.util import strtobool -from fsd_utils import CommonConfig -from fsd_utils import configclass +from fsd_utils import CommonConfig, configclass from fsd_utils.authentication.config import SupportedApp - SafeAppConfig = namedtuple("SafeAppConfig", ("login_url", "logout_endpoint", "service_title")) @@ -57,8 +55,7 @@ class DefaultConfig(object): AZURE_AD_TENANT_ID = environ.get("AZURE_AD_TENANT_ID", "") AZURE_AD_AUTHORITY = ( # consumers|organizations| - signifies the Azure AD tenant endpoint # noqa - "https://login.microsoftonline.com/" - + AZURE_AD_TENANT_ID + "https://login.microsoftonline.com/" + AZURE_AD_TENANT_ID ) # The absolute URL must match the redirect URI you set # in the app's registration in the Azure portal. diff --git a/config/envs/dev.py b/config/envs/dev.py index a43bd6b5..9d1ae9e4 100644 --- a/config/envs/dev.py +++ b/config/envs/dev.py @@ -1,9 +1,11 @@ """Flask Dev Pipeline Environment Configuration.""" + import logging -from config.envs.default import DefaultConfig as Config from fsd_utils import configclass +from config.envs.default import DefaultConfig as Config + @configclass class DevConfig(Config): diff --git a/config/envs/development.py b/config/envs/development.py index 57098212..e7516863 100644 --- a/config/envs/development.py +++ b/config/envs/development.py @@ -1,10 +1,12 @@ """Flask Local Development Environment Configuration.""" + import logging from os import getenv -from config.envs.default import DefaultConfig as Config from fsd_utils import configclass +from config.envs.default import DefaultConfig as Config + @configclass class DevelopmentConfig(Config): diff --git a/config/envs/production.py b/config/envs/production.py index 3b67aaea..8ba40811 100644 --- a/config/envs/production.py +++ b/config/envs/production.py @@ -1,10 +1,12 @@ """Flask Production Environment Configuration.""" -from os import getenv -from config.envs.default import DefaultConfig as Config from distutils.util import strtobool +from os import getenv + from fsd_utils import configclass +from config.envs.default import DefaultConfig as Config + @configclass class ProductionConfig(Config): diff --git a/config/envs/test.py b/config/envs/test.py index 92fb2847..33cc843c 100644 --- a/config/envs/test.py +++ b/config/envs/test.py @@ -1,16 +1,16 @@ """Flask Test Environment Configuration.""" -import base64 -from os import environ -from os import getenv -from config.envs.default import DefaultConfig as Config +import base64 from distutils.util import strtobool +from os import environ, getenv + from fsd_utils import configclass +from config.envs.default import DefaultConfig as Config + @configclass class TestConfig(Config): - SECRET_KEY = environ.get("SECRET_KEY", "test") COOKIE_DOMAIN = environ.get("COOKIE_DOMAIN", ".test.fundingservice.co.uk") diff --git a/config/envs/unit_test.py b/config/envs/unit_test.py index 4035576b..39fd20e3 100644 --- a/config/envs/unit_test.py +++ b/config/envs/unit_test.py @@ -1,13 +1,15 @@ """Flask Local Development Environment Configuration.""" + import logging +from distutils.util import strtobool from os import getenv -from config.envs.default import DefaultConfig as Config -from config.envs.default import SafeAppConfig -from distutils.util import strtobool from fsd_utils import configclass from fsd_utils.authentication.config import SupportedApp +from config.envs.default import DefaultConfig as Config +from config.envs.default import SafeAppConfig + @configclass class UnitTestConfig(Config): @@ -29,8 +31,7 @@ class UnitTestConfig(Config): AZURE_AD_AUTHORITY = ( # consumers|organizations| # - signifies the Azure AD tenant endpoint - "https://login.microsoftonline.com/" - + AZURE_AD_TENANT_ID + "https://login.microsoftonline.com/" + AZURE_AD_TENANT_ID ) SESSION_TYPE = ( diff --git a/frontend/default/routes.py b/frontend/default/routes.py index d05bc611..8c2e4ec7 100644 --- a/frontend/default/routes.py +++ b/frontend/default/routes.py @@ -1,8 +1,6 @@ import traceback -from flask import Blueprint -from flask import current_app -from flask import render_template +from flask import Blueprint, current_app, render_template default_bp = Blueprint("default_bp", __name__, template_folder="templates") @@ -21,6 +19,8 @@ def not_found(error): def internal_server_error(error): error_message = f"Encountered 500: {error}" stack_trace = traceback.format_exc() - current_app.logger.error(f"{error_message}\n{stack_trace}") + current_app.logger.error( + "{error_message}\n{stack_trace}", extra=dict(error_message=error_message, stack_trace=stack_trace) + ) return render_template("500.html", is_error=True), 500 diff --git a/frontend/magic_links/filters.py b/frontend/magic_links/filters.py index b9c38f65..802475da 100644 --- a/frontend/magic_links/filters.py +++ b/frontend/magic_links/filters.py @@ -1,7 +1,6 @@ from datetime import datetime -from flask_babel import format_datetime -from flask_babel import gettext +from flask_babel import format_datetime, gettext def datetime_format(value: str) -> str: diff --git a/frontend/magic_links/forms.py b/frontend/magic_links/forms.py index 7ad8ac0b..0e8cba58 100644 --- a/frontend/magic_links/forms.py +++ b/frontend/magic_links/forms.py @@ -1,8 +1,6 @@ -from flask_babel import gettext -from flask_babel import lazy_gettext +from flask_babel import gettext, lazy_gettext from flask_wtf import FlaskForm -from wtforms import EmailField -from wtforms import HiddenField +from wtforms import EmailField, HiddenField from wtforms.validators import Email diff --git a/frontend/magic_links/routes.py b/frontend/magic_links/routes.py index 9559bb1b..52d22b77 100644 --- a/frontend/magic_links/routes.py +++ b/frontend/magic_links/routes.py @@ -1,22 +1,14 @@ import uuid +from flask import Blueprint, abort, current_app, g, redirect, render_template, request, url_for +from fsd_utils.authentication.decorators import login_requested + from config import Config -from flask import abort -from flask import Blueprint -from flask import current_app -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask import url_for from frontend.magic_links.forms import EmailForm -from fsd_utils.authentication.decorators import login_requested -from models.account import AccountError -from models.account import AccountMethods +from models.account import AccountError, AccountMethods from models.data import get_round_data from models.fund import FundMethods -from models.magic_link import MagicLinkError -from models.magic_link import MagicLinkMethods +from models.magic_link import MagicLinkError, MagicLinkMethods from models.notification import NotificationError magic_links_bp = Blueprint( @@ -151,7 +143,9 @@ def new(): ) if Config.AUTO_REDIRECT_LOGIN: - current_app.logger.info(f"Auto redirecting to magic link: {created_link}") + current_app.logger.info( + "Auto redirecting to magic link: {created_link}", extra=dict(created_link=created_link) + ) return redirect(created_link) return redirect( diff --git a/frontend/sso/routes.py b/frontend/sso/routes.py index 606c7ec8..8ff31ef5 100644 --- a/frontend/sso/routes.py +++ b/frontend/sso/routes.py @@ -1,7 +1,4 @@ -from flask import Blueprint -from flask import render_template -from flask import request -from flask import url_for +from flask import Blueprint, render_template, request, url_for sso_bp = Blueprint( "sso_bp", diff --git a/frontend/user/routes.py b/frontend/user/routes.py index 69eaa03c..494f068f 100644 --- a/frontend/user/routes.py +++ b/frontend/user/routes.py @@ -1,11 +1,8 @@ -from config import Config -from flask import Blueprint -from flask import g -from flask import render_template -from flask import request -from flask import url_for +from flask import Blueprint, g, render_template, request, url_for from fsd_utils.authentication.decorators import login_requested +from config import Config + user_bp = Blueprint( "user_bp", __name__, diff --git a/models/account.py b/models/account.py index 6fac4bf5..e780cd4d 100644 --- a/models/account.py +++ b/models/account.py @@ -1,15 +1,12 @@ from dataclasses import dataclass from typing import List -import config -from config import Config from flask import current_app from fsd_utils.config.notify_constants import NotifyConstants -from models.data import get_account_data -from models.data import get_data -from models.data import get_round_data -from models.data import post_data -from models.data import put_data + +import config +from config import Config +from models.data import get_account_data, get_data, get_round_data, post_data, put_data from models.fund import FundMethods from models.magic_link import MagicLinkMethods from models.notification import Notification @@ -222,7 +219,7 @@ def get_magic_link( round_short_name=round_short_name if round_short_name else "", ) notification_content.update({NotifyConstants.MAGIC_LINK_URL_FIELD: new_link_json.get("link")}) # noqa - current_app.logger.debug(f"Magic Link URL: {new_link_json.get('link')}") + current_app.logger.debug("Magic Link URL: {link}", extra=dict(link=new_link_json.get("link"))) # Send notification Notification.send( NotifyConstants.TEMPLATE_TYPE_MAGIC_LINK, @@ -232,5 +229,7 @@ def get_magic_link( ) return new_link_json.get("link") - current_app.logger.error(f"Could not create an account ({account}) for email '{email}'") + current_app.logger.error( + "Could not create an account ({account}) for email '{email}'", extra=dict(account=account, email=email) + ) raise AccountError(message="Sorry, we couldn't create an account for this email, please contact support") diff --git a/models/data.py b/models/data.py index 048725c8..acffb217 100644 --- a/models/data.py +++ b/models/data.py @@ -3,9 +3,10 @@ import urllib.parse import requests -from config import Config from flask import current_app from fsd_utils.locale_selector.get_lang import get_lang + +from config import Config from models.round import Round @@ -56,7 +57,9 @@ def put_data(endpoint: str, params: dict = None): if response.status_code in [200, 201]: return response.json() else: - current_app.logger.error("API error response of : " + str(response.json())) + current_app.logger.error( + "API error response of : {response_string}", extra=dict(response_string=str(response.json())) + ) else: return local_api_call(endpoint, params, "put") diff --git a/models/fund.py b/models/fund.py index 55e4a65d..a898d05c 100644 --- a/models/fund.py +++ b/models/fund.py @@ -1,9 +1,10 @@ from dataclasses import dataclass from typing import List -from config import Config from flask import request from fsd_utils.locale_selector.get_lang import get_lang + +from config import Config from models.data import get_data from models.round import Round diff --git a/models/magic_link.py b/models/magic_link.py index 6dda16a3..29c87697 100644 --- a/models/magic_link.py +++ b/models/magic_link.py @@ -4,15 +4,14 @@ import random import string from dataclasses import dataclass -from datetime import datetime -from datetime import timedelta -from typing import List -from typing import TYPE_CHECKING +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, List from urllib.parse import urljoin -from config import Config from flask import current_app from flask_redis import FlaskRedis + +from config import Config from security.utils import create_token if TYPE_CHECKING: @@ -176,7 +175,7 @@ def create_magic_link( :param redirect_url: (str, optional) An optional redirect_url :return: """ - current_app.logger.info(f"Creating magic link for {account}") + current_app.logger.info("Creating magic link for {account}", extra=dict(account=account)) if not redirect_url: redirect_url = urljoin( @@ -195,7 +194,6 @@ def create_magic_link( self._create_user_record(account, redis_key) if fund_short_name and round_short_name: - magic_link_url = ( Config.AUTHENTICATOR_HOST + Config.MAGIC_LINK_LANDING_PAGE @@ -215,8 +213,8 @@ def create_magic_link( "link": magic_link_url, } ) - current_app.logger.info(f"Magic link created for {account}") + current_app.logger.info("Magic link created for {account}", extra=dict(account=account)) return new_link_json - current_app.logger.error(f"Magic link for account {account} could not be created") + current_app.logger.error("Magic link for account {account} could not be created", extra=dict(account=account)) raise MagicLinkError(message="Could not create a magic link") diff --git a/models/notification.py b/models/notification.py index da478a0a..f175bd46 100644 --- a/models/notification.py +++ b/models/notification.py @@ -2,10 +2,11 @@ import textwrap from uuid import uuid4 -from config import Config from flask import current_app from fsd_utils.config.notify_constants import NotifyConstants +from config import Config + NOTIFICATION_CONST = "notification" NOTIFICATION_S3_KEY_CONST = "auth/notification" @@ -38,7 +39,7 @@ def send(template_type: str, to_email: str, content: dict, govuk_notify_referenc - Content: """ ) - current_app.logger.info(f"{template_msg}{json.dumps(content, indent=4)}") + current_app.logger.info("{template_msg}", extra=dict(template_msg=template_msg)) return True params = { @@ -63,12 +64,14 @@ def send(template_type: str, to_email: str, content: dict, govuk_notify_referenc }, }, ) - current_app.logger.info(f"Message sent to SQS queue and message id is [{message_id}]") + current_app.logger.info( + "Message sent to SQS queue and message id is {message_id}", extra=dict(message_id=message_id) + ) return True except Exception as e: current_app.logger.error("An error occurred while sending message") current_app.logger.error(e) - raise NotificationError(message="Sorry, the notification could not be sent") + raise NotificationError(message="Sorry, the notification could not be sent") from e @staticmethod def _get_sqs_client(): diff --git a/pyproject.toml b/pyproject.toml index 1f444af4..93b006db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,24 +28,46 @@ dependencies = [ "requests==2.32.3", ] -[tool.black] +[tool.djlint] +# run with : `djlint path/to/file.html --reformat --format-css --format-js` +# this is deliberately commented out. we don't want to format these tags as +# it will introduce new lines and tabs, making the translation matching brittle. +# custom_blocks="trans,endtrans" +max_line_length=1000 # high limit, we don't want line breaks for translations. +max_attribute_length=1000 # ^^^ +exclude=".venv,venv" +profile="jinja2" + +[tool.ruff] line-length = 120 -experimental-string-processing = 1 -[tool.flake8] -max-line-length = 120 -count = true +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "W", # pycodestyle + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C90", # mccabe cyclomatic complexity + "G", # flake8-logging-format +] +ignore = [] +exclude = [ + ".venv*", + "__pycache__", +] +mccabe.max-complexity = 12 [tool.uv] [dependency-groups] dev = [ "beautifulsoup4==4.12.3", - "black==22.12.0", "debugpy==1.6.7", "deepdiff==5.8.1", "dparse==0.6.4", - "flake8-pyproject==1.2.3", "invoke==2.0.0", "moto==5.0.12", "pre-commit==4.0.1", @@ -57,4 +79,5 @@ dev = [ "selenium==4.23.1", "swagger-ui-bundle==0.0.9", "webdriver-manager==4.0.1", + "ruff>=0.8.2", ] diff --git a/security/utils.py b/security/utils.py index 91487429..6dda16c6 100644 --- a/security/utils.py +++ b/security/utils.py @@ -1,4 +1,5 @@ import jwt + from config import Config algorithm = "RS256" diff --git a/static_assets.py b/static_assets.py index 69e0a0a6..9c2c69f7 100644 --- a/static_assets.py +++ b/static_assets.py @@ -1,9 +1,9 @@ """Compile static assets.""" + from os import path from flask import Flask -from flask_assets import Bundle -from flask_assets import Environment +from flask_assets import Bundle, Environment def init_assets(app=None, auto_build=False, static_folder="frontend/static/dist"): diff --git a/testing/mocks/mocks/flask_test_client.py b/testing/mocks/mocks/flask_test_client.py index 67b0d5eb..b457f847 100644 --- a/testing/mocks/mocks/flask_test_client.py +++ b/testing/mocks/mocks/flask_test_client.py @@ -1,4 +1,5 @@ import pytest + from app import create_app diff --git a/testing/mocks/mocks/live_server.py b/testing/mocks/mocks/live_server.py index 6a5053c6..8adb2e4c 100644 --- a/testing/mocks/mocks/live_server.py +++ b/testing/mocks/mocks/live_server.py @@ -1,4 +1,5 @@ import pytest + from app import create_app from testing.mocks.mocks.redis_sessions import RedisSessions diff --git a/testing/mocks/mocks/redis_magic_links.py b/testing/mocks/mocks/redis_magic_links.py index 2e8d8d3c..3094a2f6 100644 --- a/testing/mocks/mocks/redis_magic_links.py +++ b/testing/mocks/mocks/redis_magic_links.py @@ -1,6 +1,7 @@ """ Mock redis magic links db """ + import pytest ml_data = {} diff --git a/testing/mocks/mocks/redis_sessions.py b/testing/mocks/mocks/redis_sessions.py index 89080a43..da943e2c 100644 --- a/testing/mocks/mocks/redis_sessions.py +++ b/testing/mocks/mocks/redis_sessions.py @@ -1,6 +1,7 @@ """ Mock redis sessions db """ + import pytest session_data = {} diff --git a/testing/mocks/utils.py b/testing/mocks/utils.py index 528c68fe..31ed5f8b 100644 --- a/testing/mocks/utils.py +++ b/testing/mocks/utils.py @@ -1,6 +1,7 @@ """ Utility functions for running tests and generating reports """ + import os diff --git a/tests/conftest.py b/tests/conftest.py index 17bf6d32..e880b956 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import pytest -from app import create_app from flask import current_app + +from app import create_app from models.account import AccountMethods from testing.mocks.mocks import * # noqa diff --git a/tests/test_account.py b/tests/test_account.py index 9c7f9431..52d04179 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,6 +1,6 @@ import pytest -from models.account import Account -from models.account import AccountMethods + +from models.account import Account, AccountMethods from models.fund import Fund from models.round import Round @@ -34,7 +34,6 @@ def mock_get_account(mocker, request): @pytest.fixture(scope="function") def mock_create_account(mocker): - mocker.patch( "models.account.post_data", return_value={ @@ -48,7 +47,6 @@ def mock_create_account(mocker): @pytest.fixture(scope="function") def mock_update_account(mocker): - mocker.patch( "models.account.put_data", return_value={ diff --git a/tests/test_filters.py b/tests/test_filters.py index 135279c6..28df16ec 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,5 +1,6 @@ -import frontend.magic_links.filters as filters import pytest + +import frontend.magic_links.filters as filters from app import app diff --git a/tests/test_fund.py b/tests/test_fund.py index 28943dce..1c1454b6 100644 --- a/tests/test_fund.py +++ b/tests/test_fund.py @@ -2,8 +2,7 @@ from config.envs.default import DefaultConfig from config.envs.unit_test import UnitTestConfig -from models.fund import Fund -from models.fund import FundMethods +from models.fund import Fund, FundMethods class TestFund: diff --git a/tests/test_healthchecks.py b/tests/test_healthchecks.py index 833bcb6a..56b2935a 100644 --- a/tests/test_healthchecks.py +++ b/tests/test_healthchecks.py @@ -1,6 +1,7 @@ from unittest import mock import pytest + from app import create_app from config import Config diff --git a/tests/test_magic_links.py b/tests/test_magic_links.py index 4f333e51..25974fbb 100644 --- a/tests/test_magic_links.py +++ b/tests/test_magic_links.py @@ -1,14 +1,16 @@ """ Test magic links functionality """ + import unittest.mock from unittest import mock -import frontend import pytest +from bs4 import BeautifulSoup + +import frontend from api.session.auth_session import AuthSessionView from app import app -from bs4 import BeautifulSoup from frontend.magic_links.forms import EmailForm from models.account import AccountMethods from security.utils import validate_token @@ -119,9 +121,10 @@ def test_reused_magic_link_with_active_session_shows_landing(self, flask_test_cl use_endpoint = f"/magic-links/{link_key}" landing_endpoint = f"/service/magic-links/landing/{link_key}?fund=cof&round=r2w3" - with mock.patch("models.fund.FundMethods.get_fund") as mock_get_fund, mock.patch( - "frontend.magic_links.routes.get_round_data" - ) as mock_get_round_data: + with ( + mock.patch("models.fund.FundMethods.get_fund") as mock_get_fund, + mock.patch("frontend.magic_links.routes.get_round_data") as mock_get_round_data, + ): # Mock get_fund() called in get_magic_link() mock_fund = mock.MagicMock() mock_fund.configure_mock(name="cof") diff --git a/tests/test_notification.py b/tests/test_notification.py index 41ece6b0..5aa96cf2 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -4,11 +4,10 @@ import boto3 import pytest from fsd_utils.services.aws_extended_client import SQSExtendedClient -from models.notification import Config -from models.notification import Notification -from models.notification import NotificationError from moto import mock_aws +from models.notification import Config, Notification, NotificationError + @pytest.fixture def disable_notifications(monkeypatch): @@ -28,7 +27,6 @@ def test_notification_send_disabled(app_context, disable_notifications, caplog): result = Notification.send(template_type, to_email, content) assert result is True - assert "Notification service is disabled" in caplog.text @mock_aws diff --git a/tests/test_security.py b/tests/test_security.py index b40cd920..19630ca6 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -1,18 +1,17 @@ """ Test magic links functionality """ + import base64 import pytest from jwt import decode -from jwt.exceptions import ExpiredSignatureError -from jwt.exceptions import InvalidSignatureError -from security.utils import create_token -from security.utils import validate_token +from jwt.exceptions import ExpiredSignatureError, InvalidSignatureError +from security.utils import create_token, validate_token -class TestSecurityUtils: +class TestSecurityUtils: tokens = {} def test_create_token_returns_token(self): diff --git a/tests/test_signout.py b/tests/test_signout.py index 97fdb131..d76226f1 100644 --- a/tests/test_signout.py +++ b/tests/test_signout.py @@ -1,14 +1,14 @@ """ Test session functionality """ -from unittest.mock import patch -from unittest.mock import PropertyMock + +from unittest.mock import PropertyMock, patch import pytest from bs4 import BeautifulSoup + from config.envs.default import SafeAppConfig -from security.utils import create_token -from security.utils import validate_token +from security.utils import create_token, validate_token @pytest.mark.usefixtures("flask_test_client") @@ -57,8 +57,7 @@ def test_signout_clears_cookie(self, flask_test_client, mock_redis_sessions): "Set-Cookie" ) assert ( - response.location - == "/service/magic-links/signed-out/sign_out_request?fund=test_fund&round=test_round" # noqa + response.location == "/service/magic-links/signed-out/sign_out_request?fund=test_fund&round=test_round" # noqa ) def test_magic_link_auth_can_be_signed_out(self, mocker, flask_test_client, mock_redis_sessions, create_magic_link): diff --git a/tests/test_signout_get.py b/tests/test_signout_get.py index 7a0d3fa8..6af2c88b 100644 --- a/tests/test_signout_get.py +++ b/tests/test_signout_get.py @@ -1,10 +1,11 @@ """ Test session functionality """ -from unittest.mock import patch -from unittest.mock import PropertyMock + +from unittest.mock import PropertyMock, patch import pytest + from config.envs.default import SafeAppConfig from security.utils import validate_token @@ -55,8 +56,7 @@ def test_signout_clears_cookie(self, flask_test_client, mock_redis_sessions): "Set-Cookie" ) assert ( - response.location - == "/service/magic-links/signed-out/sign_out_request?fund=test_fund&round=test_round" # noqa + response.location == "/service/magic-links/signed-out/sign_out_request?fund=test_fund&round=test_round" # noqa ) def test_magic_link_auth_can_be_signed_out(self, mocker, flask_test_client, mock_redis_sessions, create_magic_link): diff --git a/tests/test_sso.py b/tests/test_sso.py index 5fc812cd..e70f1fad 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -1,11 +1,14 @@ import pytest from flask import session from fsd_utils.authentication.utils import validate_token_rs256 -from testing.mocks.mocks.msal import ConfidentialClientApplication -from testing.mocks.mocks.msal import expected_fsd_user_token_claims -from testing.mocks.mocks.msal import HijackedConfidentialClientApplication -from testing.mocks.mocks.msal import id_token_claims -from testing.mocks.mocks.msal import RolelessConfidentialClientApplication + +from testing.mocks.mocks.msal import ( + ConfidentialClientApplication, + HijackedConfidentialClientApplication, + RolelessConfidentialClientApplication, + expected_fsd_user_token_claims, + id_token_claims, +) def test_sso_login_redirects_to_ms(flask_test_client): @@ -177,8 +180,7 @@ def test_sso_get_token_prevents_overwrite_of_existing_azure_subject_id(flask_tes assert ( "Cannot update account id: usersso - " "attempting to update existing azure_ad_subject_id " - "from abc to xyx which is not allowed." - in caplog.text + "from abc to xyx which is not allowed." in caplog.text ) @@ -191,7 +193,7 @@ def test_sso_get_token_500_when_error_in_auth_code_flow(flask_test_client, mocke response = flask_test_client.get(endpoint) assert response.status_code == 500 - assert "get-token flow failed with: {'error': 'some_error'}" in caplog.text + assert "get-token flow failed with: {'error': 'some_error'}" in caplog.records[1].error_message assert "some_error" not in response.text diff --git a/uv.lock b/uv.lock index a8878e8a..680c4f70 100644 --- a/uv.lock +++ b/uv.lock @@ -86,24 +86,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, ] -[[package]] -name = "black" -version = "22.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/59/e873cc6807fb62c11131e5258ca15577a3b7452abad08dc49286cf8245e8/black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", size = 553112 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/d9/60852a6fc2f85374db20a9767dacfe50c2172eb8388f46018c8daf836995/black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d", size = 1556665 }, - { url = "https://files.pythonhosted.org/packages/71/57/975782465cc6b514f2c972421e29b933dfbb51d4a95948a4e0e94f36ea38/black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351", size = 1205632 }, - { url = "https://files.pythonhosted.org/packages/0c/51/1f7f93c0555eaf4cbb628e26ba026e3256174a45bd9397ff1ea7cf96bad5/black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", size = 167343 }, -] - [[package]] name = "blinker" version = "1.6.2" @@ -427,32 +409,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/d5/17f02b379525d1ff9678bfa58eb9548f561c8826deb0b85797aa0eed582d/filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404", size = 10066 }, ] -[[package]] -name = "flake8" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/3c/3464b567aa367b221fa610bbbcce8015bf953977d21e52f2d711b526fb48/flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", size = 48219 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/01/cc8cdec7b61db0315c2ab62d80677a138ef06832ec17f04d87e6ef858f7f/flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3", size = 57570 }, -] - -[[package]] -name = "flake8-pyproject" -version = "1.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flake8" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/1d/635e86f9f3a96b7ea9e9f19b5efe17a987e765c39ca496e4a893bb999112/flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a", size = 4756 }, -] - [[package]] name = "flask" version = "2.2.5" @@ -615,14 +571,12 @@ dependencies = [ { name = "requests" }, ] -[package.dependency-groups] +[package.dev-dependencies] dev = [ { name = "beautifulsoup4" }, - { name = "black" }, { name = "debugpy" }, { name = "deepdiff" }, { name = "dparse" }, - { name = "flake8-pyproject" }, { name = "invoke" }, { name = "moto" }, { name = "pre-commit" }, @@ -631,6 +585,7 @@ dev = [ { name = "pytest-flask" }, { name = "pytest-mock" }, { name = "pytest-selenium" }, + { name = "ruff" }, { name = "selenium" }, { name = "swagger-ui-bundle" }, { name = "webdriver-manager" }, @@ -659,14 +614,12 @@ requires-dist = [ { name = "requests", specifier = "==2.32.3" }, ] -[package.metadata.dependency-groups] +[package.metadata.requires-dev] dev = [ { name = "beautifulsoup4", specifier = "==4.12.3" }, - { name = "black", specifier = "==22.12.0" }, { name = "debugpy", specifier = "==1.6.7" }, { name = "deepdiff", specifier = "==5.8.1" }, { name = "dparse", specifier = "==0.6.4" }, - { name = "flake8-pyproject", specifier = "==1.2.3" }, { name = "invoke", specifier = "==2.0.0" }, { name = "moto", specifier = "==5.0.12" }, { name = "pre-commit", specifier = "==4.0.1" }, @@ -675,6 +628,7 @@ dev = [ { name = "pytest-flask", specifier = "==1.3.0" }, { name = "pytest-mock", specifier = "==3.10.0" }, { name = "pytest-selenium", specifier = "==2.0.1" }, + { name = "ruff", specifier = ">=0.8.2" }, { name = "selenium", specifier = "==4.23.1" }, { name = "swagger-ui-bundle", specifier = "==0.0.9" }, { name = "webdriver-manager", specifier = "==4.0.1" }, @@ -882,15 +836,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/4b/15e5b9d40c4b58e97ebcb8ed5845a215fa5b7cf49a7f1cc7908f8db9cf46/MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", size = 17092 }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, -] - [[package]] name = "moto" version = "5.0.12" @@ -925,15 +870,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/41/646c00154efa437bf01b30444421285fb29ef624e86b2446e71eff50b7a9/msal-1.28.0-py3-none-any.whl", hash = "sha256:3064f80221a21cd535ad8c3fafbb3a3582cd9c7e9af0bb789ae14f726a0ca99b", size = 102150 }, ] -[[package]] -name = "mypy-extensions" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/60/0582ce2eaced55f65a4406fc97beba256de4b7a95a0034c6576458c6519f/mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8", size = 4252 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/eb/975c7c080f3223a5cdaff09612f3a5221e4ba534f7039db34c35d95fa6a5/mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", size = 4470 }, -] - [[package]] name = "nodeenv" version = "1.7.0" @@ -1006,15 +942,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/8e/8de486cbd03baba4deef4142bd643a3e7bbe954a784dc1bb17142572d127/packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522", size = 40750 }, ] -[[package]] -name = "pathspec" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/33/436c5cb94e9f8902e59d1d544eb298b83c84b9ec37b5b769c5a0ad6edb19/pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1", size = 29483 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/ba/a9d64c7bcbc7e3e8e5f93a52721b377e994c22d16196e2b0f1236774353a/pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", size = 31165 }, -] - [[package]] name = "platformdirs" version = "2.5.2" @@ -1074,15 +1001,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, ] -[[package]] -name = "pycodestyle" -version = "2.11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/8f/fa09ae2acc737b9507b5734a9aec9a2b35fa73409982f57db1b42f8c3c65/pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", size = 38974 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/90/a998c550d0ddd07e38605bb5c455d00fcc177a800ff9cc3dafdcb3dd7b56/pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67", size = 31132 }, -] - [[package]] name = "pycparser" version = "2.21" @@ -1092,15 +1010,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", size = 118697 }, ] -[[package]] -name = "pyflakes" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 }, -] - [[package]] name = "pygments" version = "2.15.0" @@ -1413,6 +1322,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/d9/312648cfc9c212988a3564b041bd6a8ca0e266ff42fd7b74bbb3113b300f/ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a", size = 111685 }, ] +[[package]] +name = "ruff" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/2b/01245f4f3a727d60bebeacd7ee6d22586c7f62380a2597ddb22c2f45d018/ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5", size = 3349020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/29/366be70216dba1731a00a41f2f030822b0c96c7c4f3b2c0cdce15cbace74/ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d", size = 10530649 }, + { url = "https://files.pythonhosted.org/packages/63/82/a733956540bb388f00df5a3e6a02467b16c0e529132625fe44ce4c5fb9c7/ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5", size = 10274069 }, + { url = "https://files.pythonhosted.org/packages/3d/12/0b3aa14d1d71546c988a28e1b412981c1b80c8a1072e977a2f30c595cc4a/ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c", size = 9909400 }, + { url = "https://files.pythonhosted.org/packages/23/08/f9f08cefb7921784c891c4151cce6ed357ff49e84b84978440cffbc87408/ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f", size = 10766782 }, + { url = "https://files.pythonhosted.org/packages/e4/71/bf50c321ec179aa420c8ec40adac5ae9cc408d4d37283a485b19a2331ceb/ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897", size = 10286316 }, + { url = "https://files.pythonhosted.org/packages/f2/83/c82688a2a6117539aea0ce63fdf6c08e60fe0202779361223bcd7f40bd74/ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58", size = 11338270 }, + { url = "https://files.pythonhosted.org/packages/7f/d7/bc6a45e5a22e627640388e703160afb1d77c572b1d0fda8b4349f334fc66/ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29", size = 12058579 }, + { url = "https://files.pythonhosted.org/packages/da/3b/64150c93946ec851e6f1707ff586bb460ca671581380c919698d6a9267dc/ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248", size = 11615172 }, + { url = "https://files.pythonhosted.org/packages/e4/9e/cf12b697ea83cfe92ec4509ae414dc4c9b38179cc681a497031f0d0d9a8e/ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93", size = 12882398 }, + { url = "https://files.pythonhosted.org/packages/a9/27/96d10863accf76a9c97baceac30b0a52d917eb985a8ac058bd4636aeede0/ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d", size = 11176094 }, + { url = "https://files.pythonhosted.org/packages/eb/10/cd2fd77d4a4e7f03c29351be0f53278a393186b540b99df68beb5304fddd/ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0", size = 10771884 }, + { url = "https://files.pythonhosted.org/packages/71/5d/beabb2ff18870fc4add05fa3a69a4cb1b1d2d6f83f3cf3ae5ab0d52f455d/ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa", size = 10382535 }, + { url = "https://files.pythonhosted.org/packages/ae/29/6b3fdf3ad3e35b28d87c25a9ff4c8222ad72485ab783936b2b267250d7a7/ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f", size = 10886995 }, + { url = "https://files.pythonhosted.org/packages/e9/dc/859d889b4d9356a1a2cdbc1e4a0dda94052bc5b5300098647e51a58c430b/ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22", size = 11220750 }, + { url = "https://files.pythonhosted.org/packages/0b/08/e8f519f61f1d624264bfd6b8829e4c5f31c3c61193bc3cff1f19dbe7626a/ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1", size = 8729396 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/ba1c7ab72aba37a2b71fe48ab95b80546dbad7a7f35ea28cf66fc5cea5f6/ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea", size = 9594729 }, + { url = "https://files.pythonhosted.org/packages/23/34/db20e12d3db11b8a2a8874258f0f6d96a9a4d631659d54575840557164c8/ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8", size = 9035131 }, +] + [[package]] name = "s3transfer" version = "0.6.1"