update session
This commit is contained in:
@@ -3,8 +3,20 @@ import os
|
||||
ISSUER: str = os.getenv('ISSUER') # type: ignore
|
||||
|
||||
OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore
|
||||
OAUTH2_SCOPES_SUPPORTED: str = os.getenv('OAUTH2_SCOPES_SUPPORTED', '')
|
||||
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days
|
||||
OAUTH2_SCOPES_SUPPORTED: list[str] = [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'offline_access',
|
||||
'read:users',
|
||||
'write:users',
|
||||
'read:enrollments',
|
||||
'write:enrollments',
|
||||
'read:orders',
|
||||
'write:orders',
|
||||
'read:courses',
|
||||
'write:courses',
|
||||
]
|
||||
|
||||
SESSION_SECRET: str = os.environ.get('SESSION_SECRET') # type: ignore
|
||||
SESSION_EXPIRES_IN = 86400 * 30 # 30 days
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from dataclasses import asdict
|
||||
|
||||
import authlib.oauth2 as oauth2
|
||||
@@ -17,24 +16,19 @@ from config import OAUTH2_REFRESH_TOKEN_EXPIRES_IN
|
||||
from .client import OAuth2Client
|
||||
from .requests import APIGatewayJsonRequest, APIGatewayOAuth2Request
|
||||
|
||||
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class AuthorizationServer(oauth2.AuthorizationServer):
|
||||
def __init__(
|
||||
self,
|
||||
scopes_supported: list[str],
|
||||
*,
|
||||
persistence_layer: DynamoDBPersistenceLayer,
|
||||
) -> None:
|
||||
super().__init__(scopes_supported=scopes_supported)
|
||||
self._persistence_layer = persistence_layer
|
||||
|
||||
if OAUTH2_SCOPES_SUPPORTED:
|
||||
super().__init__(
|
||||
scopes_supported=set(OAUTH2_SCOPES_SUPPORTED.split()),
|
||||
)
|
||||
|
||||
def save_token(
|
||||
self,
|
||||
token: dict,
|
||||
|
||||
@@ -49,7 +49,7 @@ class APIGatewayOAuth2Request(requests.OAuth2Request):
|
||||
super().__init__(
|
||||
request.request_context.http.method,
|
||||
uri,
|
||||
request.headers,
|
||||
headers=request.headers,
|
||||
)
|
||||
self._request = request
|
||||
self.payload = APIGatewayOAuth2Payload(request)
|
||||
|
||||
@@ -3,6 +3,7 @@ from authlib.common.urls import add_params_to_uri
|
||||
from authlib.jose import JsonWebKey
|
||||
from authlib.oauth2 import OAuth2Request, rfc7009, rfc9207
|
||||
from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants
|
||||
from authlib.oauth2.rfc6749.hooks import hooked
|
||||
from authlib.oauth2.rfc6750 import BearerTokenGenerator
|
||||
from authlib.oauth2.rfc7636 import CodeChallenge
|
||||
from authlib.oauth2.rfc9068 import JWTBearerTokenGenerator as JWTBearerTokenGenerator_
|
||||
@@ -21,7 +22,7 @@ from layercake.dynamodb import (
|
||||
from layercake.funcs import omit, pick
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ISSUER, OAUTH2_TABLE
|
||||
from config import ISSUER, OAUTH2_SCOPES_SUPPORTED, OAUTH2_TABLE
|
||||
from integrations.apigateway_oauth2.authorization_server import (
|
||||
AuthorizationServer,
|
||||
)
|
||||
@@ -64,6 +65,8 @@ class OpenIDCode(OpenIDCode_):
|
||||
}
|
||||
|
||||
def generate_user_info(self, user: User, scope: str) -> UserInfo:
|
||||
print(scope)
|
||||
print('--' * 100)
|
||||
return UserInfo(
|
||||
sub=user.id,
|
||||
name=user.name,
|
||||
@@ -173,6 +176,20 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
||||
return User(**pick(('id', 'name', 'email', 'email_verified'), 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']
|
||||
|
||||
@hooked
|
||||
def validate_token_request(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@hooked
|
||||
def create_token_response(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class RefreshTokenNotFoundError(NotFoundError):
|
||||
def __init__(self, *_):
|
||||
super().__init__('Refresh token not found')
|
||||
@@ -337,7 +354,10 @@ class JWTBearerTokenGenerator(JWTBearerTokenGenerator_):
|
||||
}
|
||||
|
||||
|
||||
server = AuthorizationServer(persistence_layer=dyn)
|
||||
server = AuthorizationServer(
|
||||
persistence_layer=dyn,
|
||||
scopes_supported=OAUTH2_SCOPES_SUPPORTED,
|
||||
)
|
||||
server.register_grant(
|
||||
AuthorizationCodeGrant,
|
||||
[
|
||||
@@ -353,6 +373,7 @@ server.register_token_generator(
|
||||
expires_generator=expires_in,
|
||||
),
|
||||
)
|
||||
server.register_grant(TokenExchangeGrant)
|
||||
server.register_grant(RefreshTokenGrant)
|
||||
server.register_endpoint(RevocationEndpoint)
|
||||
server.register_extension(IssuerParameter())
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
import jwt
|
||||
from authlib.oauth2.rfc6749 import errors
|
||||
from authlib.oauth2.rfc6749.util import scope_to_list
|
||||
from aws_lambda_powertools import Logger
|
||||
@@ -9,12 +8,12 @@ from aws_lambda_powertools.event_handler.exceptions import (
|
||||
BadRequestError,
|
||||
ForbiddenError,
|
||||
ServiceError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from joserfc.errors import JoseError
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ISSUER, OAUTH2_TABLE, SESSION_SECRET
|
||||
from config import OAUTH2_TABLE
|
||||
from oauth2 import server
|
||||
|
||||
router = Router()
|
||||
@@ -26,19 +25,24 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||
def authorize():
|
||||
current_event = router.current_event
|
||||
cookies = _parse_cookies(current_event.get('cookies', []))
|
||||
session_id = cookies.get('session_id')
|
||||
session = cookies.get('__session')
|
||||
|
||||
if not session_id:
|
||||
raise BadRequestError('Missing session_id')
|
||||
if not session:
|
||||
raise BadRequestError('Missing session')
|
||||
|
||||
try:
|
||||
sub, session_scope = verify_session(session_id)
|
||||
sid, sub = session.split(':')
|
||||
# Raise if session is not found
|
||||
dyn.collection.get_item(
|
||||
KeyPair('SESSION', sid),
|
||||
exc_cls=InvalidSession,
|
||||
)
|
||||
grant = server.get_consent_grant(
|
||||
request=router.current_event,
|
||||
end_user=sub,
|
||||
)
|
||||
user_scopes = _user_scopes(sub)
|
||||
client_scopes = set(scope_to_list(grant.client.scope))
|
||||
user_scopes = set(scope_to_list(session_scope)) if session_scope else set()
|
||||
|
||||
# Deny authorization if user lacks scopes requested by client
|
||||
if not client_scopes.issubset(user_scopes):
|
||||
@@ -49,7 +53,7 @@ def authorize():
|
||||
grant_user=sub,
|
||||
grant=grant,
|
||||
)
|
||||
except jwt.exceptions.InvalidTokenError as err:
|
||||
except JoseError as err:
|
||||
logger.exception(err)
|
||||
raise BadRequestError(str(err))
|
||||
except errors.OAuth2Error as err:
|
||||
@@ -60,39 +64,19 @@ def authorize():
|
||||
)
|
||||
|
||||
|
||||
def verify_session(session_id: str) -> tuple[str, str | None]:
|
||||
payload = jwt.decode(
|
||||
session_id,
|
||||
SESSION_SECRET,
|
||||
algorithms=['HS256'],
|
||||
issuer=ISSUER,
|
||||
options={
|
||||
'require': ['exp', 'sub', 'iss', 'sid'],
|
||||
},
|
||||
)
|
||||
|
||||
user = dyn.collection.get_items(
|
||||
KeyPair(
|
||||
pk='SESSION',
|
||||
sk=payload['sid'],
|
||||
rename_key='session',
|
||||
def _user_scopes(sub: str) -> set:
|
||||
return set(
|
||||
scope_to_list(
|
||||
dyn.collection.get_item(
|
||||
KeyPair(
|
||||
pk=sub,
|
||||
sk=SortKey(sk='SCOPE', path_spec='scope'),
|
||||
),
|
||||
exc_cls=BadRequestError,
|
||||
)
|
||||
)
|
||||
+ KeyPair(
|
||||
pk=payload['sub'],
|
||||
sk=SortKey(
|
||||
sk='SCOPE',
|
||||
path_spec='scope',
|
||||
rename_key='scope',
|
||||
),
|
||||
),
|
||||
flatten_top=False,
|
||||
)
|
||||
|
||||
if 'session' not in user:
|
||||
raise SessionRevokedError('Session revoked')
|
||||
|
||||
return payload['sub'], user.get('scope')
|
||||
|
||||
|
||||
def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
|
||||
parsed_cookies = {}
|
||||
@@ -108,4 +92,4 @@ def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
|
||||
return parsed_cookies
|
||||
|
||||
|
||||
class SessionRevokedError(UnauthorizedError): ...
|
||||
class InvalidSession(BadRequestError): ...
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Annotated
|
||||
from uuid import uuid4
|
||||
|
||||
import boto3
|
||||
import jwt
|
||||
from aws_lambda_powertools.event_handler import (
|
||||
Response,
|
||||
)
|
||||
@@ -17,10 +16,8 @@ from passlib.hash import pbkdf2_sha256
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import (
|
||||
ISSUER,
|
||||
OAUTH2_TABLE,
|
||||
SESSION_EXPIRES_IN,
|
||||
SESSION_SECRET,
|
||||
)
|
||||
|
||||
router = Router()
|
||||
@@ -45,7 +42,7 @@ def session(
|
||||
status_code=HTTPStatus.OK,
|
||||
cookies=[
|
||||
Cookie(
|
||||
name='session_id',
|
||||
name='__session',
|
||||
value=new_session(user_id),
|
||||
http_only=True,
|
||||
secure=True,
|
||||
@@ -127,26 +124,15 @@ def _get_idp_user(
|
||||
|
||||
|
||||
def new_session(sub: str) -> str:
|
||||
session_id = str(uuid4())
|
||||
sid = str(uuid4())
|
||||
now_ = now()
|
||||
exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN)
|
||||
token = jwt.encode(
|
||||
{
|
||||
'sid': session_id,
|
||||
'sub': sub,
|
||||
'iss': ISSUER,
|
||||
'iat': int(now_.timestamp()),
|
||||
'exp': exp,
|
||||
},
|
||||
SESSION_SECRET,
|
||||
algorithm='HS256',
|
||||
)
|
||||
|
||||
with dyn.transact_writer() as transact:
|
||||
transact.put(
|
||||
item={
|
||||
'id': 'SESSION',
|
||||
'sk': session_id,
|
||||
'sk': sid,
|
||||
'user_id': sub,
|
||||
'ttl': exp,
|
||||
'created_at': now_,
|
||||
@@ -155,13 +141,13 @@ def new_session(sub: str) -> str:
|
||||
transact.put(
|
||||
item={
|
||||
'id': sub,
|
||||
'sk': f'SESSION#{session_id}',
|
||||
'sk': f'SESSION#{sid}',
|
||||
'ttl': exp,
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
|
||||
return token
|
||||
return f'{sid}:{sub}'
|
||||
|
||||
|
||||
class UserNotFoundError(NotFoundError):
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
issuerUrl.search = url.search
|
||||
redirect.search = url.search
|
||||
|
||||
if (!cookies.session_id) {
|
||||
if (!cookies?.__session) {
|
||||
return new Response(null, {
|
||||
status: httpStatus.FOUND,
|
||||
headers: {
|
||||
|
||||
@@ -14,7 +14,7 @@ Globals:
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
@@ -25,8 +25,6 @@ Globals:
|
||||
DYNAMODB_SORT_KEY: sk
|
||||
OAUTH2_TABLE: !Ref OAuth2Table
|
||||
ISSUER: https://id.saladeaula.digital
|
||||
SESSION_SECRET: 7DUTFB1iLeSpiXvmxbOZim1yPVmQbmBpAzgscob0RDzrL2wVwRi1ti2ZSry7jJAf
|
||||
OAUTH2_SCOPES_SUPPORTED: openid profile email offline_access read:users read:enrollments read:orders read:courses write:courses
|
||||
|
||||
Resources:
|
||||
HttpLog:
|
||||
|
||||
@@ -21,9 +21,6 @@ def pytest_configure():
|
||||
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
||||
os.environ['DYNAMODB_SORT_KEY'] = SK
|
||||
os.environ['ISSUER'] = 'http://localhost'
|
||||
os.environ['OAUTH2_SCOPES_SUPPORTED'] = (
|
||||
'openid profile email offline_access read:users'
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_authorize(
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
session_id = new_session(USER_ID)
|
||||
session = new_session(USER_ID)
|
||||
|
||||
r = app.lambda_handler(
|
||||
http_api_proxy(
|
||||
@@ -31,7 +31,7 @@ def test_authorize(
|
||||
'state': '456',
|
||||
},
|
||||
cookies=[
|
||||
f'session_id={session_id}; HttpOnly; Secure',
|
||||
f'__session={session}; HttpOnly; Secure',
|
||||
],
|
||||
),
|
||||
lambda_context,
|
||||
@@ -60,7 +60,7 @@ def test_forbidden(
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
session_id = new_session('fd5914ec-fd37-458b-b6b9-8aeab38b666b')
|
||||
session = new_session('fd5914ec-fd37-458b-b6b9-8aeab38b666b')
|
||||
|
||||
r = app.lambda_handler(
|
||||
http_api_proxy(
|
||||
@@ -75,7 +75,7 @@ def test_forbidden(
|
||||
'state': '456',
|
||||
},
|
||||
cookies=[
|
||||
f'session_id={session_id}; HttpOnly; Secure',
|
||||
f'__session={session}; HttpOnly; Secure',
|
||||
],
|
||||
),
|
||||
lambda_context,
|
||||
@@ -84,15 +84,13 @@ def test_forbidden(
|
||||
assert r['statusCode'] == HTTPStatus.FORBIDDEN
|
||||
|
||||
|
||||
def test_authorize_revoked(
|
||||
def test_invalid_session(
|
||||
app,
|
||||
seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
invalid_session_id = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiIwNTgzNTBhYi02NGU1LTQ0MzEtYmQyNy01MGVhOWIxNmQxZGYiLCJzdWIiOiIzNTdkYjFjNS03NDQyLTQwNzUtOThhMy1mYmU1YzkzOGE0MTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwiaWF0IjoxNzU1Mzk3Nzk5LCJleHAiOjE3NTUzOTg2OTl9.dDbiHYReVERbkNH2df4sXK2VIwT7G1KjNC5UrBuN6IQ'
|
||||
|
||||
r = app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/authorize',
|
||||
@@ -106,7 +104,7 @@ def test_authorize_revoked(
|
||||
'state': '456',
|
||||
},
|
||||
cookies=[
|
||||
f'session_id={invalid_session_id}; HttpOnly; Secure',
|
||||
'__session=10:10; HttpOnly; Secure',
|
||||
],
|
||||
),
|
||||
lambda_context,
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
// 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"}
|
||||
{"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": "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"}
|
||||
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"}
|
||||
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "SCOPE", "scope": "openid"}
|
||||
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "SCOPE", "scope": ["openid"]}
|
||||
46
id.saladeaula.digital/uv.lock
generated
46
id.saladeaula.digital/uv.lock
generated
@@ -35,14 +35,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.6.1"
|
||||
version = "1.6.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" }
|
||||
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/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" },
|
||||
{ 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]]
|
||||
@@ -469,6 +469,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 = "jsonlines"
|
||||
version = "4.0.0"
|
||||
@@ -495,7 +507,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.9.14"
|
||||
version = "0.10.1"
|
||||
source = { directory = "../layercake" }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
@@ -504,6 +516,7 @@ dependencies = [
|
||||
{ name = "dictdiffer" },
|
||||
{ name = "ftfy" },
|
||||
{ name = "glom" },
|
||||
{ name = "joserfc" },
|
||||
{ name = "meilisearch" },
|
||||
{ name = "orjson" },
|
||||
{ name = "passlib" },
|
||||
@@ -511,7 +524,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"] },
|
||||
@@ -522,11 +535,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" },
|
||||
@@ -534,7 +548,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" },
|
||||
@@ -825,15 +839,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[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.4.1"
|
||||
@@ -885,6 +890,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
|
||||
Reference in New Issue
Block a user