add scope to id

This commit is contained in:
2025-10-24 20:05:56 -03:00
parent c68aa98fc9
commit c4509f5072
18 changed files with 173 additions and 58 deletions

View File

@@ -3,7 +3,7 @@ import os
from functools import partial
from typing import Any
from aws_lambda_powertools import Logger
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler.api_gateway import (
APIGatewayHttpResolver,
CORSConfig,
@@ -17,6 +17,7 @@ from json_encoder import JSONEncoder
from routes import courses, enrollments, orders, users
logger = Logger(__name__)
tracer = Tracer()
debug = 'AWS_SAM_LOCAL' in os.environ
serializer = partial(json.dumps, separators=(',', ':'), cls=JSONEncoder)
cors = CORSConfig(
@@ -26,7 +27,10 @@ cors = CORSConfig(
allow_credentials=False,
)
app = APIGatewayHttpResolver(
enable_validation=True, cors=cors, debug=debug, serializer=serializer
enable_validation=True,
cors=cors,
debug=debug,
serializer=serializer,
)
app.include_router(courses.router, prefix='/courses')
app.include_router(enrollments.router, prefix='/enrollments')
@@ -52,5 +56,6 @@ def exc_error(exc: ServiceError):
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
@tracer.capture_lambda_handler
def lambda_handler(event: dict[str, Any], context: LambdaContext) -> dict[str, Any]:
return app.resolve(event, context)

View File

@@ -1,3 +1,5 @@
import time
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import (
@@ -20,6 +22,7 @@ dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get('/<user_id>')
def get_user(user_id: str):
time.sleep(2)
return dyn.collection.get_item(
KeyPair(user_id, '0'),
exc_cls=NotFoundError,

View File

@@ -1,3 +1,5 @@
import time
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
@@ -12,6 +14,7 @@ dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get('/<user_id>/emails')
def get_emails(user_id: str):
time.sleep(1)
start_key = router.current_event.get_query_string_value('start_key', None)
return dyn.collection.query(

View File

@@ -1,3 +1,5 @@
import time
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
@@ -12,6 +14,7 @@ dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get('/<user_id>/orgs')
def get_orgs(user_id: str):
time.sleep(1)
start_key = router.current_event.get_query_string_value('start_key', None)
return dyn.collection.query(

View File

@@ -1,8 +1,28 @@
import json
from http import HTTPMethod, HTTPStatus
from ..conftest import HttpApiProxy, LambdaContext
def test_get_emails(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/users/15bacf02-1535-4bee-9022-19d106fd7518/emails',
method=HTTPMethod.GET,
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.OK
body = json.loads(r['body'])
assert len(body['items']) == 1
def test_get_orgs(
app,
seeds,

View File

@@ -1,4 +1,7 @@
{"id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5", "sk": "0", "name": "pytest" }
// User emails
{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#sergio@somosbeta.com.br"}
// User orgs
{"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#286f7729-7765-482a-880a-0b153ea799be", "name": "ACME", "cnpj": "00000000000191"}

View File

@@ -39,14 +39,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
':access_expires_at': access_expires_at,
},
)
transact.put(
item={
'id': new_image['id'],
'sk': 'METADATA#DEDUPLICATION_WINDOW',
'offset_days': 90,
'created_at': now_,
}
)
except Exception as exc:
logger.exception(exc)
return False

View File

@@ -7,6 +7,7 @@ from typing import Generator
import jsonlines
from aws_lambda_powertools.shared.json_encoder import Encoder
from layercake.dynamodb import deserialize
from layercake.strutils import md5_hash
from tqdm import tqdm
@@ -42,7 +43,12 @@ if __name__ == '__main__':
with sqlite3.connect('mydatabase.db') as conn:
cursor = conn.cursor()
cursor.execute(
'CREATE TABLE IF NOT EXISTS %s (pk TEXT, sk TEXT, json JSON)'
"""
CREATE TABLE IF NOT EXISTS %s (
_id TEXT PRIMARY KEY,
json JSON NOT NULL
)
"""
% table_name
)
@@ -50,12 +56,19 @@ if __name__ == '__main__':
readlines(input_dirpath),
desc=f'⏳ Inserting into table {table_name}',
):
_id = md5_hash(
str(
{
'id': record['id'],
'sk': record['sk'],
}
)
)
cursor.execute(
'INSERT INTO %s (pk, sk, json) VALUES (:pk, :sk, :json)'
% table_name,
'INSERT INTO %s (_id, json) VALUES (:_id, :json)' % table_name,
{
'pk': record['id'],
'sk': record['sk'],
'_id': _id,
'json': record,
},
)

View File

@@ -14,6 +14,7 @@ dev = [
"pycouchdb>=1.16.0",
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
"python-slugify>=8.0.4",
"ruff>=0.9.1",
"sqlite-utils>=3.38",
"tqdm>=4.67.1",

23
http-api/uv.lock generated
View File

@@ -435,6 +435,7 @@ dev = [
{ name = "pycouchdb" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "python-slugify" },
{ name = "ruff" },
{ name = "sqlite-utils" },
{ name = "tqdm" },
@@ -451,6 +452,7 @@ dev = [
{ name = "pycouchdb", specifier = ">=1.16.0" },
{ name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-cov", specifier = ">=6.0.0" },
{ name = "python-slugify", specifier = ">=8.0.4" },
{ name = "ruff", specifier = ">=0.9.1" },
{ name = "sqlite-utils", specifier = ">=3.38" },
{ name = "tqdm", specifier = ">=4.67.1" },
@@ -937,6 +939,18 @@ 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 = "python-slugify"
version = "8.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "text-unidecode" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
@@ -1059,6 +1073,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
]
[[package]]
name = "text-unidecode"
version = "1.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" },
]
[[package]]
name = "tqdm"
version = "4.67.1"

View File

@@ -9,14 +9,9 @@ OAUTH2_SCOPES_SUPPORTED: list[str] = [
'profile',
'email',
'offline_access',
'read:users',
'write:users',
'read:enrollments',
'write:enrollments',
'read:orders',
'write:orders',
'read:courses',
'write:courses',
'apps:admin',
'apps:studio',
'apps:insights',
]
SESSION_EXPIRES_IN = 86400 * 30 # 30 days

View File

@@ -1,4 +1,19 @@
from authlib.oauth2 import ResourceProtector as _ResourceProtector
from aws_lambda_powertools.event_handler import APIGatewayHttpResolver
from aws_lambda_powertools.event_handler.middlewares import NextMiddleware
from .requests import APIGatewayJsonRequest
class ResourceProtector(_ResourceProtector): ...
class ResourceProtector(_ResourceProtector):
def __call__(self, scopes=None, optional=False, **kwargs):
claims = kwargs
# backward compatibility
claims['scopes'] = scopes
def wrapper(app: APIGatewayHttpResolver, next_middleware: NextMiddleware):
request = APIGatewayJsonRequest(app.current_event)
print(request)
return next_middleware(app)
return wrapper

View File

@@ -1,5 +1,5 @@
import time
from dataclasses import dataclass
from dataclasses import dataclass, field
from authlib.oauth2.rfc6749 import (
AuthorizationCodeMixin,
@@ -15,6 +15,7 @@ class User:
name: str
email: str
email_verified: bool = False
scope: list[str] | None = None
def get_user_id(self):
return self.id

View File

@@ -1,3 +1,5 @@
from os import rename
from authlib.common.security import generate_token
from authlib.common.urls import add_params_to_uri
from authlib.jose import JsonWebKey
@@ -26,6 +28,7 @@ from config import ISSUER, OAUTH2_SCOPES_SUPPORTED, OAUTH2_TABLE
from integrations.apigateway_oauth2.authorization_server import (
AuthorizationServer,
)
from integrations.apigateway_oauth2.resource_protector import ResourceProtector
from integrations.apigateway_oauth2.tokens import (
OAuth2AuthorizationCode,
OAuth2Token,
@@ -65,15 +68,18 @@ class OpenIDCode(OpenIDCode_):
}
def generate_user_info(self, user: User, scope: str) -> UserInfo:
print(scope)
print('--' * 100)
return UserInfo(
user_info = UserInfo(
sub=user.id,
name=user.name,
email=user.email,
email_verified=user.email_verified,
).filter(scope)
if user.scope:
user_info['scope'] = ' '.join(user.scope)
return user_info
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
TOKEN_ENDPOINT_AUTH_METHODS = [
@@ -166,20 +172,22 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
authorization_code: OAuth2AuthorizationCode,
) -> User:
"""Authenticate the user related to this authorization_code."""
user = dyn.get_item(
KeyPair(
pk=authorization_code.user_id,
sk='0',
),
user = dyn.collection.get_items(
TransactKey(authorization_code.user_id)
+ SortKey('0')
+ SortKey('SCOPE', path_spec='scope', rename_key='scope'),
)
return User(**pick(('id', 'name', 'email', 'email_verified'), user))
return User(**pick(('id', 'name', 'email', 'email_verified', 'scope'), user))
class TokenExchangeGrant(grants.BaseGrant):
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post']
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic',
'client_secret_post',
]
@hooked
def validate_token_request(self):
@@ -288,6 +296,17 @@ class RevocationEndpoint(rfc7009.RevocationEndpoint):
token: OAuth2Token,
request: OAuth2Request,
):
"""
Mark token as revoked. Since token MUST be unique, it would be dangerous
to delete it. Consider this situation:
- Jane obtained a token XYZ
- Jane revoked (deleted) token XYZ
- Bob generated a new token XYZ
- Jane can use XYZ to access Bobs resource
- https://docs.authlib.org/en/latest/specs/rfc7009.html#authlib.oauth2.rfc7009.RevocationEndpoint.revoke_token
"""
user_id = token.user['id']
r = dyn.collection.query(KeyPair(pk=user_id, sk='SESSION'))
@@ -377,3 +396,5 @@ server.register_grant(TokenExchangeGrant)
server.register_grant(RefreshTokenGrant)
server.register_endpoint(RevocationEndpoint)
server.register_extension(IssuerParameter())
require_oauth = ResourceProtector()

View File

@@ -1,8 +1,10 @@
from aws_lambda_powertools.event_handler.api_gateway import Router
from oauth2 import require_oauth
router = Router()
@router.get('/userinfo')
@router.get('/userinfo', middlewares=[require_oauth('profile')])
def userinfo():
return {'name': 'test'}

View File

@@ -17,6 +17,7 @@ def test_authorize(
lambda_context: LambdaContext,
):
session = new_session(USER_ID)
print(session)
r = app.lambda_handler(
http_api_proxy(
@@ -26,7 +27,7 @@ def test_authorize(
'response_type': 'code',
'client_id': 'd72d4005-1fa7-4430-9754-80d5e2487bb6',
'redirect_uri': 'https://localhost/callback',
'scope': 'openid offline_access read:users',
'scope': 'openid profile email offline_access apps:admin',
'nonce': '123',
'state': '456',
},

View File

@@ -1,9 +1,9 @@
// OAuth2
{"id": "OAUTH2", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 1", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access read:users", "token_endpoint_auth_method": "client_secret_basic"}
{"id": "OAUTH2", "sk": "CLIENT_ID#8c5e92b0-9ed4-451e-8935-66084d8544b1", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 1", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access read:users", "token_endpoint_auth_method": "none"}
{"id": "OAUTH2", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 1", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access apps:admin", "token_endpoint_auth_method": "client_secret_basic"}
{"id": "OAUTH2", "sk": "CLIENT_ID#8c5e92b0-9ed4-451e-8935-66084d8544b1", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 1", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access apps:admin", "token_endpoint_auth_method": "none"}
{"id": "OAUTH2", "sk": "CLIENT_ID#6ebe1709-0831-455c-84c0-d4c753bf33c6", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 2", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access", "token_endpoint_auth_method": "none"}
{"id": "OAUTH2", "sk": "CLIENT_ID#1db63660-063d-4280-b2ea-388aca4a9459", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 3", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access read:users", "token_endpoint_auth_method": "client_secret_basic"}
{"id": "OAUTH2#CODE", "sk": "CODE#kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9", "client_id": "d72d4005-1fa7-4430-9754-80d5e2487bb6", "redirect_uri": "https://localhost/callback", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419", "nonce": null, "scope": "openid profile email read:users", "response_type": "code", "code_challenge": "ejYEIGKQUgMnNh4eV0sftb0hXdLwkvKm6OHXRYvC--I", "code_challenge_method": "S256", "created_at": "2025-08-07T12:38:26.550431-03:00"}
{"id": "OAUTH2", "sk": "CLIENT_ID#1db63660-063d-4280-b2ea-388aca4a9459", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 3", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access apps:admin", "token_endpoint_auth_method": "client_secret_basic"}
{"id": "OAUTH2#CODE", "sk": "CODE#kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9", "client_id": "d72d4005-1fa7-4430-9754-80d5e2487bb6", "redirect_uri": "https://localhost/callback", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419", "nonce": null, "scope": "openid profile email apps:admins", "response_type": "code", "code_challenge": "ejYEIGKQUgMnNh4eV0sftb0hXdLwkvKm6OHXRYvC--I", "code_challenge_method": "S256", "created_at": "2025-08-07T12:38:26.550431-03:00"}
{"id": "OAUTH2#TOKEN", "sk": "REFRESH_TOKEN#CyF3Ik3b9hMIo3REVv27gZAHd7dvwZq6QrkhWr7qHEen4UVy", "client_id": "d72d4005-1fa7-4430-9754-80d5e2487bb6", "token_type": "Bearer", "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IlRjT0VuV3JGSUFEYlZJNjJlY1pzU28ydEI1eW5mbkZZNTZ0Uy05b0stNW8ifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwiZXhwIjoxNzU5NTg2NzgzLCJjbGllbnRfaWQiOiJkNzJkNDAwNS0xZmE3LTQ0MzAtOTc1NC04MGQ1ZTI0ODdiYjYiLCJpYXQiOjE3NTg5ODE5ODMsImp0aSI6Ik9uVzRIZm1FdFl2a21CbE4iLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJlYWQ6dXNlcnMiLCJzdWIiOiIzNTdkYjFjNS03NDQyLTQwNzUtOThhMy1mYmU1YzkzOGE0MTkiLCJhdWQiOiJkNzJkNDAwNS0xZmE3LTQ0MzAtOTc1NC04MGQ1ZTI0ODdiYjYifQ.i0NVgvPuf5jvl8JcYNsVCzjVUTDLihgQO4LmLeNijx9Ed3p_EgtVtcHFWFvEebe_LwTuDDtIJveH22Piyp4zresNSc_YNumnuvoY1aNd0ic2RIEtXaklRroq0xHwL_IVT-Dt6P9xL5Hyygx47Pvmci4U3wWK32a6Sb1Mm7ZZgXA00xWI1bJ_zwxFLvDkHDp9nrAa_vEWN6zRBcWc7JYNsgiaPMC0DoL8it0k48_g44zfsjGAZLcWFMoPlYt3wIcQQDeCKMsSJI0VPnqKK0pq4OOVs-pjkMyAU5aEMPvVOwdAL3VZY16RXt3eTzsmMH1XoRdCMP6UAx4ZS10RLGUPeA", "scope": "openid profile email read:users", "user": {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "name": "S\u00e9rgio R Siqueira", "email": "sergio@somosbeta.com.br", "email_verified": false}, "expires_in": 180, "issued_at": 1758981984, "ttl": 1759586784}
@@ -16,7 +16,7 @@
// User data
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SCOPE", "scope": ["openid", "profile", "email", "offline_access", "read:users", "read:courses", "impersonate:users"]}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SCOPE", "scope": ["openid", "profile", "email", "offline_access", "apps:admin"]}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SESSION#36af142e-9f6d-49d3-bfe9-6a6bd6ab2712", "created_at": "2025-09-17T13:44:34.544491-03:00", "ttl": 1760719474}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "0", "name": "Johnny Cash", "email": "johnny@johnnycash.com"}

46
streams-events/uv.lock generated
View File

@@ -31,14 +31,14 @@ wheels = [
[[package]]
name = "authlib"
version = "1.6.2"
version = "1.6.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/e4f4ab5ce465821fe2229e10985ab80462941fe5d96387ae76bafd36f0ba/authlib-1.6.2.tar.gz", hash = "sha256:3bde83ac0392683eeef589cd5ab97e63cbe859e552dd75dca010548e79202cb1", size = 160429, upload-time = "2025-08-23T08:34:32.665Z" }
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/f4/00/fb65909bf4c8d7da893a12006074343402a8dc8c00d916b3cee524d97f3f/authlib-1.6.2-py2.py3-none-any.whl", hash = "sha256:2dd5571013cacf6b15f7addce03ed057ffdf629e9e81bacd9c08455a190e9b57", size = 239601, upload-time = "2025-08-23T08:34:31.4Z" },
{ 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]]
@@ -470,6 +470,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 = "jsonpath-ng"
version = "1.7.0"
@@ -484,7 +496,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.9.14"
version = "0.11.0"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },
@@ -493,6 +505,7 @@ dependencies = [
{ name = "dictdiffer" },
{ name = "ftfy" },
{ name = "glom" },
{ name = "joserfc" },
{ name = "meilisearch" },
{ name = "orjson" },
{ name = "passlib" },
@@ -500,7 +513,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"] },
@@ -511,11 +524,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" },
@@ -523,7 +537,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" },
@@ -802,15 +816,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" },
]
[[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.3.4"
@@ -860,6 +865,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
]
[[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"