This commit is contained in:
2025-04-01 19:15:10 -03:00
parent dbe7a924e2
commit ef4bfc07f3
16 changed files with 197 additions and 44 deletions

1
http-api/.gitignore vendored
View File

@@ -2,3 +2,4 @@
env.json env.json
dynamodb_volume/ dynamodb_volume/
elastic_volume/ elastic_volume/
meili_data/

View File

@@ -4,6 +4,7 @@ from typing import Any
from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler.api_gateway import ( from aws_lambda_powertools.event_handler.api_gateway import (
APIGatewayHttpResolver, APIGatewayHttpResolver,
CORSConfig,
Response, Response,
content_types, content_types,
) )
@@ -12,20 +13,26 @@ from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities.typing import LambdaContext
from middlewares import AuthorizerMiddleware, TenantMiddleware from middlewares import AuthorizerMiddleware, TenantMiddleware
from routes import courses, enrollments, lookup, me, orders, users, webhooks from routes import courses, enrollments, lookup, orders, settings, users, webhooks
DEBUG = os.getenv('LOG_LEVEL') == 'DEBUG' DEBUG = 'AWS_SAM_LOCAL' in os.environ
tracer = Tracer() tracer = Tracer()
logger = Logger(__name__) logger = Logger(__name__)
app = APIGatewayHttpResolver(enable_validation=True, debug=DEBUG) cors = CORSConfig(
allow_origin='*',
allow_headers=['Content-Type', 'X-Requested-With', 'Authorization', 'X-Tenant'],
max_age=600,
allow_credentials=False,
)
app = APIGatewayHttpResolver(enable_validation=True, cors=cors, debug=DEBUG)
app.use(middlewares=[AuthorizerMiddleware(), TenantMiddleware()]) app.use(middlewares=[AuthorizerMiddleware(), TenantMiddleware()])
app.include_router(courses.router, prefix='/courses') app.include_router(courses.router, prefix='/courses')
app.include_router(enrollments.router, prefix='/enrollments') app.include_router(enrollments.router, prefix='/enrollments')
app.include_router(orders.router, prefix='/orders') app.include_router(orders.router, prefix='/orders')
app.include_router(users.router, prefix='/users') app.include_router(users.router, prefix='/users')
app.include_router(webhooks.router, prefix='/webhooks') app.include_router(webhooks.router, prefix='/webhooks')
app.include_router(me.router, prefix='/me') app.include_router(settings.router, prefix='/settings')
app.include_router(lookup.router, prefix='/lookup') app.include_router(lookup.router, prefix='/lookup')

View File

@@ -48,19 +48,21 @@ tracer = Tracer()
logger = Logger(__name__) logger = Logger(__name__)
idp_client = boto3.client('cognito-idp') idp_client = boto3.client('cognito-idp')
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
collect = DynamoDBCollection(user_layer) user_collect = DynamoDBCollection(user_layer)
@tracer.capture_lambda_handler @tracer.capture_lambda_handler
@logger.inject_lambda_context @logger.inject_lambda_context
@event_source(data_class=APIGatewayAuthorizerEventV2) @event_source(data_class=APIGatewayAuthorizerEventV2)
def lambda_handler(event: APIGatewayAuthorizerEventV2, context: LambdaContext) -> dict: def lambda_handler(event: APIGatewayAuthorizerEventV2, context: LambdaContext) -> dict:
"""Authenticates a user using a bearer token (for user or API).
Only handles authentication; any additional logic (e.g., tenant) is performed afterward."""
bearer = _parse_bearer_token(event.headers.get('authorization', '')) bearer = _parse_bearer_token(event.headers.get('authorization', ''))
if not bearer: if not bearer:
return APIGatewayAuthorizerResponseV2(authorize=False).asdict() return APIGatewayAuthorizerResponseV2(authorize=False).asdict()
attrs = _authorizer(bearer).asdict() attrs = _authorizer(bearer, user_collect).asdict()
return APIGatewayAuthorizerResponseV2(**attrs).asdict() return APIGatewayAuthorizerResponseV2(**attrs).asdict()
@@ -76,7 +78,7 @@ class BearerToken:
@dataclass @dataclass
class Authorizer: class AuthorizerResponseV2:
authorize: bool = False authorize: bool = False
context: dict[str, Any] | None = None context: dict[str, Any] | None = None
auth_flow_type: AuthFlowType = AuthFlowType.USER_AUTH auth_flow_type: AuthFlowType = AuthFlowType.USER_AUTH
@@ -92,11 +94,15 @@ class Authorizer:
return data return data
def _get_apikey(token: str) -> dict[str, dict | str]: def _get_apikey(token: str, /, collect: DynamoDBCollection) -> dict[str, dict | str]:
return collect.get_item(KeyPair('apikey', token)) return collect.get_item(KeyPair('apikey', token))
def _authorizer(bearer: BearerToken) -> Authorizer: def _authorizer(
bearer: BearerToken,
/,
collect: DynamoDBCollection,
) -> AuthorizerResponseV2:
""" """
Build an Authorizer object based on the bearer token's auth type. Build an Authorizer object based on the bearer token's auth type.
@@ -113,13 +119,13 @@ def _authorizer(bearer: BearerToken) -> Authorizer:
try: try:
if bearer.auth_flow_type == AuthFlowType.USER_AUTH: if bearer.auth_flow_type == AuthFlowType.USER_AUTH:
user = get_user(bearer.token, idp_client) user = get_user(bearer.token, idp_client)
return Authorizer(True, {'user': user}) return AuthorizerResponseV2(True, {'user': user})
apikey = _get_apikey(bearer.token) apikey = _get_apikey(bearer.token, collect)
context = pick(('tenant', 'user'), apikey) context = pick(('tenant', 'user'), apikey)
return Authorizer(True, context, AuthFlowType.API_AUTH) return AuthorizerResponseV2(True, context, AuthFlowType.API_AUTH)
except Exception: except Exception:
return Authorizer() return AuthorizerResponseV2()
def _parse_bearer_token(s: str) -> BearerToken | None: def _parse_bearer_token(s: str) -> BearerToken | None:

View File

@@ -9,6 +9,15 @@ services:
working_dir: /home/dynamodblocal working_dir: /home/dynamodblocal
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
meilisearch:
container_name: meilisearch
image: getmeili/meilisearch:v1.13
volumes:
- ./meili_data:/meili_data
restart: unless-stopped
ports:
- 7700:7700
elastic: elastic:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.3 image: docker.elastic.co/elasticsearch/elasticsearch:8.11.3
container_name: elastic container_name: elastic

View File

@@ -34,6 +34,7 @@ def token(username: str) -> KonvivaToken:
r = requests.get(url.geturl(), headers=headers) r = requests.get(url.geturl(), headers=headers)
r.raise_for_status() r.raise_for_status()
# Because Konviva does not return the proper HTTP status code
if err := glom(r.json(), 'errors.0', default=None): if err := glom(r.json(), 'errors.0', default=None):
raise KonvivaError(err) raise KonvivaError(err)

View File

@@ -63,7 +63,15 @@ class TenantMiddleware(BaseMiddlewareHandler):
next_middleware: NextMiddleware, next_middleware: NextMiddleware,
) -> Response: ) -> Response:
context = app.current_event.request_context.authorizer.get_lambda context = app.current_event.request_context.authorizer.get_lambda
tenant = app.current_event.headers.get('x-tenant')
auth_flow_type = context.get('auth_flow_type') auth_flow_type = context.get('auth_flow_type')
match auth_flow_type, tenant:
case AuthFlowType.API_AUTH, None:
app.append_context(tenant=context['tenant'])
case AuthFlowType.USER_AUTH, str():
print(tenant)
return next_middleware(app) return next_middleware(app)
@@ -108,7 +116,8 @@ class AuditLogMiddleware(BaseMiddlewareHandler):
ip_addr = req_context.http.source_ip ip_addr = req_context.http.source_ip
response = next_middleware(app) response = next_middleware(app)
# Successful request # Successful response
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status#successful_responses
if 200 <= response.status_code < 300 and user: if 200 <= response.status_code < 300 and user:
now_ = now() now_ = now()
data = ( data = (
@@ -124,7 +133,8 @@ class AuditLogMiddleware(BaseMiddlewareHandler):
self.collect.put_item( self.collect.put_item(
key=KeyPair( key=KeyPair(
pk=ComposeKey(user.id, prefix='logs'), # Post-migration: remove `delimiter` from ComposeKey.
pk=ComposeKey(user.id, prefix='logs', delimiter=':'),
sk=now_.isoformat(), sk=now_.isoformat(),
), ),
action=self.action, action=self.action,

View File

@@ -13,27 +13,28 @@ from settings import USER_TABLE
router = Router() router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
collect = DynamoDBCollection(user_layer) user_collect = DynamoDBCollection(user_layer)
LIMIT = 25 LIMIT = 25
@router.get('/', include_in_schema=False) @router.get('/', include_in_schema=False)
def me(): def settings():
user: User = router.context['user'] user: User = router.context['user']
acls = collect.get_items( acls = user_collect.get_items(
KeyPair(user.id, PrefixKey('acls')), KeyPair(user.id, PrefixKey('acls')),
limit=LIMIT, limit=LIMIT,
) )
workspaces = collect.get_items( tenants = user_collect.get_items(
KeyPair(user.id, PrefixKey('orgs')), KeyPair(user.id, PrefixKey('orgs')),
limit=LIMIT, limit=LIMIT,
) )
return { return {
'acls': acls['items'], 'acls': acls['items'],
'workspaces': workspaces['items'], # Note: ensure compatibility with search on React's tenant menu
'tenants': [x | {'id': x['sk']} for x in tenants['items']],
} }

View File

@@ -33,7 +33,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError): ...
router = Router() router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError) user_collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
elastic_client = Elasticsearch(**ELASTIC_CONN) elastic_client = Elasticsearch(**ELASTIC_CONN)
@@ -61,7 +61,7 @@ def get_users():
compress=True, compress=True,
tags=['User'], tags=['User'],
summary='Create user', summary='Create user',
middlewares=[AuditLogMiddleware('USER_ADD', collect)], middlewares=[AuditLogMiddleware('USER_ADD', user_collect)],
) )
def post_user(payload: User): def post_user(payload: User):
return Response(status_code=HTTPStatus.CREATED) return Response(status_code=HTTPStatus.CREATED)
@@ -84,7 +84,7 @@ def patch_reset(id: str, payload: NewPasswordPayload):
summary='Get user', summary='Get user',
) )
def get_user(id: str): def get_user(id: str):
return collect.get_item(KeyPair(id, '0')) return user_collect.get_item(KeyPair(id, '0'))
@router.get('/<id>/idp', compress=True, include_in_schema=False) @router.get('/<id>/idp', compress=True, include_in_schema=False)
@@ -99,7 +99,7 @@ def get_idp(id: str):
summary='Get user emails', summary='Get user emails',
) )
def get_emails(id: str): def get_emails(id: str):
return collect.get_items( return user_collect.get_items(
KeyPair(id, PrefixKey('emails')), KeyPair(id, PrefixKey('emails')),
start_key=router.current_event.get_query_string_value('start_key', None), start_key=router.current_event.get_query_string_value('start_key', None),
) )
@@ -112,7 +112,7 @@ def get_emails(id: str):
summary='Get user logs', summary='Get user logs',
) )
def get_logs(id: str): def get_logs(id: str):
return collect.get_items( return user_collect.get_items(
# Post-migration: uncomment to enable PartitionKey with a composite key (id with `logs` prefix). # Post-migration: uncomment to enable PartitionKey with a composite key (id with `logs` prefix).
# PartitionKey(ComposeKey(id, prefix='logs')), # PartitionKey(ComposeKey(id, prefix='logs')),
PartitionKey(ComposeKey(id, prefix='log', delimiter=':')), PartitionKey(ComposeKey(id, prefix='log', delimiter=':')),
@@ -127,7 +127,7 @@ def get_logs(id: str):
summary='Get user orgs', summary='Get user orgs',
) )
def get_orgs(id: str): def get_orgs(id: str):
return collect.get_items( return user_collect.get_items(
KeyPair(id, PrefixKey('orgs')), KeyPair(id, PrefixKey('orgs')),
start_key=router.current_event.get_query_string_value('start_key', None), start_key=router.current_event.get_query_string_value('start_key', None),
) )

View File

@@ -23,7 +23,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:26 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:28
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo
@@ -39,6 +39,8 @@ Globals:
ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}" ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}"
KONVIVA_API_URL: https://saladeaula.digital KONVIVA_API_URL: https://saladeaula.digital
KONVIVA_SECRET_KEY: "{{resolve:ssm:/betaeducacao/konviva/secret_key/str}}" KONVIVA_SECRET_KEY: "{{resolve:ssm:/betaeducacao/konviva/secret_key/str}}"
MEILISEARCH_HOST: https://meili.vps.eduseg.com.br
MEILISEARCH_API_KEY: "{{resolve:ssm:/saladeaula/meili_api_key}}"
Resources: Resources:
HttpLog: HttpLog:
@@ -53,6 +55,8 @@ Resources:
AllowOrigins: ["*"] AllowOrigins: ["*"]
AllowMethods: [GET, POST, PUT, DELETE, PATCH, OPTIONS] AllowMethods: [GET, POST, PUT, DELETE, PATCH, OPTIONS]
AllowHeaders: [Content-Type, X-Requested-With, Authorization, X-Tenant] AllowHeaders: [Content-Type, X-Requested-With, Authorization, X-Tenant]
AllowCredentials: false
MaxAge: 600
Auth: Auth:
DefaultAuthorizer: LambdaRequestAuthorizer DefaultAuthorizer: LambdaRequestAuthorizer
Authorizers: Authorizers:
@@ -63,6 +67,7 @@ Resources:
EnableSimpleResponses: true EnableSimpleResponses: true
Identity: Identity:
Headers: [Authorization] Headers: [Authorization]
ReauthorizeEvery: 300
HttpApiFunction: HttpApiFunction:
Type: AWS::Serverless::Function Type: AWS::Serverless::Function
@@ -76,6 +81,14 @@ Resources:
- DynamoDBCrudPolicy: - DynamoDBCrudPolicy:
TableName: !Ref CourseTable TableName: !Ref CourseTable
Events: Events:
Preflight:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: OPTIONS
ApiId: !Ref HttpApi
Auth:
Authorizer: NONE
AnyRequest: AnyRequest:
Type: HttpApi Type: HttpApi
Properties: Properties:

View File

@@ -34,6 +34,7 @@ class HttpApiProxy:
body: dict = {}, body: dict = {},
*, *,
headers: dict = {}, headers: dict = {},
auth_flow_type: str = 'USER_AUTH',
**kwargs, **kwargs,
) -> dict: ) -> dict:
return { return {
@@ -59,7 +60,7 @@ class HttpApiProxy:
'custom:user_id': '5OxmMjL-ujoR5IMGegQz', 'custom:user_id': '5OxmMjL-ujoR5IMGegQz',
'sub': 'c4f30dbd-083e-4b84-aa50-c31afe9b9c01', 'sub': 'c4f30dbd-083e-4b84-aa50-c31afe9b9c01',
}, },
'auth_flow_type': 'USER_AUTH', 'auth_flow_type': auth_flow_type,
}, },
'jwt': { 'jwt': {
'claims': {'claim1': 'value1', 'claim2': 'value2'}, 'claims': {'claim1': 'value1', 'claim2': 'value2'},

View File

@@ -4,7 +4,7 @@ from http import HTTPMethod, HTTPStatus
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_me( def test_settings(
mock_app, mock_app,
dynamodb_seeds, dynamodb_seeds,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
@@ -12,10 +12,7 @@ def test_me(
): ):
# This data was added from seeds # This data was added from seeds
r = mock_app.lambda_handler( r = mock_app.lambda_handler(
http_api_proxy( http_api_proxy(raw_path='/settings', method=HTTPMethod.GET),
raw_path='/me',
method=HTTPMethod.GET,
),
lambda_context, lambda_context,
) )
@@ -37,7 +34,7 @@ def test_me(
'roles': ['ADMIN'], 'roles': ['ADMIN'],
}, },
], ],
'workspaces': [ 'tenants': [
{ {
'sk': 'cJtK9SsnJhKPyxESe7g3DG', 'sk': 'cJtK9SsnJhKPyxESe7g3DG',
'name': 'Beta Educação', 'name': 'Beta Educação',

View File

@@ -5,8 +5,6 @@ from .conftest import LambdaContext
def test_bearer_jwt(lambda_context: LambdaContext): def test_bearer_jwt(lambda_context: LambdaContext):
import auth as app
# You should mock the Cognito user to pass the test # You should mock the Cognito user to pass the test
app.get_user = lambda *args, **kwargs: { app.get_user = lambda *args, **kwargs: {
'sub': '58efed8d-d276-41a8-8502-4ab8b5a6415e', 'sub': '58efed8d-d276-41a8-8502-4ab8b5a6415e',
@@ -32,11 +30,7 @@ def test_bearer_jwt(lambda_context: LambdaContext):
} }
def test_bearer_apikey( def test_bearer_apikey(dynamodb_seeds, lambda_context: LambdaContext):
monkeypatch,
dynamodb_seeds,
lambda_context: LambdaContext,
):
event = { event = {
'headers': { 'headers': {
'authorization': 'Bearer sk-MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw', 'authorization': 'Bearer sk-MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw',

View File

@@ -0,0 +1,31 @@
from http import HTTPMethod
from aws_lambda_powertools.event_handler.api_gateway import APIGatewayHttpResolver
from middlewares import TenantMiddleware
from .conftest import HttpApiProxy, LambdaContext
def test_eval(
dynamodb_seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
app = APIGatewayHttpResolver()
app.use(middlewares=[TenantMiddleware()])
@app.get('/')
def index():
return {}
result = app(
http_api_proxy(
raw_path='/',
method=HTTPMethod.GET,
headers={'Tenant': 'cJtK9SsnJhKPyxESe7g3DG'},
),
lambda_context,
)
assert result['statusCode'] == 200

42
http-api/uv.lock generated
View File

@@ -10,6 +10,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
] ]
[[package]]
name = "arnparse"
version = "0.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/42/949284e998282b167e273872fa9c39b06d41a6055163c30aa2daaeee76a0/arnparse-0.0.2.tar.gz", hash = "sha256:cb87f17200d07121108a9085d4a09cc69a55582647776b9a917b0b1f279db8f8", size = 2677 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/6f/630bedeb32964e99661990811a66389201b62c047b35c17e332dad9be2a3/arnparse-0.0.2-py2.py3-none-any.whl", hash = "sha256:b0906734e4b8f19e39b1e32944c6cd6274b6da90c066a83882ac7a11d27553e0", size = 2904 },
]
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "25.3.0" version = "25.3.0"
@@ -107,6 +116,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/5b/f96cf58c37704b907ac2f9cc94e45ba0a2aa3b2062421aa8b8614f1d78de/botocore-1.37.20-py3-none-any.whl", hash = "sha256:c34f4f25fda7c4f726adf5a948590bd6bd7892c05278d31e344b5908e7b43301", size = 13432464 }, { url = "https://files.pythonhosted.org/packages/2d/5b/f96cf58c37704b907ac2f9cc94e45ba0a2aa3b2062421aa8b8614f1d78de/botocore-1.37.20-py3-none-any.whl", hash = "sha256:c34f4f25fda7c4f726adf5a948590bd6bd7892c05278d31e344b5908e7b43301", size = 13432464 },
] ]
[[package]]
name = "camel-converter"
version = "4.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/3d/dd783586dc0c4aee5b6b88489666fdb2c0c344ea0aa8a5c10746cc423707/camel_converter-4.0.1.tar.gz", hash = "sha256:401414549ae4ac4073e38cdc4aa6d464dc534fc40aa06ff787bf0960b0c86535", size = 38915 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/e5/806359514cc8305f047bd6d39d63890298c0596f7328b534059724bd1a9e/camel_converter-4.0.1-py3-none-any.whl", hash = "sha256:0cba7ca1354a29ca2191983deecc9dcf28889f606c28d6ed18ac7d4586b163ac", size = 6243 },
]
[package.optional-dependencies]
pydantic = [
{ name = "pydantic" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.1.31" version = "2025.1.31"
@@ -444,15 +467,17 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.1.11" version = "0.1.13"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" },
{ name = "aws-lambda-powertools", extra = ["all"] }, { name = "aws-lambda-powertools", extra = ["all"] },
{ name = "boto3" }, { name = "boto3" },
{ name = "elasticsearch" }, { name = "elasticsearch" },
{ name = "elasticsearch-dsl" }, { name = "elasticsearch-dsl" },
{ name = "ftfy" }, { name = "ftfy" },
{ name = "glom" }, { name = "glom" },
{ name = "meilisearch" },
{ name = "orjson" }, { name = "orjson" },
{ name = "pycpfcnpj" }, { name = "pycpfcnpj" },
{ name = "pydantic", extra = ["email"] }, { name = "pydantic", extra = ["email"] },
@@ -464,12 +489,14 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.8.0" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.8.0" },
{ name = "boto3", specifier = ">=1.37.16" }, { name = "boto3", specifier = ">=1.37.16" },
{ name = "elasticsearch", specifier = ">=8.17.2" }, { name = "elasticsearch", specifier = ">=8.17.2" },
{ name = "elasticsearch-dsl", specifier = ">=8.17.1" }, { name = "elasticsearch-dsl", specifier = ">=8.17.1" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
{ name = "meilisearch", specifier = ">=0.34.0" },
{ name = "orjson", specifier = ">=3.10.15" }, { name = "orjson", specifier = ">=3.10.15" },
{ 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" },
@@ -488,6 +515,19 @@ dev = [
{ name = "ruff", specifier = ">=0.11.1" }, { name = "ruff", specifier = ">=0.11.1" },
] ]
[[package]]
name = "meilisearch"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "camel-converter", extra = ["pydantic"] },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/42/b6a62f355057521c0d9df44a402205e3037299fdcb9cee4dfa22eebd22f0/meilisearch-0.34.0.tar.gz", hash = "sha256:6244af23fa118f5a127ebf3f1297ea8d1d73324bf189b13d61cc201e18cd9e90", size = 23623 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/2f/264c07a3f488260ea36c78cbc201b76e6baf9ef92e0c7f78657a6a5e5f22/meilisearch-0.34.0-py3-none-any.whl", hash = "sha256:fae8ad2a15d12c27fa0a1fff2ae2e4e3e2e22b869950408d63c87e2c095a9f61", size = 24373 },
]
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.10.16" version = "3.10.16"

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "layercake" name = "layercake"
version = "0.1.11" version = "0.1.13"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
authors = [ authors = [
@@ -21,6 +21,8 @@ dependencies = [
"pytz>=2025.1", "pytz>=2025.1",
"shortuuid>=1.0.13", "shortuuid>=1.0.13",
"requests>=2.32.3", "requests>=2.32.3",
"meilisearch>=0.34.0",
"arnparse>=0.0.2",
] ]
[dependency-groups] [dependency-groups]

42
layercake/uv.lock generated
View File

@@ -10,6 +10,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
] ]
[[package]]
name = "arnparse"
version = "0.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/42/949284e998282b167e273872fa9c39b06d41a6055163c30aa2daaeee76a0/arnparse-0.0.2.tar.gz", hash = "sha256:cb87f17200d07121108a9085d4a09cc69a55582647776b9a917b0b1f279db8f8", size = 2677 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/6f/630bedeb32964e99661990811a66389201b62c047b35c17e332dad9be2a3/arnparse-0.0.2-py2.py3-none-any.whl", hash = "sha256:b0906734e4b8f19e39b1e32944c6cd6274b6da90c066a83882ac7a11d27553e0", size = 2904 },
]
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "25.3.0" version = "25.3.0"
@@ -107,6 +116,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/e9/98d3f5135cce841b54a8952f244db906511acba624252559e21188f84e90/botocore-1.37.16-py3-none-any.whl", hash = "sha256:d74d04830ead12933a96dc407175ae98b32a5dd0059d7d2b28fc7aa4ed9d3b48", size = 13422674 }, { url = "https://files.pythonhosted.org/packages/d2/e9/98d3f5135cce841b54a8952f244db906511acba624252559e21188f84e90/botocore-1.37.16-py3-none-any.whl", hash = "sha256:d74d04830ead12933a96dc407175ae98b32a5dd0059d7d2b28fc7aa4ed9d3b48", size = 13422674 },
] ]
[[package]]
name = "camel-converter"
version = "4.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/3d/dd783586dc0c4aee5b6b88489666fdb2c0c344ea0aa8a5c10746cc423707/camel_converter-4.0.1.tar.gz", hash = "sha256:401414549ae4ac4073e38cdc4aa6d464dc534fc40aa06ff787bf0960b0c86535", size = 38915 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/e5/806359514cc8305f047bd6d39d63890298c0596f7328b534059724bd1a9e/camel_converter-4.0.1-py3-none-any.whl", hash = "sha256:0cba7ca1354a29ca2191983deecc9dcf28889f606c28d6ed18ac7d4586b163ac", size = 6243 },
]
[package.optional-dependencies]
pydantic = [
{ name = "pydantic" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.1.31" version = "2025.1.31"
@@ -465,15 +488,17 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.1.10" version = "0.1.12"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "arnparse" },
{ name = "aws-lambda-powertools", extra = ["all"] }, { name = "aws-lambda-powertools", extra = ["all"] },
{ name = "boto3" }, { name = "boto3" },
{ name = "elasticsearch" }, { name = "elasticsearch" },
{ name = "elasticsearch-dsl" }, { name = "elasticsearch-dsl" },
{ name = "ftfy" }, { name = "ftfy" },
{ name = "glom" }, { name = "glom" },
{ name = "meilisearch" },
{ name = "orjson" }, { name = "orjson" },
{ name = "pycpfcnpj" }, { name = "pycpfcnpj" },
{ name = "pydantic", extra = ["email"] }, { name = "pydantic", extra = ["email"] },
@@ -494,12 +519,14 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.8.0" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.8.0" },
{ name = "boto3", specifier = ">=1.37.16" }, { name = "boto3", specifier = ">=1.37.16" },
{ name = "elasticsearch", specifier = ">=8.17.2" }, { name = "elasticsearch", specifier = ">=8.17.2" },
{ name = "elasticsearch-dsl", specifier = ">=8.17.1" }, { name = "elasticsearch-dsl", specifier = ">=8.17.1" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
{ name = "meilisearch", specifier = ">=0.34.0" },
{ name = "orjson", specifier = ">=3.10.15" }, { name = "orjson", specifier = ">=3.10.15" },
{ 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" },
@@ -565,6 +592,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
] ]
[[package]]
name = "meilisearch"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "camel-converter", extra = ["pydantic"] },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/42/b6a62f355057521c0d9df44a402205e3037299fdcb9cee4dfa22eebd22f0/meilisearch-0.34.0.tar.gz", hash = "sha256:6244af23fa118f5a127ebf3f1297ea8d1d73324bf189b13d61cc201e18cd9e90", size = 23623 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/2f/264c07a3f488260ea36c78cbc201b76e6baf9ef92e0c7f78657a6a5e5f22/meilisearch-0.34.0-py3-none-any.whl", hash = "sha256:fae8ad2a15d12c27fa0a1fff2ae2e4e3e2e22b869950408d63c87e2c095a9f61", size = 24373 },
]
[[package]] [[package]]
name = "mergedeep" name = "mergedeep"
version = "1.3.4" version = "1.3.4"