From 44996f0daeae6f9743aa9b4e14f785a46b6ceeaf Mon Sep 17 00:00:00 2001 From: Stefan - ZipKid - Goethals Date: Thu, 10 Sep 2026 13:55:04 +0200 Subject: [PATCH 1/5] Ruff fixes --- src/authenticate.py | 31 +++++++++++-------- src/authorize.py | 28 ++++++++++++------ src/batch_authorize.py | 14 +++++++-- src/delegate.py | 28 ++++++++++++------ src/generate_ci.py | 4 +-- src/index.py | 13 ++++++-- src/logout.py | 9 +++++- src/use_grant.py | 8 ++++- src/utils.py | 16 +++++----- templates/authorizer-dummy.py | 3 +- templates/authorizer.py | 56 +++++++++++++++++++++++++++-------- templates/example.py | 24 +++++++++++---- templates/params.py | 20 +++++++++---- templates/validator.py | 6 ++-- test/authorize_test.py | 1 + test/batch_authorize_test.py | 1 + test/cognito_utils_test.py | 1 - test/utils.py | 3 +- 18 files changed, 185 insertions(+), 81 deletions(-) diff --git a/src/authenticate.py b/src/authenticate.py index a0a5ca4..aefd97f 100644 --- a/src/authenticate.py +++ b/src/authenticate.py @@ -6,13 +6,19 @@ import jwt import requests import requests.auth - -from cognito_utils import validate_cognito_id_token -from utils import bad_request, internal_server_error, get_refresh_token_jwt_secret, get_state_jwt_secret, \ - generate_cookie, get_config from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext +from cognito_utils import validate_cognito_id_token +from utils import ( + bad_request, + generate_cookie, + get_config, + get_refresh_token_jwt_secret, + get_state_jwt_secret, + internal_server_error, +) + logger = Logger() class InternalServerError(Exception): pass @@ -48,8 +54,8 @@ def exchange_cognito_code(event: dict, cognito_code: str) -> dict: data=post_data, auth=requests.auth.HTTPBasicAuth(client_id, client_secret) ) - except requests.exceptions.ConnectionError as e: - logger.exception({"message": "Connection error to Cognito", "exception": e}) + except requests.exceptions.ConnectionError: + logger.exception({"message": "Connection error to Cognito"}) raise InternalServerError() if token_response.status_code != 200: @@ -67,7 +73,6 @@ def exchange_cognito_code(event: dict, cognito_code: str) -> dict: logger.exception({ "message": "Uncaught error", "cognito_reply": token_response.text, - "exception": e, "backtrace": traceback.format_exc() }) raise InternalServerError() from e @@ -82,11 +87,11 @@ def exchange_cognito_code(event: dict, cognito_code: str) -> dict: user_pool_id=os.environ['COGNITO_USER_POOL_ID'], client_id=client_id, ) - except requests.exceptions.RequestException as e: - logger.exception({"message": "Connection error to Cognito", "exception": e}) + except requests.exceptions.RequestException: + logger.exception({"message": "Connection error to Cognito"}) raise InternalServerError() - except jwt.InvalidTokenError as e: - logger.exception({"message": "id_token invalid", "exception": e}) + except jwt.InvalidTokenError: + logger.exception({"message": "id_token invalid"}) raise InternalServerError() logger.info("Cognito ID token is valid") @@ -154,8 +159,8 @@ def handler(event, context: LambdaContext) -> dict: f"redirect_uri={urllib.parse.quote_plus(state['redirect_uri'])}" else: raise ValueError(f"Invalid action `{state['action']}`") - except (KeyError, ValueError) as e: - logger.exception({"message": "state is invalid", "exception": e}) + except (KeyError, ValueError): + logger.exception({"message": "state is invalid"}) return internal_server_error() return { diff --git a/src/authorize.py b/src/authorize.py index a759abd..36b9cdd 100644 --- a/src/authorize.py +++ b/src/authorize.py @@ -1,15 +1,25 @@ -import time -from urllib.parse import urlsplit, urlunsplit, urlencode +from urllib.parse import urlencode, urlsplit, urlunsplit import jwt from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext +from utils import ( + BadRequest, + InternalServerError, + NotLoggedIn, + access_token_from_refresh_token, + bad_request, + get_config, + get_refresh_token, + get_state_jwt_secret, + internal_server_error, + is_allowed_domain, + redirect_to_cognito, +) + logger = Logger() -from utils import get_config, bad_request, get_access_token_jwt_secret, redirect_to_cognito, NotLoggedIn, BadRequest, \ - InternalServerError, internal_server_error, get_refresh_token, get_state_jwt_secret, is_allowed_domain, \ - access_token_from_refresh_token @logger.inject_lambda_context def handler(event, context: LambdaContext) -> dict: @@ -45,10 +55,10 @@ def handler(event, context: LambdaContext) -> dict: logger.error(f"{redirect_uri} is not an allowed domain") return bad_request('', f"{redirect_uri} is not an allowed domain") - if 'domains' in refresh_token: # delegated token with domain restrictions - if redirect_uri_comp.netloc not in refresh_token['domains']: - logger.error(f"{redirect_uri} is not an allowed domain for this refresh token") - return bad_request('', f"{redirect_uri} is not an allowed domain for this refresh token") + # delegated token with domain restrictions + if 'domains' in refresh_token and redirect_uri_comp.netloc not in refresh_token['domains']: + logger.error(f"{redirect_uri} is not an allowed domain for this refresh token") + return bad_request('', f"{redirect_uri} is not an allowed domain for this refresh token") try: access_token = access_token_from_refresh_token( diff --git a/src/batch_authorize.py b/src/batch_authorize.py index aaf980c..2c615ff 100644 --- a/src/batch_authorize.py +++ b/src/batch_authorize.py @@ -1,11 +1,19 @@ import json -from utils import bad_request, NotLoggedIn, BadRequest, \ - InternalServerError, internal_server_error, get_refresh_token, get_domains, \ - access_token_from_refresh_token from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext +from utils import ( + BadRequest, + InternalServerError, + NotLoggedIn, + access_token_from_refresh_token, + bad_request, + get_domains, + get_refresh_token, + internal_server_error, +) + logger = Logger() @logger.inject_lambda_context diff --git a/src/delegate.py b/src/delegate.py index 2393f71..fafcc9b 100644 --- a/src/delegate.py +++ b/src/delegate.py @@ -5,13 +5,25 @@ import urllib.parse import jwt - -from utils import redirect_to_cognito, get_refresh_token, NotLoggedIn, BadRequest, \ - bad_request, InternalServerError, internal_server_error, \ - get_grant_jwt_secret, get_state_jwt_secret, get_config, is_allowed_domain, dynamodb_client, get_domains from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext +from utils import ( + BadRequest, + InternalServerError, + NotLoggedIn, + bad_request, + dynamodb_client, + get_config, + get_domains, + get_grant_jwt_secret, + get_refresh_token, + get_state_jwt_secret, + internal_server_error, + is_allowed_domain, + redirect_to_cognito, +) + logger = Logger() @logger.inject_lambda_context @@ -55,9 +67,8 @@ def handler(event, context: LambdaContext) -> dict: for group_entry in page['Items']: try: groups[group_entry['group']['S']] = group_entry['domains']['SS'] - except KeyError as e: + except KeyError: logger.exception("Invalid group in DynamoDB: " + repr(group_entry)) - pass with open(os.path.join(os.path.dirname(__file__), 'delegate.html')) as f: html = f.read() @@ -100,9 +111,8 @@ def handler(event, context: LambdaContext) -> dict: if not is_allowed_domain(domain): return bad_request('', 'Unknown domain in request') - if 'domains' in refresh_token: - if not domains.issubset(refresh_token['domains']): - return bad_request('', 'domain requested outside refresh_token') + if 'domains' in refresh_token and not domains.issubset(refresh_token['domains']): + return bad_request('', 'domain requested outside refresh_token') # Validate no commas in new subject to avoid future join ambiguity if ',' in subject: diff --git a/src/generate_ci.py b/src/generate_ci.py index 94bef20..7ff2468 100644 --- a/src/generate_ci.py +++ b/src/generate_ci.py @@ -3,11 +3,11 @@ import time import jwt - -from utils import get_access_token_jwt_secret, bad_request, is_allowed_domain from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext +from utils import bad_request, get_access_token_jwt_secret, is_allowed_domain + logger = Logger() @logger.inject_lambda_context diff --git a/src/index.py b/src/index.py index 4c39028..5c657a0 100644 --- a/src/index.py +++ b/src/index.py @@ -4,8 +4,17 @@ import jwt -from utils import NotLoggedIn, BadRequest, InternalServerError, internal_server_error, cognito_url, \ - get_state_jwt_secret, get_csrf_jwt_secret, get_raw_refresh_token, parse_raw_refresh_token +from utils import ( + BadRequest, + InternalServerError, + NotLoggedIn, + cognito_url, + get_csrf_jwt_secret, + get_raw_refresh_token, + get_state_jwt_secret, + internal_server_error, + parse_raw_refresh_token, +) def handler(event, context) -> dict: diff --git a/src/logout.py b/src/logout.py index 0d779e4..e2199e5 100644 --- a/src/logout.py +++ b/src/logout.py @@ -6,7 +6,14 @@ from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext -from utils import generate_cookie, get_config, bad_request, get_csrf_jwt_secret, get_raw_refresh_token, NotLoggedIn +from utils import ( + NotLoggedIn, + bad_request, + generate_cookie, + get_config, + get_csrf_jwt_secret, + get_raw_refresh_token, +) logger = Logger() diff --git a/src/use_grant.py b/src/use_grant.py index dfcd3e5..1d33f67 100644 --- a/src/use_grant.py +++ b/src/use_grant.py @@ -4,7 +4,13 @@ import jwt -from utils import bad_request, get_grant_jwt_secret, generate_cookie, get_config, get_refresh_token_jwt_secret +from utils import ( + bad_request, + generate_cookie, + get_config, + get_grant_jwt_secret, + get_refresh_token_jwt_secret, +) def handler(event, context) -> dict: diff --git a/src/utils.py b/src/utils.py index be6436e..41af05a 100644 --- a/src/utils.py +++ b/src/utils.py @@ -3,7 +3,6 @@ import sys import time import traceback -import typing from http import cookies from urllib.parse import urlencode @@ -30,7 +29,7 @@ def __init__(self): self.group_table = "groups" def update(self, settings_dict: dict): - for attr in vars(self).keys(): + for attr in vars(self): if attr in settings_dict: setattr(self, attr, settings_dict[attr]) @@ -48,7 +47,7 @@ def get_config() -> Config: body = response['Body'].read() config = json.loads(body) c.update(config) - except Exception as e: + except Exception as e: # noqa: BLE001 print(f"s3.GetObject(Bucket={bucket}, Key={CONFIG_KEY}) failed, continuing with defaults:") traceback.print_exception(type(e), e, e.__traceback__, file=sys.stdout) return c @@ -101,8 +100,8 @@ def get_csrf_jwt_secret() -> str: def canonicalize_headers( - headers: typing.Union[typing.Dict[str, str], typing.List[typing.Tuple[str, str]]] -) -> typing.Dict[str, typing.List[str]]: + headers: dict[str, str] | list[tuple[str, str]] +) -> dict[str, list[str]]: """ HTTP headers are case-insensitive. Join equivalent headers together. """ @@ -112,7 +111,7 @@ def canonicalize_headers( for k, v in headers.items() ] - canonical_headers = dict() + canonical_headers = {} for name, value in headers: name = name.lower() if name not in canonical_headers: @@ -122,7 +121,7 @@ def canonicalize_headers( return canonical_headers -def generate_cookie(key: str, value: str, max_age: int = None, path: str = None) -> str: +def generate_cookie(key: str, value: str, max_age: int | None = None, path: str | None = None) -> str: """ Generate the string usable in a Set-Cookie:-header. """ @@ -258,7 +257,7 @@ def get_refresh_token(event) -> dict: return parse_raw_refresh_token(raw_refresh_token) # may raise -def get_domains() -> typing.List[str]: +def get_domains() -> list[str]: domains = [] scan_paginator = dynamodb_client.get_paginator('scan') response_iterator = scan_paginator.paginate( @@ -270,7 +269,6 @@ def get_domains() -> typing.List[str]: domains.append(domain_entry['domain']['S']) except KeyError: logger.exception("Invalid domain in DynamoDB: " + repr(domain_entry)) - pass return domains diff --git a/templates/authorizer-dummy.py b/templates/authorizer-dummy.py index 7dd5e1a..9a67da2 100644 --- a/templates/authorizer-dummy.py +++ b/templates/authorizer-dummy.py @@ -1,9 +1,8 @@ """Authorizer Dummy stack.""" -from troposphere import Template, Sub, GetAtt - import cfnutils.output import custom_resources.cloudformation import custom_resources.ssm +from troposphere import GetAtt, Sub, Template template = Template(Description="Authorizer dummy stack for prod") diff --git a/templates/authorizer.py b/templates/authorizer.py index cb0e98b..2ab723f 100644 --- a/templates/authorizer.py +++ b/templates/authorizer.py @@ -1,17 +1,49 @@ """Authorizer stack.""" -from troposphere import Template, Parameter, Ref, Sub, GetAtt, Output, Export, Join, AWS_STACK_NAME, apigateway, \ - Equals, route53, FindInMap, AWS_REGION, serverless, constants, awslambda, kms, iam, s3, dynamodb, \ - ImportValue, Not, And, Condition, If, AWS_NO_VALUE -from troposphere.cloudfront import Origin, CustomOriginConfig, Distribution, \ - DistributionConfig, ViewerCertificate, DefaultCacheBehavior -import custom_resources.ssm +import cfnutils.kms +import cfnutils.mappings +import cfnutils.output import custom_resources.acm -import custom_resources.cognito import custom_resources.cloudformation +import custom_resources.cognito import custom_resources.s3 -import cfnutils.mappings -import cfnutils.kms -import cfnutils.output +import custom_resources.ssm +from troposphere import ( + AWS_NO_VALUE, + AWS_REGION, + AWS_STACK_NAME, + And, + Condition, + Equals, + Export, + FindInMap, + GetAtt, + If, + ImportValue, + Join, + Not, + Output, + Parameter, + Ref, + Sub, + Template, + apigateway, + awslambda, + constants, + dynamodb, + iam, + kms, + route53, + s3, + serverless, +) +from troposphere.cloudfront import ( + CustomOriginConfig, + DefaultCacheBehavior, + Distribution, + DistributionConfig, + Origin, + ViewerCertificate, +) template = Template() @@ -439,9 +471,7 @@ "Effect": "Allow", "Resource": [ Sub( - "arn:aws:ssm:${{AWS::Region}}:${{AWS::AccountId}}:parameter${{{param}}}".format( - param=p.title, - )) + f"arn:aws:ssm:${{AWS::Region}}:${{AWS::AccountId}}:parameter${{{p.title}}}") for p in [jwt_secret_parameter] ], }], diff --git a/templates/example.py b/templates/example.py index b9fd33b..873197e 100644 --- a/templates/example.py +++ b/templates/example.py @@ -1,12 +1,26 @@ -from troposphere import Template, cloudfront, constants, Sub, Join, Parameter, Ref, Output, GetAtt, \ - Equals, route53, FindInMap, AWS_REGION, ImportValue, s3 +import cfnutils.mappings +import cfnutils.output import custom_resources.acm import custom_resources.cloudformation import custom_resources.dynamodb import custom_resources.s3 -import cfnutils.mappings -import cfnutils.output - +from troposphere import ( + AWS_REGION, + Equals, + FindInMap, + GetAtt, + ImportValue, + Join, + Output, + Parameter, + Ref, + Sub, + Template, + cloudfront, + constants, + route53, + s3, +) template = Template() diff --git a/templates/params.py b/templates/params.py index bb73862..5754613 100644 --- a/templates/params.py +++ b/templates/params.py @@ -3,12 +3,22 @@ This stack gathers the information needed to use the Authorizer in one place. """ -from troposphere import Template, Parameter, Ref, Sub, Output, Export, Join, AWS_STACK_NAME, constants, \ - GetAtt, ImportValue -import custom_resources.ssm -import custom_resources.cloudformation import cfnutils.output - +import custom_resources.cloudformation +import custom_resources.ssm +from troposphere import ( + AWS_STACK_NAME, + Export, + GetAtt, + ImportValue, + Join, + Output, + Parameter, + Ref, + Sub, + Template, + constants, +) template = Template() diff --git a/templates/validator.py b/templates/validator.py index 0e0dd5d..7fcb475 100644 --- a/templates/validator.py +++ b/templates/validator.py @@ -1,11 +1,9 @@ """ Validator stack. """ -from troposphere import Template, constants, Parameter, awslambda, Ref, Output - -import custom_resources.awslambda import cfnutils.output - +import custom_resources.awslambda +from troposphere import Output, Parameter, Ref, Template, awslambda, constants template = Template() diff --git a/test/authorize_test.py b/test/authorize_test.py index 23e1803..b399339 100644 --- a/test/authorize_test.py +++ b/test/authorize_test.py @@ -2,6 +2,7 @@ import authorize import utils + from .utils import gen_refresh_token diff --git a/test/batch_authorize_test.py b/test/batch_authorize_test.py index cdb72b3..f47f82f 100644 --- a/test/batch_authorize_test.py +++ b/test/batch_authorize_test.py @@ -2,6 +2,7 @@ from unittest import mock import batch_authorize + from .utils import gen_refresh_token diff --git a/test/cognito_utils_test.py b/test/cognito_utils_test.py index 67227ed..40d6924 100644 --- a/test/cognito_utils_test.py +++ b/test/cognito_utils_test.py @@ -5,7 +5,6 @@ from src import cognito_utils - id_token = "eyJraWQiOiJTVUxMd0xFeWthcVpCbHpYQityR0pZY0h6Q1Y2SHZ2ZXhSZk5oZVptZW1BPSIsImFsZyI6IlJTMjU2In0.eyJhd" \ "F9oYXNoIjoiTWU4NjYzazVNRGhDcGxhRDF4R1hEUSIsInN1YiI6Ijg0NGE0MDEwLTBlYjEtNGY3Yy1hOGM5LTMyYjFmNzZlND" \ "hhYiIsImF1ZCI6IjIzZW1xbjBibTU4bmVqdXZsOWp1NXVnNTBtIiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE1MzA" \ diff --git a/test/utils.py b/test/utils.py index 6212475..120aea0 100644 --- a/test/utils.py +++ b/test/utils.py @@ -1,8 +1,7 @@ import time -import typing -def gen_refresh_token(domain: typing.Optional[str], exp_in: int = 5): +def gen_refresh_token(domain: str | None, exp_in: int = 5): now = int(time.time()) token = { 'iat': now, From 4a83c9360530a6dd9feb62cd4dde28c9344ce30c Mon Sep 17 00:00:00 2001 From: Stefan - ZipKid - Goethals Date: Thu, 10 Sep 2026 14:10:01 +0200 Subject: [PATCH 2/5] More Ruff Fixes --- src/authenticate.py | 6 ++-- src/authorize.py | 2 +- src/use_grant.py | 2 +- src/utils.py | 4 +-- templates/example.py | 6 ++-- test/authorize_test.py | 16 +++++------ test/batch_authorize_test.py | 2 +- test/cognito_utils_test.py | 23 ++++++++-------- test/delegate_test.py | 16 +++++------ test/generate_ci_test.py | 8 +++--- test/utils.py | 2 +- test/utils_test.py | 53 +++++++++++++++++------------------- 12 files changed, 68 insertions(+), 72 deletions(-) diff --git a/src/authenticate.py b/src/authenticate.py index aefd97f..e4bd6c9 100644 --- a/src/authenticate.py +++ b/src/authenticate.py @@ -52,7 +52,7 @@ def exchange_cognito_code(event: dict, cognito_code: str) -> dict: token_response = requests.post( endpointurl, data=post_data, - auth=requests.auth.HTTPBasicAuth(client_id, client_secret) + auth=requests.auth.HTTPBasicAuth(client_id, client_secret), ) except requests.exceptions.ConnectionError: logger.exception({"message": "Connection error to Cognito"}) @@ -73,7 +73,7 @@ def exchange_cognito_code(event: dict, cognito_code: str) -> dict: logger.exception({ "message": "Uncaught error", "cognito_reply": token_response.text, - "backtrace": traceback.format_exc() + "backtrace": traceback.format_exc(), }) raise InternalServerError() from e @@ -171,7 +171,7 @@ def handler(event, context: LambdaContext) -> dict: 'Set-Cookie': generate_cookie( get_config().cookie_name_refresh_token, raw_refresh_token, - max_age=int(cognito_token['exp'] - now) + max_age=int(cognito_token['exp'] - now), ), }, 'body': 'Redirecting...', diff --git a/src/authorize.py b/src/authorize.py index 36b9cdd..7a5d0ff 100644 --- a/src/authorize.py +++ b/src/authorize.py @@ -63,7 +63,7 @@ def handler(event, context: LambdaContext) -> dict: try: access_token = access_token_from_refresh_token( refresh_token, - redirect_uri_comp.netloc + redirect_uri_comp.netloc, ) except BadRequest as e: return bad_request('', e) diff --git a/src/use_grant.py b/src/use_grant.py index 1d33f67..68b6be1 100644 --- a/src/use_grant.py +++ b/src/use_grant.py @@ -39,7 +39,7 @@ def handler(event, context) -> dict: raw_refresh_token = jwt.encode( refresh_token, get_refresh_token_jwt_secret(), - algorithm='HS256' + algorithm='HS256', ) with open(os.path.join(os.path.dirname(__file__), 'use_grant.html')) as f: diff --git a/src/utils.py b/src/utils.py index 41af05a..ceb634f 100644 --- a/src/utils.py +++ b/src/utils.py @@ -100,7 +100,7 @@ def get_csrf_jwt_secret() -> str: def canonicalize_headers( - headers: dict[str, str] | list[tuple[str, str]] + headers: dict[str, str] | list[tuple[str, str]], ) -> dict[str, list[str]]: """ HTTP headers are case-insensitive. Join equivalent headers together. @@ -199,7 +199,7 @@ def redirect_to_cognito(state: str = '') -> dict: 'statusCode': 302, 'headers': { 'Location': location, - 'Content-Type': 'text/html' + 'Content-Type': 'text/html', }, 'body': f"""\ diff --git a/templates/example.py b/templates/example.py index 873197e..7509600 100644 --- a/templates/example.py +++ b/templates/example.py @@ -63,7 +63,7 @@ AllowedValues=['yes', 'no'], Default='no', # Default to no, so new stacks request, but don't use certs # This avoids stacks failing since the cert is not approved yet - Description="Use TLS certificate" + Description="Use TLS certificate", )) template.set_parameter_label(param_use_cert, "Use TLS certificate") @@ -162,7 +162,7 @@ S3OriginConfig=cloudfront.S3OriginConfig( OriginAccessIdentity=Join('', [ 'origin-access-identity/cloudfront/', Ref(example_bucket_oai), - ]) + ]), ), ), ], @@ -178,7 +178,7 @@ LambdaFunctionAssociations=[ cloudfront.LambdaFunctionAssociation( EventType='viewer-request', - LambdaFunctionARN=Ref(param_authorizer_lae_arn) + LambdaFunctionARN=Ref(param_authorizer_lae_arn), ), ], # Rest of config as per your needs diff --git a/test/authorize_test.py b/test/authorize_test.py index b399339..efc8efc 100644 --- a/test/authorize_test.py +++ b/test/authorize_test.py @@ -8,7 +8,7 @@ def test_no_redirect_uri(): resp = authorize.handler({}, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_not_logged_in(): @@ -21,7 +21,7 @@ def test_not_logged_in(): 'redirect_uri': 'https://example.org/', }, }, None) - assert 302 == resp['statusCode'] + assert resp['statusCode'] == 302 assert cognito_url == resp['headers']['Location'] @@ -32,7 +32,7 @@ def test_bad_request(): 'redirect_uri': 'https://example.org/', }, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_normal(): @@ -45,7 +45,7 @@ def test_normal(): 'redirect_uri': 'https://example.org/', }, }, None) - assert 302 == resp['statusCode'] + assert resp['statusCode'] == 302 assert resp['headers']['Location'].startswith('https://example.org/') @@ -59,7 +59,7 @@ def test_wrong_domain(): 'redirect_uri': 'https://example.org/', }, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_no_exp(): @@ -73,7 +73,7 @@ def test_no_exp(): 'redirect_uri': 'https://example.org/', }, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_no_azp(): @@ -87,7 +87,7 @@ def test_no_azp(): 'redirect_uri': 'https://example.org/', }, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_unlisted_domain(): @@ -100,4 +100,4 @@ def test_unlisted_domain(): 'redirect_uri': 'https://example.com/', }, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 diff --git a/test/batch_authorize_test.py b/test/batch_authorize_test.py index f47f82f..f6a075b 100644 --- a/test/batch_authorize_test.py +++ b/test/batch_authorize_test.py @@ -11,7 +11,7 @@ def test_normal(): with mock.patch('batch_authorize.get_refresh_token', return_value=refresh_token), \ mock.patch('utils.get_jwt_secret', return_value='secret'): resp = batch_authorize.handler({}, None) - assert 200 == resp['statusCode'] + assert resp['statusCode'] == 200 body = resp['body'] tokens = json.loads(body) assert isinstance(tokens, dict) diff --git a/test/cognito_utils_test.py b/test/cognito_utils_test.py index 40d6924..32cf81b 100644 --- a/test/cognito_utils_test.py +++ b/test/cognito_utils_test.py @@ -25,7 +25,7 @@ "iqwtmu3slLOEENce0vNI1SU2WzqxQ9sUKLv0mKWesvF9ukJ8hEN9GYJ2ng6wUtnRlKh8qlIkiBlKogNQiQk21bvk6B" "VX0TWQ_RRlth22zMxdv0VUDNZd8xopy9DSJ9-9jpFidbSY1y24vbeDYewztshsHomAaW2cAzpxmJ12oSs9OgvLROFP" "tbANG7-0netCHeTPaAtXLo_0s-c35gHUziCcxYEM4PR7GZOvX1IUfIvxblG1BNHJAi79cDbw", - "use": "sig" + "use": "sig", }, { "alg": "RS256", @@ -36,9 +36,9 @@ "Eg5mRA4Gu5CHcM88gNB6eFfPUpXT_XxSmRd5AXT9yfTE6lhFNsfxX5v_yl_qDRHEnST0dJm9xL9hGAbe5ZeKHf3HDY" "D-k1lR5TqceEutzJdpJg-grm6VhXvFF52U9ZmfBkA3yi8D_895WSKbHTfGCCKfP4mdF286jrFifGkxu2EK-lCM0dwv" "4l_JZxFB3ds1hkTs5uog1PHzeoBBwSs1aaC6QT_M_whfVBur1TGKRXq0OHaQkPhYo5KR6SXQ", - "use": "sig" - } - ] + "use": "sig", + }, + ], } @@ -63,11 +63,10 @@ def jwt_decode(*args, **kwargs): def test_jwt_parsing_expired(): - with mock.patch('src.cognito_utils.get_jwt_keys', return_value=jwk): - with pytest.raises(jwt.exceptions.ExpiredSignatureError): - cognito_utils.validate_cognito_id_token( - token=id_token, - region='unused because of get_jwt_keys() mock', - user_pool_id='unused because of get_jwt_keys() mock', - client_id='unused because of get_jwt_keys() mock', - ) + with mock.patch('src.cognito_utils.get_jwt_keys', return_value=jwk), pytest.raises(jwt.exceptions.ExpiredSignatureError): + cognito_utils.validate_cognito_id_token( + token=id_token, + region='unused because of get_jwt_keys() mock', + user_pool_id='unused because of get_jwt_keys() mock', + client_id='unused because of get_jwt_keys() mock', + ) diff --git a/test/delegate_test.py b/test/delegate_test.py index d17f7bc..85c2432 100644 --- a/test/delegate_test.py +++ b/test/delegate_test.py @@ -13,14 +13,14 @@ def test_no_token(): mock.patch('utils.get_jwt_secret', return_value='secret'), \ mock.patch('utils.cognito_url', return_value=cognito_url): resp = delegate.handler({}, None) - assert 302 == resp['statusCode'] + assert resp['statusCode'] == 302 assert cognito_url == resp['headers']['Location'] def test_bad_token(): with mock.patch('delegate.get_refresh_token', side_effect=utils.BadRequest): resp = delegate.handler({}, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def gen_refresh_token(domain: str, exp_in: int = 5): @@ -44,7 +44,7 @@ def test_post(): 'httpMethod': 'POST', 'body': body, }, None) - assert 200 == resp['statusCode'] + assert resp['statusCode'] == 200 def test_post_too_long(): @@ -58,7 +58,7 @@ def test_post_too_long(): 'httpMethod': 'POST', 'body': body, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_post_domain_outside_list(): @@ -72,7 +72,7 @@ def test_post_domain_outside_list(): 'httpMethod': 'POST', 'body': body, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_domain_outside_token(): @@ -87,7 +87,7 @@ def test_domain_outside_token(): 'httpMethod': 'POST', 'body': body, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_post_no_subject(): @@ -101,7 +101,7 @@ def test_post_no_subject(): 'httpMethod': 'POST', 'body': body, }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_sub_delegate(): @@ -116,6 +116,6 @@ def test_sub_delegate(): 'httpMethod': 'POST', 'body': body, }, None) - assert 200 == resp['statusCode'] + assert resp['statusCode'] == 200 delegate_token = jwt.decode(resp['body'], 'secret', algorithms=["HS256"], options={"verify_signature": False}) assert 'test1' in delegate_token['sub'] diff --git a/test/generate_ci_test.py b/test/generate_ci_test.py index ed5e852..4a9a095 100644 --- a/test/generate_ci_test.py +++ b/test/generate_ci_test.py @@ -15,7 +15,7 @@ def test_post(): 'requestContext': { 'identity': { 'caller': "test" }}, 'body': json.dumps(body), }, None) - assert 200 == resp['statusCode'] + assert resp['statusCode'] == 200 ci_token = jwt.decode(resp['body'], 'secret', algorithms=["HS256"], options={"verify_signature": False}) assert 'example.org' in ci_token['domains'] assert 'another-example.org' in ci_token['domains'] @@ -30,7 +30,7 @@ def test_post_too_long(): 'requestContext': { 'identity': { 'caller': "test" }}, 'body': json.dumps(body), }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_post_domain_outside_list(): @@ -42,7 +42,7 @@ def test_post_domain_outside_list(): 'requestContext': { 'identity': { 'caller': "test" }}, 'body': json.dumps(body), }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 def test_post_no_subject(): @@ -54,4 +54,4 @@ def test_post_no_subject(): 'requestContext': { 'identity': { 'caller': "test" }}, 'body': json.dumps(body), }, None) - assert 400 == resp['statusCode'] + assert resp['statusCode'] == 400 diff --git a/test/utils.py b/test/utils.py index 120aea0..64cd752 100644 --- a/test/utils.py +++ b/test/utils.py @@ -10,4 +10,4 @@ def gen_refresh_token(domain: str | None, exp_in: int = 5): } if domain is not None: token['domains'] = [domain] - return token \ No newline at end of file + return token diff --git a/test/utils_test.py b/test/utils_test.py index aa45159..2540678 100644 --- a/test/utils_test.py +++ b/test/utils_test.py @@ -14,21 +14,21 @@ def test_canon_header(): 'Cookie': 'foo', 'cookie': 'bar', }) == { - 'cookie': ['foo', 'bar'] + 'cookie': ['foo', 'bar'], } assert utils.canonicalize_headers([ ('Cookie', 'foo'), ('cookie', 'bar'), ]) == { - 'cookie': ['foo', 'bar'] + 'cookie': ['foo', 'bar'], } def test_refresh_token_no_cookie(): with pytest.raises(utils.NotLoggedIn): token = utils.get_refresh_token({ - 'headers': {} + 'headers': {}, }) @@ -37,18 +37,17 @@ def test_refresh_token_other_cookie(): token = utils.get_refresh_token({ 'headers': { 'Cookie': 'foo=bar', - } + }, }) def test_refresh_token_invalid_token(): - with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"): - with pytest.raises(utils.BadRequest): - token = utils.get_refresh_token({ - 'headers': { - 'Cookie': f"{utils.get_config().cookie_name_refresh_token}=foobar", - } - }) + with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"), pytest.raises(utils.BadRequest): + token = utils.get_refresh_token({ + 'headers': { + 'Cookie': f"{utils.get_config().cookie_name_refresh_token}=foobar", + }, + }) def test_refresh_token_expired_token(): @@ -62,18 +61,17 @@ def test_refresh_token_expired_token(): 'secret', algorithm='HS256', ) - with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"): - with pytest.raises(utils.NotLoggedIn): - token = utils.get_refresh_token({ - 'headers': { - 'Cookie': f"{utils.get_config().cookie_name_refresh_token}={raw_token}", - } - }) + with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"), pytest.raises(utils.NotLoggedIn): + token = utils.get_refresh_token({ + 'headers': { + 'Cookie': f"{utils.get_config().cookie_name_refresh_token}={raw_token}", + }, + }) def test_refresh_token_valid_token(): now = time.time() - in_token = {'iat': now-1, 'exp': now + 5, 'azp': 'test', } + in_token = {'iat': now-1, 'exp': now + 5, 'azp': 'test' } raw_token = jwt.encode( in_token, 'secret', @@ -83,14 +81,14 @@ def test_refresh_token_valid_token(): token = utils.get_refresh_token({ 'headers': { 'Cookie': f"{utils.get_config().cookie_name_refresh_token}={raw_token}", - } + }, }) assert in_token == token def test_refresh_token_unsigned_token(): now = time.time() - in_token = {'iat': now, 'exp': now + 5, 'azp': 'test', } + in_token = {'iat': now, 'exp': now + 5, 'azp': 'test' } raw_token = \ jwt.utils.base64url_encode(json.dumps({ "typ": "JWT", @@ -100,10 +98,9 @@ def test_refresh_token_unsigned_token(): jwt.utils.base64url_encode(json.dumps(in_token).encode('utf-8')).decode('utf-8') + \ '.' + \ '' # no signature - with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"): - with pytest.raises(utils.BadRequest): - utils.get_refresh_token({ - 'headers': { - 'Cookie': f"{utils.get_config().cookie_name_refresh_token}={raw_token}", - } - }) + with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"), pytest.raises(utils.BadRequest): + utils.get_refresh_token({ + 'headers': { + 'Cookie': f"{utils.get_config().cookie_name_refresh_token}={raw_token}", + }, + }) From edc98adfc4b23bf17c5e82ac5f5a25099ba7eb42 Mon Sep 17 00:00:00 2001 From: Stefan - ZipKid - Goethals Date: Thu, 10 Sep 2026 14:10:13 +0200 Subject: [PATCH 3/5] uv / ruff config --- .gitignore | 2 + pyproject.toml | 41 +++++++++++++++++ tasks.py | 49 +++++++++++---------- uv.lock | 117 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 255ff72..6bb6bf7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ /output __pycache__ + +*.egg-info \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..06d8e45 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "aws-cloudfront-authorizer" +version = "1.0.0" +requires-python = ">=3.12" +dependencies = [ + "invoke>=2.2.1", + "troposphere>=4.10.1", +] + +[tool.uv] +package = true + +[tool.uv.sources] +central-helpers = { git = "ssh://git@bitbucket.org/vrt-prod/aws-cloudformation-helpers.git" } +custom-resources = { git = "https://github.com/vrtdev/custom-resources.git" } + +[tool.setuptools] +packages = [ + "templates", +] + +[tool.ruff] +line-length = 140 +indent-width = 4 + +[tool.ruff.lint] +extend-select = [ + "E", + "W", + "A", + "COM", + "TID", + "B", + "SIM", + "UP", +] + +[[tool.uv.index]] +name = "nexus" +url = "https://nexus.core.a51.be/repository/pypi/simple" +default = true diff --git a/tasks.py b/tasks.py index 1c5d6d6..896ad92 100644 --- a/tasks.py +++ b/tasks.py @@ -3,10 +3,22 @@ import fnmatch import glob import os +import sys from invoke import task +def glob_templates(filename): + templates = [x for x in glob.glob(filename)] + if len(templates) == 0: + print(f"File `{filename}` not found") + sys.exit(1) + templates = [x for x in templates if x[-3:] == '.py'] + if len(templates) == 0: + print(f"File `{filename}` doesn't seem to match any Python files, skipping") + sys.exit(0) + return templates + @task( default=True, help={ @@ -17,14 +29,14 @@ ) def build(ctx, warnings='once::DeprecationWarning', filename=None): """Build all templates.""" - import sys - import subprocess import inspect + import subprocess + import sys if filename is not None: templates = [x for x in glob.glob(filename)] if len(templates) == 0: - print("File `{}` not found".format(filename)) - exit(1) + print(f"File `{filename}` not found") + sys.exit(1) else: print("Building all templates") os.chdir(os.path.dirname(os.path.abspath(inspect.stack()[0][1]))) @@ -32,35 +44,28 @@ def build(ctx, warnings='once::DeprecationWarning', filename=None): rv = 0 for template in templates: - print(" + Executing {0}".format(template)) - if subprocess.call([sys.executable, '-W{0}'.format(warnings), '{0}'.format(template)]) != 0: + print(f" + Executing {template}") + if subprocess.call([sys.executable, f'-W{warnings}', f'{template}']) != 0: rv = 1 - exit(rv) + sys.exit(rv) @task( - aliases=["flake8", "pep8"], help={ 'filename': 'File(s) to lint. Supports globbing.', - 'envdir': 'Specify the python virtual env dir to ignore. Defaults to "venv".', - 'noglob': 'Disable globbing of filenames. Can give issues in virtual environments', + 'envdir': 'Does nothing, left for backwards compatibility.', + 'noglob': 'Does nothing, left for backwards compatibility.', }, ) def lint(ctx, filename=None, envdir='venv', noglob=False): - """Run flake8 python linter.""" - command = 'flake8 --jobs=1 --exclude .git,' + envdir + """Run python linter.""" + command = ['ruff', 'check'] if filename is not None: - if noglob: - templates = [filename] - else: - templates = [x for x in glob.glob(filename)] - if len(templates) == 0: - print("File `{0}` not found".format(filename)) - exit(1) - - command += ' ' + " ".join(templates) + templates = glob_templates(filename) + command += templates + command = ' '.join(command) print("Running command: '" + command + "'") ctx.run(command) @@ -77,7 +82,7 @@ def clean(ctx, verbose=False, compiled=False): patterns.append('output/*.json') patterns.append('output/*/*.json') if compiled is True: - for root, dirnames, filenames in os.walk('.'): + for root, _dirnames, filenames in os.walk('.'): for filename in fnmatch.filter(filenames, '*.pyc'): patterns.append(os.path.join(root, filename)) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..02274c3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,117 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aws-cloudfront-authorizer" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "invoke" }, + { name = "troposphere" }, +] + +[package.metadata] +requires-dist = [ + { name = "invoke", specifier = ">=2.2.1" }, + { name = "troposphere", specifier = ">=4.10.1" }, +] + +[[package]] +name = "cfn-flip" +version = "1.3.0" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +dependencies = [ + { name = "click" }, + { name = "pyyaml" }, + { name = "six" }, +] +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/cfn-flip/1.3.0/cfn_flip-1.3.0.tar.gz", hash = "sha256:003e02a089c35e1230ffd0e1bcfbbc4b12cc7d2deb2fcc6c4228ac9819307362", size = 16113, upload-time = "2021-10-07T10:05:14.956Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/cfn-flip/1.3.0/cfn_flip-1.3.0-py3-none-any.whl", hash = "sha256:faca8e77f0d32fb84cce1db1ef4c18b14a325d31125dae73c13bcc01947d2722", size = 21387, upload-time = "2021-10-07T10:05:13.378Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/click/8.5.0/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/click/8.5.0/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "invoke" +version = "3.0.3" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/invoke/3.0.3/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/invoke/3.0.3/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://nexus.core.a51.be/repository/pypi/packages/pyyaml/6.0.3/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/six/1.17.0/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/six/1.17.0/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "troposphere" +version = "4.10.2" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +dependencies = [ + { name = "cfn-flip" }, +] +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/troposphere/4.10.2/troposphere-4.10.2.tar.gz", hash = "sha256:24978785ac4ce43a9c03348eaf393bedd552eb86e8f4d405e36990cd11695c53", size = 545896, upload-time = "2026-05-16T18:43:30.761Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/troposphere/4.10.2/troposphere-4.10.2-py3-none-any.whl", hash = "sha256:e74dbbc39ac375e19431bcf3cf181482313c71bfe1b8778d1ddbcb46a85b5a9c", size = 630411, upload-time = "2026-05-16T18:43:28.814Z" }, +] From 6f0f55ff8f67ab3aff6012a4fd9b33455e6f9c29 Mon Sep 17 00:00:00 2001 From: Stefan - ZipKid - Goethals Date: Thu, 10 Sep 2026 14:26:13 +0200 Subject: [PATCH 4/5] More Ruff Linter Fixes --- src/authenticate.py | 12 +++++++----- src/batch_authorize.py | 6 ++---- src/cognito_utils.py | 5 ++--- src/index.py | 5 ++--- src/utils.py | 15 +++++++++------ test/utils_test.py | 8 ++++---- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/authenticate.py b/src/authenticate.py index e4bd6c9..37effe6 100644 --- a/src/authenticate.py +++ b/src/authenticate.py @@ -21,8 +21,10 @@ logger = Logger() -class InternalServerError(Exception): pass -class BadRequest(Exception): pass +class InternalServerError(Exception): + pass +class BadRequest(Exception): + pass def exchange_cognito_code(event: dict, cognito_code: str) -> dict: @@ -56,7 +58,7 @@ def exchange_cognito_code(event: dict, cognito_code: str) -> dict: ) except requests.exceptions.ConnectionError: logger.exception({"message": "Connection error to Cognito"}) - raise InternalServerError() + raise InternalServerError() from None if token_response.status_code != 200: try: @@ -89,10 +91,10 @@ def exchange_cognito_code(event: dict, cognito_code: str) -> dict: ) except requests.exceptions.RequestException: logger.exception({"message": "Connection error to Cognito"}) - raise InternalServerError() + raise InternalServerError() from None except jwt.InvalidTokenError: logger.exception({"message": "id_token invalid"}) - raise InternalServerError() + raise InternalServerError() from None logger.info("Cognito ID token is valid") diff --git a/src/batch_authorize.py b/src/batch_authorize.py index 2c615ff..849f971 100644 --- a/src/batch_authorize.py +++ b/src/batch_authorize.py @@ -33,10 +33,8 @@ def handler(event, context: LambdaContext) -> dict: except InternalServerError as e: return internal_server_error('', e) - if 'domains' in refresh_token: # delegated token with domain restrictions - domains = refresh_token['domains'] - else: - domains = get_domains() + # delegated token with domain restrictions + domains = refresh_token['domains'] if 'domains' in refresh_token else get_domains() access_tokens = {} try: diff --git a/src/cognito_utils.py b/src/cognito_utils.py index 12c117d..060128f 100644 --- a/src/cognito_utils.py +++ b/src/cognito_utils.py @@ -1,3 +1,4 @@ +import contextlib import functools import jwt @@ -6,10 +7,8 @@ # Use pure python implementation for crypto from jwt_rsa_algo import RsaAlgorithm -try: +with contextlib.suppress(ValueError): # Assume already registered jwt.register_algorithm('RS256', RsaAlgorithm(RsaAlgorithm.SHA256)) -except ValueError: - pass # Assume already registered @functools.lru_cache(maxsize=1) diff --git a/src/index.py b/src/index.py index 5c657a0..ee6f9e6 100644 --- a/src/index.py +++ b/src/index.py @@ -1,3 +1,4 @@ +import contextlib import json import os import time @@ -33,10 +34,8 @@ def handler(event, context) -> dict: azp = refresh_token['azp'] # Mandatory sub = refresh_token.get('sub', []) # optional - try: + with contextlib.suppress(KeyError): domains = refresh_token['domains'] - except KeyError: - pass except (NotLoggedIn, BadRequest): pass except InternalServerError as e: diff --git a/src/utils.py b/src/utils.py index ceb634f..76056dc 100644 --- a/src/utils.py +++ b/src/utils.py @@ -214,9 +214,12 @@ def redirect_to_cognito(state: str = '') -> dict: } -class NotLoggedIn(Exception): pass -class BadRequest(Exception): pass -class InternalServerError(Exception): pass +class NotLoggedIn(Exception): + pass +class BadRequest(Exception): + pass +class InternalServerError(Exception): + pass def get_raw_refresh_token(event) -> str: @@ -226,7 +229,7 @@ def get_raw_refresh_token(event) -> str: raw_refresh_token = request_cookies[get_config().cookie_name_refresh_token].value except (KeyError, IndexError): logger.exception("No refresh_token cookie found") - raise NotLoggedIn() + raise NotLoggedIn() from None return raw_refresh_token @@ -240,10 +243,10 @@ def parse_raw_refresh_token(raw_refresh_token: str) -> dict: logger.info({"message": "Valid refresh_token found", "jwt": refresh_token}) except jwt.ExpiredSignatureError: logger.exception("Expired token") - raise NotLoggedIn() + raise NotLoggedIn() from None except jwt.InvalidTokenError: logger.exception("Invalid token") - raise BadRequest("Could not decode token") + raise BadRequest("Could not decode token") from None return refresh_token diff --git a/test/utils_test.py b/test/utils_test.py index 2540678..79bacec 100644 --- a/test/utils_test.py +++ b/test/utils_test.py @@ -27,14 +27,14 @@ def test_canon_header(): def test_refresh_token_no_cookie(): with pytest.raises(utils.NotLoggedIn): - token = utils.get_refresh_token({ + utils.get_refresh_token({ 'headers': {}, }) def test_refresh_token_other_cookie(): with pytest.raises(utils.NotLoggedIn): - token = utils.get_refresh_token({ + utils.get_refresh_token({ 'headers': { 'Cookie': 'foo=bar', }, @@ -43,7 +43,7 @@ def test_refresh_token_other_cookie(): def test_refresh_token_invalid_token(): with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"), pytest.raises(utils.BadRequest): - token = utils.get_refresh_token({ + utils.get_refresh_token({ 'headers': { 'Cookie': f"{utils.get_config().cookie_name_refresh_token}=foobar", }, @@ -62,7 +62,7 @@ def test_refresh_token_expired_token(): algorithm='HS256', ) with mock.patch('src.utils.get_refresh_token_jwt_secret', return_value="secret"), pytest.raises(utils.NotLoggedIn): - token = utils.get_refresh_token({ + utils.get_refresh_token({ 'headers': { 'Cookie': f"{utils.get_config().cookie_name_refresh_token}={raw_token}", }, From c6845e313a7c96a6e3ca10e76647cbb97d93a628 Mon Sep 17 00:00:00 2001 From: Stefan - ZipKid - Goethals Date: Thu, 10 Sep 2026 15:05:29 +0200 Subject: [PATCH 5/5] Add pytest --- pyproject.toml | 1 + uv.lock | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 06d8e45..aadf468 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "1.0.0" requires-python = ">=3.12" dependencies = [ "invoke>=2.2.1", + "pytest", "troposphere>=4.10.1", ] diff --git a/uv.lock b/uv.lock index 02274c3..3addcf4 100644 --- a/uv.lock +++ b/uv.lock @@ -8,12 +8,14 @@ version = "1.0.0" source = { editable = "." } dependencies = [ { name = "invoke" }, + { name = "pytest" }, { name = "troposphere" }, ] [package.metadata] requires-dist = [ { name = "invoke", specifier = ">=2.2.1" }, + { name = "pytest" }, { name = "troposphere", specifier = ">=4.10.1" }, ] @@ -40,6 +42,24 @@ wheels = [ { url = "https://nexus.core.a51.be/repository/pypi/packages/click/8.5.0/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/colorama/0.4.6/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/colorama/0.4.6/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/iniconfig/2.3.0/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/iniconfig/2.3.0/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "invoke" version = "3.0.3" @@ -49,6 +69,49 @@ wheels = [ { url = "https://nexus.core.a51.be/repository/pypi/packages/invoke/3.0.3/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, ] +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/packaging/26.3/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/packaging/26.3/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/pluggy/1.6.0/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/pluggy/1.6.0/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/pygments/2.21.0/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/pygments/2.21.0/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://nexus.core.a51.be/repository/pypi/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://nexus.core.a51.be/repository/pypi/packages/pytest/9.1.1/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://nexus.core.a51.be/repository/pypi/packages/pytest/9.1.1/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3"