From ef4bfc07f3ba81d5ec4008b6024b508aa4a6b56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Tue, 1 Apr 2025 19:15:10 -0300 Subject: [PATCH] wip --- http-api/.gitignore | 1 + http-api/app.py | 15 +++++-- http-api/auth.py | 24 +++++++---- http-api/compose.yaml | 9 ++++ http-api/konviva.py | 1 + http-api/middlewares.py | 14 ++++++- http-api/routes/{me => settings}/__init__.py | 11 ++--- http-api/routes/users/__init__.py | 12 +++--- http-api/template.yaml | 15 ++++++- http-api/tests/conftest.py | 3 +- .../routes/{test_me.py => test_settings.py} | 9 ++-- http-api/tests/test_auth.py | 8 +--- http-api/tests/test_middelwares.py | 31 ++++++++++++++ http-api/uv.lock | 42 ++++++++++++++++++- layercake/pyproject.toml | 4 +- layercake/uv.lock | 42 ++++++++++++++++++- 16 files changed, 197 insertions(+), 44 deletions(-) rename http-api/routes/{me => settings}/__init__.py (76%) rename http-api/tests/routes/{test_me.py => test_settings.py} (89%) create mode 100644 http-api/tests/test_middelwares.py diff --git a/http-api/.gitignore b/http-api/.gitignore index 74d68c9..90371f8 100644 --- a/http-api/.gitignore +++ b/http-api/.gitignore @@ -2,3 +2,4 @@ env.json dynamodb_volume/ elastic_volume/ +meili_data/ diff --git a/http-api/app.py b/http-api/app.py index edf8ba0..2be5868 100644 --- a/http-api/app.py +++ b/http-api/app.py @@ -4,6 +4,7 @@ from typing import Any from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler.api_gateway import ( APIGatewayHttpResolver, + CORSConfig, Response, content_types, ) @@ -12,20 +13,26 @@ from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext 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() 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.include_router(courses.router, prefix='/courses') app.include_router(enrollments.router, prefix='/enrollments') app.include_router(orders.router, prefix='/orders') app.include_router(users.router, prefix='/users') 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') diff --git a/http-api/auth.py b/http-api/auth.py index 4a6c79f..637136b 100644 --- a/http-api/auth.py +++ b/http-api/auth.py @@ -48,19 +48,21 @@ tracer = Tracer() logger = Logger(__name__) idp_client = boto3.client('cognito-idp') user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -collect = DynamoDBCollection(user_layer) +user_collect = DynamoDBCollection(user_layer) @tracer.capture_lambda_handler @logger.inject_lambda_context @event_source(data_class=APIGatewayAuthorizerEventV2) 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', '')) if not bearer: return APIGatewayAuthorizerResponseV2(authorize=False).asdict() - attrs = _authorizer(bearer).asdict() + attrs = _authorizer(bearer, user_collect).asdict() return APIGatewayAuthorizerResponseV2(**attrs).asdict() @@ -76,7 +78,7 @@ class BearerToken: @dataclass -class Authorizer: +class AuthorizerResponseV2: authorize: bool = False context: dict[str, Any] | None = None auth_flow_type: AuthFlowType = AuthFlowType.USER_AUTH @@ -92,11 +94,15 @@ class Authorizer: 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)) -def _authorizer(bearer: BearerToken) -> Authorizer: +def _authorizer( + bearer: BearerToken, + /, + collect: DynamoDBCollection, +) -> AuthorizerResponseV2: """ Build an Authorizer object based on the bearer token's auth type. @@ -113,13 +119,13 @@ def _authorizer(bearer: BearerToken) -> Authorizer: try: if bearer.auth_flow_type == AuthFlowType.USER_AUTH: 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) - return Authorizer(True, context, AuthFlowType.API_AUTH) + return AuthorizerResponseV2(True, context, AuthFlowType.API_AUTH) except Exception: - return Authorizer() + return AuthorizerResponseV2() def _parse_bearer_token(s: str) -> BearerToken | None: diff --git a/http-api/compose.yaml b/http-api/compose.yaml index 909a514..fc24509 100644 --- a/http-api/compose.yaml +++ b/http-api/compose.yaml @@ -9,6 +9,15 @@ services: working_dir: /home/dynamodblocal 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: image: docker.elastic.co/elasticsearch/elasticsearch:8.11.3 container_name: elastic diff --git a/http-api/konviva.py b/http-api/konviva.py index d3dbeec..25f857d 100644 --- a/http-api/konviva.py +++ b/http-api/konviva.py @@ -34,6 +34,7 @@ def token(username: str) -> KonvivaToken: r = requests.get(url.geturl(), headers=headers) r.raise_for_status() + # Because Konviva does not return the proper HTTP status code if err := glom(r.json(), 'errors.0', default=None): raise KonvivaError(err) diff --git a/http-api/middlewares.py b/http-api/middlewares.py index f69782b..ffb28ef 100644 --- a/http-api/middlewares.py +++ b/http-api/middlewares.py @@ -63,7 +63,15 @@ class TenantMiddleware(BaseMiddlewareHandler): next_middleware: NextMiddleware, ) -> Response: 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') + + 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) @@ -108,7 +116,8 @@ class AuditLogMiddleware(BaseMiddlewareHandler): ip_addr = req_context.http.source_ip 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: now_ = now() data = ( @@ -124,7 +133,8 @@ class AuditLogMiddleware(BaseMiddlewareHandler): self.collect.put_item( key=KeyPair( - pk=ComposeKey(user.id, prefix='logs'), + # Post-migration: remove `delimiter` from ComposeKey. + pk=ComposeKey(user.id, prefix='logs', delimiter=':'), sk=now_.isoformat(), ), action=self.action, diff --git a/http-api/routes/me/__init__.py b/http-api/routes/settings/__init__.py similarity index 76% rename from http-api/routes/me/__init__.py rename to http-api/routes/settings/__init__.py index 856d825..6044822 100644 --- a/http-api/routes/me/__init__.py +++ b/http-api/routes/settings/__init__.py @@ -13,27 +13,28 @@ from settings import USER_TABLE router = Router() user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -collect = DynamoDBCollection(user_layer) +user_collect = DynamoDBCollection(user_layer) LIMIT = 25 @router.get('/', include_in_schema=False) -def me(): +def settings(): user: User = router.context['user'] - acls = collect.get_items( + acls = user_collect.get_items( KeyPair(user.id, PrefixKey('acls')), limit=LIMIT, ) - workspaces = collect.get_items( + tenants = user_collect.get_items( KeyPair(user.id, PrefixKey('orgs')), limit=LIMIT, ) return { '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']], } diff --git a/http-api/routes/users/__init__.py b/http-api/routes/users/__init__.py index 7a994ee..c98361c 100644 --- a/http-api/routes/users/__init__.py +++ b/http-api/routes/users/__init__.py @@ -33,7 +33,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError): ... router = Router() 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) @@ -61,7 +61,7 @@ def get_users(): compress=True, tags=['User'], summary='Create user', - middlewares=[AuditLogMiddleware('USER_ADD', collect)], + middlewares=[AuditLogMiddleware('USER_ADD', user_collect)], ) def post_user(payload: User): return Response(status_code=HTTPStatus.CREATED) @@ -84,7 +84,7 @@ def patch_reset(id: str, payload: NewPasswordPayload): summary='Get user', ) def get_user(id: str): - return collect.get_item(KeyPair(id, '0')) + return user_collect.get_item(KeyPair(id, '0')) @router.get('//idp', compress=True, include_in_schema=False) @@ -99,7 +99,7 @@ def get_idp(id: str): summary='Get user emails', ) def get_emails(id: str): - return collect.get_items( + return user_collect.get_items( KeyPair(id, PrefixKey('emails')), 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', ) 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). # PartitionKey(ComposeKey(id, prefix='logs')), PartitionKey(ComposeKey(id, prefix='log', delimiter=':')), @@ -127,7 +127,7 @@ def get_logs(id: str): summary='Get user orgs', ) def get_orgs(id: str): - return collect.get_items( + return user_collect.get_items( KeyPair(id, PrefixKey('orgs')), start_key=router.current_event.get_query_string_value('start_key', None), ) diff --git a/http-api/template.yaml b/http-api/template.yaml index 1e70c0b..a506ce2 100644 --- a/http-api/template.yaml +++ b/http-api/template.yaml @@ -23,7 +23,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:26 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:28 Environment: Variables: TZ: America/Sao_Paulo @@ -39,6 +39,8 @@ Globals: ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}" KONVIVA_API_URL: https://saladeaula.digital 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: HttpLog: @@ -53,6 +55,8 @@ Resources: AllowOrigins: ["*"] AllowMethods: [GET, POST, PUT, DELETE, PATCH, OPTIONS] AllowHeaders: [Content-Type, X-Requested-With, Authorization, X-Tenant] + AllowCredentials: false + MaxAge: 600 Auth: DefaultAuthorizer: LambdaRequestAuthorizer Authorizers: @@ -63,6 +67,7 @@ Resources: EnableSimpleResponses: true Identity: Headers: [Authorization] + ReauthorizeEvery: 300 HttpApiFunction: Type: AWS::Serverless::Function @@ -76,6 +81,14 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref CourseTable Events: + Preflight: + Type: HttpApi + Properties: + Path: /{proxy+} + Method: OPTIONS + ApiId: !Ref HttpApi + Auth: + Authorizer: NONE AnyRequest: Type: HttpApi Properties: diff --git a/http-api/tests/conftest.py b/http-api/tests/conftest.py index f169c84..d4651d3 100644 --- a/http-api/tests/conftest.py +++ b/http-api/tests/conftest.py @@ -34,6 +34,7 @@ class HttpApiProxy: body: dict = {}, *, headers: dict = {}, + auth_flow_type: str = 'USER_AUTH', **kwargs, ) -> dict: return { @@ -59,7 +60,7 @@ class HttpApiProxy: 'custom:user_id': '5OxmMjL-ujoR5IMGegQz', 'sub': 'c4f30dbd-083e-4b84-aa50-c31afe9b9c01', }, - 'auth_flow_type': 'USER_AUTH', + 'auth_flow_type': auth_flow_type, }, 'jwt': { 'claims': {'claim1': 'value1', 'claim2': 'value2'}, diff --git a/http-api/tests/routes/test_me.py b/http-api/tests/routes/test_settings.py similarity index 89% rename from http-api/tests/routes/test_me.py rename to http-api/tests/routes/test_settings.py index 873e1ed..8497ee7 100644 --- a/http-api/tests/routes/test_me.py +++ b/http-api/tests/routes/test_settings.py @@ -4,7 +4,7 @@ from http import HTTPMethod, HTTPStatus from ..conftest import HttpApiProxy, LambdaContext -def test_me( +def test_settings( mock_app, dynamodb_seeds, http_api_proxy: HttpApiProxy, @@ -12,10 +12,7 @@ def test_me( ): # This data was added from seeds r = mock_app.lambda_handler( - http_api_proxy( - raw_path='/me', - method=HTTPMethod.GET, - ), + http_api_proxy(raw_path='/settings', method=HTTPMethod.GET), lambda_context, ) @@ -37,7 +34,7 @@ def test_me( 'roles': ['ADMIN'], }, ], - 'workspaces': [ + 'tenants': [ { 'sk': 'cJtK9SsnJhKPyxESe7g3DG', 'name': 'Beta Educação', diff --git a/http-api/tests/test_auth.py b/http-api/tests/test_auth.py index ed427b8..d1d95da 100644 --- a/http-api/tests/test_auth.py +++ b/http-api/tests/test_auth.py @@ -5,8 +5,6 @@ from .conftest import LambdaContext def test_bearer_jwt(lambda_context: LambdaContext): - import auth as app - # You should mock the Cognito user to pass the test app.get_user = lambda *args, **kwargs: { 'sub': '58efed8d-d276-41a8-8502-4ab8b5a6415e', @@ -32,11 +30,7 @@ def test_bearer_jwt(lambda_context: LambdaContext): } -def test_bearer_apikey( - monkeypatch, - dynamodb_seeds, - lambda_context: LambdaContext, -): +def test_bearer_apikey(dynamodb_seeds, lambda_context: LambdaContext): event = { 'headers': { 'authorization': 'Bearer sk-MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw', diff --git a/http-api/tests/test_middelwares.py b/http-api/tests/test_middelwares.py new file mode 100644 index 0000000..c51bc83 --- /dev/null +++ b/http-api/tests/test_middelwares.py @@ -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 diff --git a/http-api/uv.lock b/http-api/uv.lock index f3c1045..5216adc 100644 --- a/http-api/uv.lock +++ b/http-api/uv.lock @@ -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 }, ] +[[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]] name = "attrs" 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 }, ] +[[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]] name = "certifi" version = "2025.1.31" @@ -444,15 +467,17 @@ wheels = [ [[package]] name = "layercake" -version = "0.1.11" +version = "0.1.13" source = { directory = "../layercake" } dependencies = [ + { name = "arnparse" }, { name = "aws-lambda-powertools", extra = ["all"] }, { name = "boto3" }, { name = "elasticsearch" }, { name = "elasticsearch-dsl" }, { name = "ftfy" }, { name = "glom" }, + { name = "meilisearch" }, { name = "orjson" }, { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, @@ -464,12 +489,14 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "arnparse", specifier = ">=0.0.2" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.8.0" }, { name = "boto3", specifier = ">=1.37.16" }, { name = "elasticsearch", specifier = ">=8.17.2" }, { name = "elasticsearch-dsl", specifier = ">=8.17.1" }, { name = "ftfy", specifier = ">=6.3.1" }, { name = "glom", specifier = ">=24.11.0" }, + { name = "meilisearch", specifier = ">=0.34.0" }, { name = "orjson", specifier = ">=3.10.15" }, { name = "pycpfcnpj", specifier = ">=1.8" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" }, @@ -488,6 +515,19 @@ dev = [ { 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]] name = "orjson" version = "3.10.16" diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index a92bf8a..6729294 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.1.11" +version = "0.1.13" description = "Add your description here" readme = "README.md" authors = [ @@ -21,6 +21,8 @@ dependencies = [ "pytz>=2025.1", "shortuuid>=1.0.13", "requests>=2.32.3", + "meilisearch>=0.34.0", + "arnparse>=0.0.2", ] [dependency-groups] diff --git a/layercake/uv.lock b/layercake/uv.lock index add06f4..afc28c5 100644 --- a/layercake/uv.lock +++ b/layercake/uv.lock @@ -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 }, ] +[[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]] name = "attrs" 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 }, ] +[[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]] name = "certifi" version = "2025.1.31" @@ -465,15 +488,17 @@ wheels = [ [[package]] name = "layercake" -version = "0.1.10" +version = "0.1.12" source = { editable = "." } dependencies = [ + { name = "arnparse" }, { name = "aws-lambda-powertools", extra = ["all"] }, { name = "boto3" }, { name = "elasticsearch" }, { name = "elasticsearch-dsl" }, { name = "ftfy" }, { name = "glom" }, + { name = "meilisearch" }, { name = "orjson" }, { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, @@ -494,12 +519,14 @@ dev = [ [package.metadata] requires-dist = [ + { name = "arnparse", specifier = ">=0.0.2" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.8.0" }, { name = "boto3", specifier = ">=1.37.16" }, { name = "elasticsearch", specifier = ">=8.17.2" }, { name = "elasticsearch-dsl", specifier = ">=8.17.1" }, { name = "ftfy", specifier = ">=6.3.1" }, { name = "glom", specifier = ">=24.11.0" }, + { name = "meilisearch", specifier = ">=0.34.0" }, { name = "orjson", specifier = ">=3.10.15" }, { name = "pycpfcnpj", specifier = ">=1.8" }, { 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 }, ] +[[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]] name = "mergedeep" version = "1.3.4"