diff --git a/enrollments-events/app/enrollment.py b/enrollments-events/app/enrollment.py index 856e273..842fe07 100644 --- a/enrollments-events/app/enrollment.py +++ b/enrollments-events/app/enrollment.py @@ -1,4 +1,7 @@ +from abc import ABC +from dataclasses import dataclass from datetime import timedelta +from enum import Enum from typing import NotRequired, Self, TypedDict from layercake.dateutils import now, ttl @@ -32,15 +35,16 @@ Subscription = TypedDict( ) -class LinkedEntity(str): - def __new__(cls, id: str, type: str) -> Self: - return super().__new__(cls, '#'.join([type.lower(), id])) +class Kind(str, Enum): + ORDER = 'ORDER' + ENROLLMENT = 'ENROLLMENT' - def __init__(self, id: str, type: str) -> None: - # __init__ is used to store the parameters for later reference. - # For immutable types like str, __init__ cannot change the instance's value. - self.id = id - self.type = type + +@dataclass(frozen=True) +class LinkedEntity(ABC): + id: str + kind: Kind + table_name: str | None = None class DeduplicationConflictError(Exception): @@ -76,29 +80,27 @@ def enroll( ) # Relationships between this enrollment and its related entities - for parent_entity in linked_entities: - perent_id = parent_entity.id - entity_sk = f'LINKED_ENTITIES#{parent_entity.type}' - keyprefix = parent_entity.type.lower() - + for entity in linked_entities: # Parent knows the child transact.put( item={ - 'id': perent_id, - 'sk': f'{entity_sk}#CHILD', + 'id': entity.id, + 'sk': f'LINKED_ENTITIES#{entity.kind.value}#CHILD', 'created_at': now_, - f'{keyprefix}_id': enrollment.id, + 'enrollment_id': enrollment.id, }, cond_expr='attribute_not_exists(sk)', + table_name=entity.table_name, ) + keyprefix = entity.kind.value.lower() # Child knows the parent transact.put( item={ 'id': enrollment.id, - 'sk': f'{entity_sk}#PARENT', + 'sk': f'LINKED_ENTITIES#{entity.kind.value}#PARENT', 'created_at': now_, - f'{keyprefix}_id': perent_id, + f'{keyprefix}_id': entity.id, }, cond_expr='attribute_not_exists(sk)', ) diff --git a/enrollments-events/app/events/allocate_slots.py b/enrollments-events/app/events/allocate_slots.py index b36c9f5..b7a47bd 100644 --- a/enrollments-events/app/events/allocate_slots.py +++ b/enrollments-events/app/events/allocate_slots.py @@ -81,6 +81,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: class Course: id: str name: str + access_period: int def _get_courses(ids: set) -> tuple[Course, ...]: @@ -93,6 +94,7 @@ def _get_courses(ids: set) -> tuple[Course, ...]: Course( id=idx, name=obj['name'], + access_period=obj['access_period'], ) for idx, obj in result.items() ) diff --git a/enrollments-events/app/events/enroll.py b/enrollments-events/app/events/enroll.py index 41832b4..bc60608 100644 --- a/enrollments-events/app/events/enroll.py +++ b/enrollments-events/app/events/enroll.py @@ -18,7 +18,11 @@ from layercake.dynamodb import ( from boto3clients import dynamodb_client from config import COURSE_TABLE, ENROLLMENT_TABLE, ORDER_TABLE -from enrollment import LinkedEntity, enroll +from enrollment import ( + Kind, + LinkedEntity, + enroll, +) from schemas import Course, Enrollment, User logger = Logger(__name__) @@ -89,7 +93,15 @@ def _handler(record: Course, context: dict) -> Enrollment: enrollment, persistence_layer=enrollment_layer, deduplication_window={'offset_days': 90}, - linked_entities=frozenset({LinkedEntity(context['order_id'], 'ORDER')}), + linked_entities=frozenset( + { + LinkedEntity( + id=context['order_id'], + kind=Kind.ORDER, + table_name=ORDER_TABLE, + ), + } + ), ) return enrollment diff --git a/enrollments-events/app/events/issue_cert.py b/enrollments-events/app/events/issue_cert.py index 3955c95..ec0f4fc 100644 --- a/enrollments-events/app/events/issue_cert.py +++ b/enrollments-events/app/events/issue_cert.py @@ -56,6 +56,9 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: ) try: + if 's3_uri' not in cert: + raise ValueError('Template URI is missing') + # Send template URI and data to Paperforge API to generate a PDF r = requests.post( PAPERFORGE_API, @@ -79,10 +82,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: ), }, ), + timeout=5, ) r.raise_for_status() - object_key = f'issuedcerts/{enrollment_id}.pdf' + object_key = f'certs/{enrollment_id}.pdf' s3_uri = f's3://{BUCKET_NAME}/{object_key}' s3_client.put_object( @@ -93,10 +97,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: ) logger.debug(f'PDF uploaded successfully to {s3_uri}') - except KeyError: + except ValueError as exc: # PDF generation fails if template URI is missing s3_uri = None - logger.debug('Template URI is missing') + logger.exception(exc) except requests.exceptions.RequestException as exc: logger.exception(exc) raise diff --git a/enrollments-events/app/events/reenroll_if_failed.py b/enrollments-events/app/events/reenroll_if_failed.py index c71bee7..09b55c1 100644 --- a/enrollments-events/app/events/reenroll_if_failed.py +++ b/enrollments-events/app/events/reenroll_if_failed.py @@ -10,7 +10,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, SortKey, TransactKey from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE -from enrollment import LinkedEntity, enroll +from enrollment import Kind, LinkedEntity, enroll from schemas import Course, Enrollment, User logger = Logger(__name__) @@ -52,7 +52,12 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: 'offset_days': metadata['dedup_window_offset_days'], }, linked_entities=frozenset( - {LinkedEntity(new_image['id'], 'ENROLLMENT')}, + { + LinkedEntity( + id=new_image['id'], + kind=Kind.ENROLLMENT, + ), + }, ), persistence_layer=dyn, ) diff --git a/enrollments-events/app/events/cert_reporting/__init__.py b/enrollments-events/app/events/reporting/__init__.py similarity index 100% rename from enrollments-events/app/events/cert_reporting/__init__.py rename to enrollments-events/app/events/reporting/__init__.py diff --git a/enrollments-events/app/events/cert_reporting/append_cert.py b/enrollments-events/app/events/reporting/append_cert.py similarity index 100% rename from enrollments-events/app/events/cert_reporting/append_cert.py rename to enrollments-events/app/events/reporting/append_cert.py diff --git a/enrollments-events/app/events/cert_reporting/send_report_email.py b/enrollments-events/app/events/reporting/send_report_email.py similarity index 100% rename from enrollments-events/app/events/cert_reporting/send_report_email.py rename to enrollments-events/app/events/reporting/send_report_email.py diff --git a/enrollments-events/app/events/set_access_expired.py b/enrollments-events/app/events/set_access_expired.py index 383cf4d..c8ac9e8 100644 --- a/enrollments-events/app/events/set_access_expired.py +++ b/enrollments-events/app/events/set_access_expired.py @@ -29,8 +29,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: pk=old_image['id'], sk='0', ), - update_expr='SET access_expired = :true, \ - updated_at = :now', + update_expr='SET access_expired = :true, updated_at = :now', expr_attr_values={ ':true': True, ':now': now_, diff --git a/enrollments-events/template.yaml b/enrollments-events/template.yaml index a3c501d..4a86f5e 100644 --- a/enrollments-events/template.yaml +++ b/enrollments-events/template.yaml @@ -308,6 +308,7 @@ Resources: Type: AWS::Serverless::Function Properties: Handler: events.issue_cert.lambda_handler + # Timeout: 30 LoggingConfig: LogGroup: !Ref EventLog Policies: @@ -331,10 +332,10 @@ Resources: old_image: status: [IN_PROGRESS] - EventCertReportingAppendCertFunction: + EventReportingAppendCertFunction: Type: AWS::Serverless::Function Properties: - Handler: events.cert_reporting.append_cert.lambda_handler + Handler: events.reporting.append_cert.lambda_handler LoggingConfig: LogGroup: !Ref EventLog Policies: @@ -346,23 +347,19 @@ Resources: Properties: Pattern: resources: [!Ref EnrollmentTable] + detail-type: [MODIFY] detail: keys: sk: ["0"] new_image: status: [COMPLETED] - cert: - exists: true org_id: - exists: true - old_image: - cert: - exists: false + - exists: true - EventCertReportingSendReportEmailFunction: + EventReportingSendReportEmailFunction: Type: AWS::Serverless::Function Properties: - Handler: events.cert_reporting.send_report_email.lambda_handler + Handler: events.reporting.send_report_email.lambda_handler LoggingConfig: LogGroup: !Ref EventLog Policies: diff --git a/enrollments-events/tests/conftest.py b/enrollments-events/tests/conftest.py index 7826e46..dcc83be 100644 --- a/enrollments-events/tests/conftest.py +++ b/enrollments-events/tests/conftest.py @@ -20,6 +20,7 @@ def pytest_configure(): os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME os.environ['BUCKET_NAME'] = 'saladeaula.digital' + os.environ['LOG_LEVEL'] = 'DEBUG' @dataclass diff --git a/enrollments-events/tests/events/cert_reporting/test_append_cert.py b/enrollments-events/tests/events/reporting/test_append_cert.py similarity index 97% rename from enrollments-events/tests/events/cert_reporting/test_append_cert.py rename to enrollments-events/tests/events/reporting/test_append_cert.py index a629852..4d4f323 100644 --- a/enrollments-events/tests/events/cert_reporting/test_append_cert.py +++ b/enrollments-events/tests/events/reporting/test_append_cert.py @@ -1,6 +1,6 @@ from datetime import timedelta -import app.events.cert_reporting.append_cert as app +import app.events.reporting.append_cert as app from aws_lambda_powertools.utilities.typing import LambdaContext from layercake.dateutils import now from layercake.dynamodb import ( diff --git a/enrollments-events/tests/events/cert_reporting/test_send_report_email.py b/enrollments-events/tests/events/reporting/test_send_report_email.py similarity index 92% rename from enrollments-events/tests/events/cert_reporting/test_send_report_email.py rename to enrollments-events/tests/events/reporting/test_send_report_email.py index 3e37b47..d5e8749 100644 --- a/enrollments-events/tests/events/cert_reporting/test_send_report_email.py +++ b/enrollments-events/tests/events/reporting/test_send_report_email.py @@ -1,4 +1,4 @@ -import app.events.cert_reporting.send_report_email as app +import app.events.reporting.send_report_email as app from aws_lambda_powertools.utilities.typing import LambdaContext from layercake.dynamodb import ( DynamoDBPersistenceLayer, diff --git a/enrollments-events/tests/events/test_enroll.py b/enrollments-events/tests/events/test_enroll.py index 59e32f2..f2ce5fe 100644 --- a/enrollments-events/tests/events/test_enroll.py +++ b/enrollments-events/tests/events/test_enroll.py @@ -1,6 +1,6 @@ import app.events.enroll as app from aws_lambda_powertools.utilities.typing import LambdaContext -from layercake.dynamodb import DynamoDBPersistenceLayer +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair def test_enroll( @@ -8,12 +8,26 @@ def test_enroll( dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, ): + order_id = 'cpYSbBcie2NDbZhDKCxCih' event = { 'detail': { 'new_image': { - 'id': 'cpYSbBcie2NDbZhDKCxCih', + 'id': order_id, 'sk': 'generated_items', } } } assert app.lambda_handler(event, lambda_context) # type: ignore + + # Parent knows the child + order_entity = dynamodb_persistence_layer.collection.get_item( + KeyPair(order_id, 'LINKED_ENTITIES#ORDER#CHILD') + ) + assert order_entity + + # Child knows the parent + enrollment_entity = dynamodb_persistence_layer.collection.get_item( + KeyPair(order_entity['enrollment_id'], 'LINKED_ENTITIES#ORDER#PARENT'), + ) + + assert enrollment_entity['order_id'] == order_id diff --git a/enrollments-events/tests/events/test_reenroll_if_failed.py b/enrollments-events/tests/events/test_reenroll_if_failed.py index c216ea7..84ae659 100644 --- a/enrollments-events/tests/events/test_reenroll_if_failed.py +++ b/enrollments-events/tests/events/test_reenroll_if_failed.py @@ -30,19 +30,19 @@ def test_reenroll( assert app.lambda_handler(event, lambda_context) # type: ignore # Parent knows the child - child_entity = dynamodb_persistence_layer.collection.get_item( + current_entity = dynamodb_persistence_layer.collection.get_item( KeyPair( pk=parent_id, sk='LINKED_ENTITIES#ENROLLMENT#CHILD', ) ) - assert child_entity + assert current_entity # Child knows the parent - parent_entity = dynamodb_persistence_layer.collection.get_item( + new_entity = dynamodb_persistence_layer.collection.get_item( KeyPair( - pk=child_entity['enrollment_id'], + pk=current_entity['enrollment_id'], sk='LINKED_ENTITIES#ENROLLMENT#PARENT', ) ) - assert parent_entity['enrollment_id'] == parent_id + assert new_entity['enrollment_id'] == parent_id diff --git a/http-api/app/routes/enrollments/__init__.py b/http-api/app/routes/enrollments/__init__.py index 866b2df..68b0b87 100644 --- a/http-api/app/routes/enrollments/__init__.py +++ b/http-api/app/routes/enrollments/__init__.py @@ -1,4 +1,5 @@ import urllib.parse as parse +from os import rename from aws_lambda_powertools.event_handler.api_gateway import Router from layercake.dynamodb import ( @@ -73,16 +74,16 @@ def get_enrollment(id: str): record = enrollment_layer.collection.get_items( TransactKey(id) + SortKey('0') + + SortKey('ORG', rename_key='org') # + SortKey('STARTED', rename_key='started_at', path_spec='started_at') # + SortKey('COMPLETED', rename_key='completed_at', path_spec='completed_at') # + SortKey('FAILED', rename_key='failed_at', path_spec='failed_at') + SortKey('CANCELED', rename_key='canceled') - + SortKey('ARCHIVED', rename_key='archived_at', path_spec='archived_at') + # + SortKey('ARCHIVED', rename_key='archived_at', path_spec='archived_at') + SortKey('CANCEL_POLICY', rename_key='cancel_policy') + SortKey('LOCK', rename_key='lock') + SortKey('parent_vacancy', path_spec='vacancy') + SortKey('author') - + SortKey('tenant') ) events = enrollment_layer.collection.query(KeyPair(id, 'SCHEDULE#')) diff --git a/http-api/app/routes/enrollments/download_issued_cert.py b/http-api/app/routes/enrollments/download_issued_cert.py index e208738..1322223 100644 --- a/http-api/app/routes/enrollments/download_issued_cert.py +++ b/http-api/app/routes/enrollments/download_issued_cert.py @@ -14,7 +14,7 @@ enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) def download(id: str): params = { 'Bucket': BUCKET_NAME, - 'Key': f'issuedcerts/{id}.pdf', + 'Key': f'certs/{id}.pdf', 'ResponseContentDisposition': f'attachment; filename="{id}.pdf"', } diff --git a/konviva-events/app/enrollment.py b/konviva-events/app/enrollment.py index 225d6f0..e7cb6f0 100644 --- a/konviva-events/app/enrollment.py +++ b/konviva-events/app/enrollment.py @@ -55,7 +55,7 @@ def update_progress( key=KeyPair(id, '0'), update_expr='SET progress = :progress, \ #status = :in_progress, \ - started_at = :now, \ + started_at = if_not_exists(started_at, :now), \ updated_at = :now', cond_expr='#status = :pending', expr_attr_names={ @@ -69,15 +69,6 @@ def update_progress( }, exc_cls=EnrollmentConflictError, ) - # Record the start date if it does not already exist - transact.put( - item={ - 'id': id, - 'sk': 'STARTED', - 'created_at': now_, - }, - cond_expr='attribute_not_exists(sk)', - ) # Schedule a reminder for inactivity transact.put( item={ @@ -192,7 +183,7 @@ def _set_status_as_completed( update_expr='SET #status = :completed, \ progress = :progress, \ score = :score, \ - completed_at = :now, \ + completed_at = if_not_exists(completed_at, :now), \ updated_at = :now', cond_expr='#status = :in_progress', expr_attr_names={'#status': 'status'}, @@ -205,14 +196,6 @@ def _set_status_as_completed( }, exc_cls=EnrollmentConflictError, ) - transact.put( - item={ - 'id': id, - 'sk': 'COMPLETED', - 'created_at': now_, - }, - cond_expr='attribute_not_exists(sk)', - ) if cert_exp_interval: transact.put( @@ -293,7 +276,7 @@ def _set_status_as_failed( progress = :progress, \ score = :score, \ access_expired = :true, \ - failed_at = :now, \ + failed_at = if_not_exists(failed_at, :now), \ updated_at = :now', cond_expr='#status = :in_progress', expr_attr_names={'#status': 'status'}, diff --git a/konviva-events/tests/test_app.py b/konviva-events/tests/test_app.py index 19df906..2ca3064 100644 --- a/konviva-events/tests/test_app.py +++ b/konviva-events/tests/test_app.py @@ -33,12 +33,13 @@ def test_start_progress( r = dynamodb_persistence_layer.collection.query( PartitionKey('d9da85f2-e09f-472d-9515-3d91d70f1e8a') ) - assert any(item.get('sk') == 'STARTED' for item in r['items']) + docx = next((x for x in r['items'] if x['sk'] == '0'), {}) + assert 'started_at' in docx assert any( item.get('sk') == 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS' for item in r['items'] ) - assert len(r['items']) == 3 + assert len(r['items']) == 2 def test_update_progress( @@ -110,8 +111,9 @@ def test_set_as_completed( PartitionKey('6c7e3d9b-f5d1-4da4-9e55-0825bb6ff2b8') ) - assert len(r['items']) == 8 - assert any(item.get('sk') == 'COMPLETED' for item in r['items']) + docx = next((x for x in r['items'] if x['sk'] == '0'), {}) + assert 'completed_at' in docx + assert len(r['items']) == 7 assert any(item.get('sk') == 'LOCK' for item in r['items']) assert any( item.get('sk') == 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS' diff --git a/konviva-events/uv.lock b/konviva-events/uv.lock index d6aec73..7f2a808 100644 --- a/konviva-events/uv.lock +++ b/konviva-events/uv.lock @@ -31,14 +31,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.1" +version = "1.6.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, ] [[package]] @@ -430,6 +430,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, ] +[[package]] +name = "joserfc" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/a0/4b8dfecc8ec3c15aa1f2ff7d5b947344378b5b595ce37c8a8fe6e25c1400/joserfc-1.4.0.tar.gz", hash = "sha256:e8c2f327bf10a937d284d57e9f8aec385381e5e5850469b50a7dade1aba59759", size = 196339, upload-time = "2025-10-09T07:47:00.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/05/342459b7629c6fcb5f99a646886ee2904491955b8cce6b26b0b9a498f67c/joserfc-1.4.0-py3-none-any.whl", hash = "sha256:46917e6b53f1ec0c7e20d34d6f3e6c27da0fa43d0d4ebfb89aada7c86582933a", size = 66390, upload-time = "2025-10-09T07:46:59.591Z" }, +] + [[package]] name = "jsonlines" version = "4.0.0" @@ -485,7 +497,7 @@ dev = [ [[package]] name = "layercake" -version = "0.9.14" +version = "0.10.1" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, @@ -494,6 +506,7 @@ dependencies = [ { name = "dictdiffer" }, { name = "ftfy" }, { name = "glom" }, + { name = "joserfc" }, { name = "meilisearch" }, { name = "orjson" }, { name = "passlib" }, @@ -501,7 +514,7 @@ dependencies = [ { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, - { name = "pyjwt" }, + { name = "python-multipart" }, { name = "pytz" }, { name = "requests" }, { name = "smart-open", extra = ["s3"] }, @@ -512,11 +525,12 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "arnparse", specifier = ">=0.0.2" }, - { name = "authlib", specifier = ">=1.6.1" }, + { name = "authlib", specifier = ">=1.6.5" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" }, { name = "dictdiffer", specifier = ">=0.9.0" }, { name = "ftfy", specifier = ">=6.3.1" }, { name = "glom", specifier = ">=24.11.0" }, + { name = "joserfc", specifier = ">=1.2.2" }, { name = "meilisearch", specifier = ">=0.34.0" }, { name = "orjson", specifier = ">=3.10.15" }, { name = "passlib", specifier = ">=1.7.4" }, @@ -524,7 +538,7 @@ requires-dist = [ { name = "pycpfcnpj", specifier = ">=1.8" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" }, { name = "pydantic-extra-types", specifier = ">=2.10.3" }, - { name = "pyjwt", specifier = ">=2.10.1" }, + { name = "python-multipart", specifier = ">=0.0.20" }, { name = "pytz", specifier = ">=2025.1" }, { name = "requests", specifier = ">=2.32.3" }, { name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" }, @@ -761,15 +775,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, -] - [[package]] name = "pytest" version = "8.4.1" @@ -821,6 +826,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + [[package]] name = "pytz" version = "2025.2"