From 161b75db8d37b0281013e983196d23ef46277f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Mon, 6 Oct 2025 23:16:42 -0300 Subject: [PATCH] update layercake --- api.saladeaula.digital/app/boto3clients.py | 3 ++ api.saladeaula.digital/app/config.py | 2 ++ api.saladeaula.digital/app/form_data.py | 33 +++++++++++++++++++ .../app/routes/courses/__init__.py | 30 ++++++++++------- api.saladeaula.digital/pyproject.toml | 1 - api.saladeaula.digital/template.yaml | 10 ++++-- api.saladeaula.digital/tests/conftest.py | 1 + .../tests/routes/test_courses.py | 8 +++-- api.saladeaula.digital/uv.lock | 6 ++-- .../app/events/reenroll_if_failed.py | 8 +++-- layercake/pyproject.toml | 3 +- layercake/uv.lock | 13 +++++++- .../events/billing/send_email_on_closing.py | 1 + 13 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 api.saladeaula.digital/app/form_data.py diff --git a/api.saladeaula.digital/app/boto3clients.py b/api.saladeaula.digital/app/boto3clients.py index 6e333d2..8f6f55e 100644 --- a/api.saladeaula.digital/app/boto3clients.py +++ b/api.saladeaula.digital/app/boto3clients.py @@ -5,8 +5,10 @@ import boto3 if TYPE_CHECKING: from mypy_boto3_dynamodb.client import DynamoDBClient + from mypy_boto3_s3 import S3Client else: DynamoDBClient = object + S3Client = object def get_dynamodb_client() -> DynamoDBClient: @@ -18,4 +20,5 @@ def get_dynamodb_client() -> DynamoDBClient: return boto3.client('dynamodb', endpoint_url=f'http://{host}:8000') +s3_client: S3Client = boto3.client('s3') dynamodb_client: DynamoDBClient = get_dynamodb_client() diff --git a/api.saladeaula.digital/app/config.py b/api.saladeaula.digital/app/config.py index 96e8b69..94932dc 100644 --- a/api.saladeaula.digital/app/config.py +++ b/api.saladeaula.digital/app/config.py @@ -3,3 +3,5 @@ import os USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore + +BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore diff --git a/api.saladeaula.digital/app/form_data.py b/api.saladeaula.digital/app/form_data.py new file mode 100644 index 0000000..8f8f652 --- /dev/null +++ b/api.saladeaula.digital/app/form_data.py @@ -0,0 +1,33 @@ +from io import BytesIO +from typing import Any + +from python_multipart import parse_form + + +def parse( + headers: dict[str, Any], + body: BytesIO, +) -> dict[str, Any]: + ret = {} + + def on_field(field): + field_name = field.field_name.decode().split('.') + + if len(field_name) > 1: + key, sub = field_name + + if key not in ret: + ret[key] = {} + + ret[key][sub] = field.value + else: + key, *_ = field_name + ret[key] = field.value + + def on_file(file): + file.file_object.seek(0) + ret[file.field_name.decode()] = file.file_object.read(-1) + + parse_form(headers, body, on_field=on_field, on_file=on_file) + + return ret diff --git a/api.saladeaula.digital/app/routes/courses/__init__.py b/api.saladeaula.digital/app/routes/courses/__init__.py index 7b44cf8..d923ab9 100644 --- a/api.saladeaula.digital/app/routes/courses/__init__.py +++ b/api.saladeaula.digital/app/routes/courses/__init__.py @@ -13,8 +13,8 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from pydantic import UUID4, BaseModel from api_gateway import JSONResponse -from boto3clients import dynamodb_client -from config import COURSE_TABLE +from boto3clients import dynamodb_client, s3_client +from config import BUCKET_NAME, COURSE_TABLE from form_data import parse logger = Logger(__name__) @@ -32,21 +32,18 @@ def get_course(course_id: str): class Cert(BaseModel): exp_interval: int | None = None - rawfile: bytes | None = None + s3_uri: str | None = None def model_dump(self, **kwargs) -> dict[str, Any]: - return super().model_dump( - exclude={'rawfile'}, - exclude_none=True, - **kwargs, - ) + return super().model_dump(exclude_none=True, **kwargs) class Course(BaseModel): id: UUID4 name: str access_period: int - cert: Cert | None = None + cert: Cert + rawfile: bytes | None = None @router.put('/') @@ -58,10 +55,21 @@ def edit_course(course_id: str): body = BytesIO(event.decoded_body.encode()) course = Course.model_validate( - {'id': course_id} | parse(event.headers, body), + {'id': course_id, 'cert': {}} | parse(event.headers, body), ) now_ = now() + if course.rawfile: + object_key = f'certs/{course_id}.html' + course.cert.s3_uri = f's3://{BUCKET_NAME}/{object_key}' + + s3_client.put_object( + Bucket=BUCKET_NAME, + Key=object_key, + Body=course.rawfile, + ContentType='text/html', + ) + with dyn.transact_writer() as transact: transact.update( key=KeyPair(str(course.id), '0'), @@ -72,7 +80,7 @@ def edit_course(course_id: str): }, expr_attr_values={ ':name': course.name, - ':cert': course.cert.model_dump() if course.cert else None, + ':cert': course.cert.model_dump(), ':access_period': course.access_period, ':updated_at': now_, }, diff --git a/api.saladeaula.digital/pyproject.toml b/api.saladeaula.digital/pyproject.toml index 8743b0c..ae716e9 100644 --- a/api.saladeaula.digital/pyproject.toml +++ b/api.saladeaula.digital/pyproject.toml @@ -12,7 +12,6 @@ dev = [ "jsonlines>=4.0.0", "pytest>=8.3.4", "pytest-cov>=6.0.0", - "python-multipart>=0.0.20", "requests-toolbelt>=1.0.0", "ruff>=0.9.1", "sqlite-utils>=3.38", diff --git a/api.saladeaula.digital/template.yaml b/api.saladeaula.digital/template.yaml index 6dd1b02..ece0d8f 100644 --- a/api.saladeaula.digital/template.yaml +++ b/api.saladeaula.digital/template.yaml @@ -8,6 +8,9 @@ Parameters: EnrollmentTable: Type: String Default: betaeducacao-prod-enrollments + BucketName: + Type: String + Default: saladeaula.digital Globals: Function: @@ -17,7 +20,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:97 Environment: Variables: TZ: America/Sao_Paulo @@ -27,6 +30,7 @@ Globals: DYNAMODB_PARTITION_KEY: id COURSE_TABLE: !Ref CourseTable ENROLLMENT_TABLE: !Ref EnrollmentTable + BUCKET_NAME: !Ref BucketName Resources: HttpLog: @@ -42,7 +46,7 @@ Resources: AllowMethods: [GET, POST, PUT, DELETE, PATCH, OPTIONS] AllowHeaders: [Content-Type, X-Requested-With, Authorization] AllowCredentials: false - MaxAge: 600 + MaxAge: 600 # 10 minutes Auth: DefaultAuthorizer: OAuth2Authorizer Authorizers: @@ -66,6 +70,8 @@ Resources: TableName: !Ref CourseTable - DynamoDBCrudPolicy: TableName: !Ref EnrollmentTable + - S3WritePolicy: + BucketName: !Ref BucketName Events: Preflight: Type: HttpApi diff --git a/api.saladeaula.digital/tests/conftest.py b/api.saladeaula.digital/tests/conftest.py index 052c8ad..1a6c9e9 100644 --- a/api.saladeaula.digital/tests/conftest.py +++ b/api.saladeaula.digital/tests/conftest.py @@ -17,6 +17,7 @@ SK = 'sk' def pytest_configure(): os.environ['TZ'] = 'America/Sao_Paulo' os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME + os.environ['BUCKET_NAME'] = 'saladeaula.digital' os.environ['DYNAMODB_PARTITION_KEY'] = PK os.environ['DYNAMODB_SORT_KEY'] = SK diff --git a/api.saladeaula.digital/tests/routes/test_courses.py b/api.saladeaula.digital/tests/routes/test_courses.py index 9c90ab1..924a4cf 100644 --- a/api.saladeaula.digital/tests/routes/test_courses.py +++ b/api.saladeaula.digital/tests/routes/test_courses.py @@ -37,8 +37,7 @@ def test_edit_course( 'given_cert': 'true', 'name': 'pytest updated from test', 'access_period': '365', - 'cert.exp_interval': '360', - 'cert.rawfile': f, + 'rawfile': ('sample.html', f, 'text/html'), } ) r = app.lambda_handler( @@ -59,4 +58,7 @@ def test_edit_course( key={'id': course_id, 'sk': '0'}, ) - print(r) + assert ( + r['cert']['s3_uri'] + == 's3://saladeaula.digital/certs/2a8963fc-4694-4fe2-953a-316d1b10f1f5.html' + ) diff --git a/api.saladeaula.digital/uv.lock b/api.saladeaula.digital/uv.lock index df71235..783e7e8 100644 --- a/api.saladeaula.digital/uv.lock +++ b/api.saladeaula.digital/uv.lock @@ -25,7 +25,6 @@ dev = [ { name = "jsonlines" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "python-multipart" }, { name = "requests-toolbelt" }, { name = "ruff" }, { name = "sqlite-utils" }, @@ -41,7 +40,6 @@ dev = [ { name = "jsonlines", specifier = ">=4.0.0" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "python-multipart", specifier = ">=0.0.20" }, { name = "requests-toolbelt", specifier = ">=1.0.0" }, { name = "ruff", specifier = ">=0.9.1" }, { name = "sqlite-utils", specifier = ">=3.38" }, @@ -594,7 +592,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.9.14" +version = "0.10.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, @@ -611,6 +609,7 @@ dependencies = [ { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, + { name = "python-multipart" }, { name = "pytz" }, { name = "requests" }, { name = "smart-open", extra = ["s3"] }, @@ -634,6 +633,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 = "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" }, diff --git a/enrollments-events/app/events/reenroll_if_failed.py b/enrollments-events/app/events/reenroll_if_failed.py index dd27fb6..30e91f5 100644 --- a/enrollments-events/app/events/reenroll_if_failed.py +++ b/enrollments-events/app/events/reenroll_if_failed.py @@ -50,7 +50,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: enrollment, org=metadata.get('org', None), subscription=subscription, - deduplication_window={'offset_days': metadata['dedup_window_offset_days']}, - linked_entities=frozenset({LinkedEntity(new_image['id'], 'ENROLLMENT')}), + deduplication_window={ + 'offset_days': metadata['dedup_window_offset_days'], + }, + linked_entities=frozenset( + {LinkedEntity(new_image['id'], 'ENROLLMENT')}, + ), persistence_layer=dyn, ) diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index 621aa32..dc5b57b 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.9.14" +version = "0.10.0" description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." readme = "README.md" authors = [ @@ -27,6 +27,7 @@ dependencies = [ "passlib>=1.7.4", "psycopg[binary]>=3.2.9", "joserfc>=1.2.2", + "python-multipart>=0.0.20", ] [dependency-groups] diff --git a/layercake/uv.lock b/layercake/uv.lock index 8cc0377..aa76731 100644 --- a/layercake/uv.lock +++ b/layercake/uv.lock @@ -675,7 +675,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.9.14" +version = "0.10.0" source = { editable = "." } dependencies = [ { name = "arnparse" }, @@ -692,6 +692,7 @@ dependencies = [ { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, + { name = "python-multipart" }, { name = "pytz" }, { name = "requests" }, { name = "smart-open", extra = ["s3"] }, @@ -726,6 +727,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 = "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" }, @@ -1293,6 +1295,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" diff --git a/order-events/app/events/billing/send_email_on_closing.py b/order-events/app/events/billing/send_email_on_closing.py index f3e4ae2..48eb492 100644 --- a/order-events/app/events/billing/send_email_on_closing.py +++ b/order-events/app/events/billing/send_email_on_closing.py @@ -18,6 +18,7 @@ REPLY_TO = ('Carolina Brand', 'carolina@somosbeta.com.br') BCC = [ 'sergio@somosbeta.com.br', 'carolina@somosbeta.com.br', + 'tiago@somosbeta.com.br', ] MESSAGE = """ Oi, tudo bem?