update layercake
This commit is contained in:
@@ -5,8 +5,10 @@ import boto3
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mypy_boto3_dynamodb.client import DynamoDBClient
|
from mypy_boto3_dynamodb.client import DynamoDBClient
|
||||||
|
from mypy_boto3_s3 import S3Client
|
||||||
else:
|
else:
|
||||||
DynamoDBClient = object
|
DynamoDBClient = object
|
||||||
|
S3Client = object
|
||||||
|
|
||||||
|
|
||||||
def get_dynamodb_client() -> DynamoDBClient:
|
def get_dynamodb_client() -> DynamoDBClient:
|
||||||
@@ -18,4 +20,5 @@ def get_dynamodb_client() -> DynamoDBClient:
|
|||||||
return boto3.client('dynamodb', endpoint_url=f'http://{host}:8000')
|
return boto3.client('dynamodb', endpoint_url=f'http://{host}:8000')
|
||||||
|
|
||||||
|
|
||||||
|
s3_client: S3Client = boto3.client('s3')
|
||||||
dynamodb_client: DynamoDBClient = get_dynamodb_client()
|
dynamodb_client: DynamoDBClient = get_dynamodb_client()
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ import os
|
|||||||
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
||||||
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
|
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
|
||||||
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
|
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
|
||||||
|
|
||||||
|
BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore
|
||||||
|
|||||||
33
api.saladeaula.digital/app/form_data.py
Normal file
33
api.saladeaula.digital/app/form_data.py
Normal file
@@ -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
|
||||||
@@ -13,8 +13,8 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
|||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
|
|
||||||
from api_gateway import JSONResponse
|
from api_gateway import JSONResponse
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client, s3_client
|
||||||
from config import COURSE_TABLE
|
from config import BUCKET_NAME, COURSE_TABLE
|
||||||
from form_data import parse
|
from form_data import parse
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
@@ -32,21 +32,18 @@ def get_course(course_id: str):
|
|||||||
|
|
||||||
class Cert(BaseModel):
|
class Cert(BaseModel):
|
||||||
exp_interval: int | None = None
|
exp_interval: int | None = None
|
||||||
rawfile: bytes | None = None
|
s3_uri: str | None = None
|
||||||
|
|
||||||
def model_dump(self, **kwargs) -> dict[str, Any]:
|
def model_dump(self, **kwargs) -> dict[str, Any]:
|
||||||
return super().model_dump(
|
return super().model_dump(exclude_none=True, **kwargs)
|
||||||
exclude={'rawfile'},
|
|
||||||
exclude_none=True,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Course(BaseModel):
|
class Course(BaseModel):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
name: str
|
name: str
|
||||||
access_period: int
|
access_period: int
|
||||||
cert: Cert | None = None
|
cert: Cert
|
||||||
|
rawfile: bytes | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.put('/<course_id>')
|
@router.put('/<course_id>')
|
||||||
@@ -58,10 +55,21 @@ def edit_course(course_id: str):
|
|||||||
|
|
||||||
body = BytesIO(event.decoded_body.encode())
|
body = BytesIO(event.decoded_body.encode())
|
||||||
course = Course.model_validate(
|
course = Course.model_validate(
|
||||||
{'id': course_id} | parse(event.headers, body),
|
{'id': course_id, 'cert': {}} | parse(event.headers, body),
|
||||||
)
|
)
|
||||||
now_ = now()
|
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:
|
with dyn.transact_writer() as transact:
|
||||||
transact.update(
|
transact.update(
|
||||||
key=KeyPair(str(course.id), '0'),
|
key=KeyPair(str(course.id), '0'),
|
||||||
@@ -72,7 +80,7 @@ def edit_course(course_id: str):
|
|||||||
},
|
},
|
||||||
expr_attr_values={
|
expr_attr_values={
|
||||||
':name': course.name,
|
':name': course.name,
|
||||||
':cert': course.cert.model_dump() if course.cert else None,
|
':cert': course.cert.model_dump(),
|
||||||
':access_period': course.access_period,
|
':access_period': course.access_period,
|
||||||
':updated_at': now_,
|
':updated_at': now_,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ dev = [
|
|||||||
"jsonlines>=4.0.0",
|
"jsonlines>=4.0.0",
|
||||||
"pytest>=8.3.4",
|
"pytest>=8.3.4",
|
||||||
"pytest-cov>=6.0.0",
|
"pytest-cov>=6.0.0",
|
||||||
"python-multipart>=0.0.20",
|
|
||||||
"requests-toolbelt>=1.0.0",
|
"requests-toolbelt>=1.0.0",
|
||||||
"ruff>=0.9.1",
|
"ruff>=0.9.1",
|
||||||
"sqlite-utils>=3.38",
|
"sqlite-utils>=3.38",
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ Parameters:
|
|||||||
EnrollmentTable:
|
EnrollmentTable:
|
||||||
Type: String
|
Type: String
|
||||||
Default: betaeducacao-prod-enrollments
|
Default: betaeducacao-prod-enrollments
|
||||||
|
BucketName:
|
||||||
|
Type: String
|
||||||
|
Default: saladeaula.digital
|
||||||
|
|
||||||
Globals:
|
Globals:
|
||||||
Function:
|
Function:
|
||||||
@@ -17,7 +20,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:97
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
@@ -27,6 +30,7 @@ Globals:
|
|||||||
DYNAMODB_PARTITION_KEY: id
|
DYNAMODB_PARTITION_KEY: id
|
||||||
COURSE_TABLE: !Ref CourseTable
|
COURSE_TABLE: !Ref CourseTable
|
||||||
ENROLLMENT_TABLE: !Ref EnrollmentTable
|
ENROLLMENT_TABLE: !Ref EnrollmentTable
|
||||||
|
BUCKET_NAME: !Ref BucketName
|
||||||
|
|
||||||
Resources:
|
Resources:
|
||||||
HttpLog:
|
HttpLog:
|
||||||
@@ -42,7 +46,7 @@ Resources:
|
|||||||
AllowMethods: [GET, POST, PUT, DELETE, PATCH, OPTIONS]
|
AllowMethods: [GET, POST, PUT, DELETE, PATCH, OPTIONS]
|
||||||
AllowHeaders: [Content-Type, X-Requested-With, Authorization]
|
AllowHeaders: [Content-Type, X-Requested-With, Authorization]
|
||||||
AllowCredentials: false
|
AllowCredentials: false
|
||||||
MaxAge: 600
|
MaxAge: 600 # 10 minutes
|
||||||
Auth:
|
Auth:
|
||||||
DefaultAuthorizer: OAuth2Authorizer
|
DefaultAuthorizer: OAuth2Authorizer
|
||||||
Authorizers:
|
Authorizers:
|
||||||
@@ -66,6 +70,8 @@ Resources:
|
|||||||
TableName: !Ref CourseTable
|
TableName: !Ref CourseTable
|
||||||
- DynamoDBCrudPolicy:
|
- DynamoDBCrudPolicy:
|
||||||
TableName: !Ref EnrollmentTable
|
TableName: !Ref EnrollmentTable
|
||||||
|
- S3WritePolicy:
|
||||||
|
BucketName: !Ref BucketName
|
||||||
Events:
|
Events:
|
||||||
Preflight:
|
Preflight:
|
||||||
Type: HttpApi
|
Type: HttpApi
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ SK = 'sk'
|
|||||||
def pytest_configure():
|
def pytest_configure():
|
||||||
os.environ['TZ'] = 'America/Sao_Paulo'
|
os.environ['TZ'] = 'America/Sao_Paulo'
|
||||||
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
|
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
|
||||||
|
os.environ['BUCKET_NAME'] = 'saladeaula.digital'
|
||||||
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
||||||
os.environ['DYNAMODB_SORT_KEY'] = SK
|
os.environ['DYNAMODB_SORT_KEY'] = SK
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ def test_edit_course(
|
|||||||
'given_cert': 'true',
|
'given_cert': 'true',
|
||||||
'name': 'pytest updated from test',
|
'name': 'pytest updated from test',
|
||||||
'access_period': '365',
|
'access_period': '365',
|
||||||
'cert.exp_interval': '360',
|
'rawfile': ('sample.html', f, 'text/html'),
|
||||||
'cert.rawfile': f,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
r = app.lambda_handler(
|
r = app.lambda_handler(
|
||||||
@@ -59,4 +58,7 @@ def test_edit_course(
|
|||||||
key={'id': course_id, 'sk': '0'},
|
key={'id': course_id, 'sk': '0'},
|
||||||
)
|
)
|
||||||
|
|
||||||
print(r)
|
assert (
|
||||||
|
r['cert']['s3_uri']
|
||||||
|
== 's3://saladeaula.digital/certs/2a8963fc-4694-4fe2-953a-316d1b10f1f5.html'
|
||||||
|
)
|
||||||
|
|||||||
6
api.saladeaula.digital/uv.lock
generated
6
api.saladeaula.digital/uv.lock
generated
@@ -25,7 +25,6 @@ dev = [
|
|||||||
{ name = "jsonlines" },
|
{ name = "jsonlines" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-cov" },
|
{ name = "pytest-cov" },
|
||||||
{ name = "python-multipart" },
|
|
||||||
{ name = "requests-toolbelt" },
|
{ name = "requests-toolbelt" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
{ name = "sqlite-utils" },
|
{ name = "sqlite-utils" },
|
||||||
@@ -41,7 +40,6 @@ dev = [
|
|||||||
{ name = "jsonlines", specifier = ">=4.0.0" },
|
{ name = "jsonlines", specifier = ">=4.0.0" },
|
||||||
{ name = "pytest", specifier = ">=8.3.4" },
|
{ name = "pytest", specifier = ">=8.3.4" },
|
||||||
{ name = "pytest-cov", specifier = ">=6.0.0" },
|
{ name = "pytest-cov", specifier = ">=6.0.0" },
|
||||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
|
||||||
{ name = "requests-toolbelt", specifier = ">=1.0.0" },
|
{ name = "requests-toolbelt", specifier = ">=1.0.0" },
|
||||||
{ name = "ruff", specifier = ">=0.9.1" },
|
{ name = "ruff", specifier = ">=0.9.1" },
|
||||||
{ name = "sqlite-utils", specifier = ">=3.38" },
|
{ name = "sqlite-utils", specifier = ">=3.38" },
|
||||||
@@ -594,7 +592,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.9.14"
|
version = "0.10.0"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
@@ -611,6 +609,7 @@ dependencies = [
|
|||||||
{ name = "pycpfcnpj" },
|
{ name = "pycpfcnpj" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
{ name = "pydantic-extra-types" },
|
{ name = "pydantic-extra-types" },
|
||||||
|
{ name = "python-multipart" },
|
||||||
{ name = "pytz" },
|
{ name = "pytz" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "smart-open", extra = ["s3"] },
|
{ name = "smart-open", extra = ["s3"] },
|
||||||
@@ -634,6 +633,7 @@ requires-dist = [
|
|||||||
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
||||||
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
||||||
|
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||||
{ name = "pytz", specifier = ">=2025.1" },
|
{ name = "pytz", specifier = ">=2025.1" },
|
||||||
{ name = "requests", specifier = ">=2.32.3" },
|
{ name = "requests", specifier = ">=2.32.3" },
|
||||||
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
|
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
enrollment,
|
enrollment,
|
||||||
org=metadata.get('org', None),
|
org=metadata.get('org', None),
|
||||||
subscription=subscription,
|
subscription=subscription,
|
||||||
deduplication_window={'offset_days': metadata['dedup_window_offset_days']},
|
deduplication_window={
|
||||||
linked_entities=frozenset({LinkedEntity(new_image['id'], 'ENROLLMENT')}),
|
'offset_days': metadata['dedup_window_offset_days'],
|
||||||
|
},
|
||||||
|
linked_entities=frozenset(
|
||||||
|
{LinkedEntity(new_image['id'], 'ENROLLMENT')},
|
||||||
|
),
|
||||||
persistence_layer=dyn,
|
persistence_layer=dyn,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.9.14"
|
version = "0.10.0"
|
||||||
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -27,6 +27,7 @@ dependencies = [
|
|||||||
"passlib>=1.7.4",
|
"passlib>=1.7.4",
|
||||||
"psycopg[binary]>=3.2.9",
|
"psycopg[binary]>=3.2.9",
|
||||||
"joserfc>=1.2.2",
|
"joserfc>=1.2.2",
|
||||||
|
"python-multipart>=0.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
13
layercake/uv.lock
generated
13
layercake/uv.lock
generated
@@ -675,7 +675,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.9.14"
|
version = "0.10.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
@@ -692,6 +692,7 @@ dependencies = [
|
|||||||
{ name = "pycpfcnpj" },
|
{ name = "pycpfcnpj" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
{ name = "pydantic-extra-types" },
|
{ name = "pydantic-extra-types" },
|
||||||
|
{ name = "python-multipart" },
|
||||||
{ name = "pytz" },
|
{ name = "pytz" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "smart-open", extra = ["s3"] },
|
{ name = "smart-open", extra = ["s3"] },
|
||||||
@@ -726,6 +727,7 @@ requires-dist = [
|
|||||||
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
||||||
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
||||||
|
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||||
{ name = "pytz", specifier = ">=2025.1" },
|
{ name = "pytz", specifier = ">=2025.1" },
|
||||||
{ name = "requests", specifier = ">=2.32.3" },
|
{ name = "requests", specifier = ">=2.32.3" },
|
||||||
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2025.2"
|
version = "2025.2"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ REPLY_TO = ('Carolina Brand', 'carolina@somosbeta.com.br')
|
|||||||
BCC = [
|
BCC = [
|
||||||
'sergio@somosbeta.com.br',
|
'sergio@somosbeta.com.br',
|
||||||
'carolina@somosbeta.com.br',
|
'carolina@somosbeta.com.br',
|
||||||
|
'tiago@somosbeta.com.br',
|
||||||
]
|
]
|
||||||
MESSAGE = """
|
MESSAGE = """
|
||||||
Oi, tudo bem?<br/><br/>
|
Oi, tudo bem?<br/><br/>
|
||||||
|
|||||||
Reference in New Issue
Block a user