wip
This commit is contained in:
@@ -11,7 +11,7 @@ from aws_lambda_powertools.event_handler.exceptions import ServiceError
|
|||||||
from aws_lambda_powertools.logging import correlation_paths
|
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
|
from middlewares import AuthorizerMiddleware, TenantMiddleware
|
||||||
from routes import courses, enrollments, lookup, me, orders, users, webhooks
|
from routes import courses, enrollments, lookup, me, orders, users, webhooks
|
||||||
|
|
||||||
DEBUG = os.getenv('LOG_LEVEL') == 'DEBUG'
|
DEBUG = os.getenv('LOG_LEVEL') == 'DEBUG'
|
||||||
@@ -19,7 +19,7 @@ DEBUG = os.getenv('LOG_LEVEL') == 'DEBUG'
|
|||||||
tracer = Tracer()
|
tracer = Tracer()
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
app = APIGatewayHttpResolver(enable_validation=True, debug=DEBUG)
|
app = APIGatewayHttpResolver(enable_validation=True, debug=DEBUG)
|
||||||
app.use(middlewares=[AuthorizerMiddleware()])
|
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')
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Example
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
from aws_lambda_powertools import Logger, Tracer
|
from aws_lambda_powertools import Logger, Tracer
|
||||||
@@ -53,31 +54,46 @@ 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):
|
def lambda_handler(event: APIGatewayAuthorizerEventV2, context: LambdaContext) -> dict:
|
||||||
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()
|
||||||
|
|
||||||
kwargs = asdict(_authorizer(bearer))
|
attrs = _authorizer(bearer).asdict()
|
||||||
return APIGatewayAuthorizerResponseV2(**kwargs).asdict()
|
return APIGatewayAuthorizerResponseV2(**attrs).asdict()
|
||||||
|
|
||||||
|
|
||||||
class TokenType(str, Enum):
|
class AuthFlowType(str, Enum):
|
||||||
API_KEY = 'API_KEY'
|
USER_AUTH = 'USER_AUTH'
|
||||||
USER_TOKEN = 'USER_TOKEN'
|
API_AUTH = 'API_AUTH'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BearerToken:
|
class BearerToken:
|
||||||
auth_type: TokenType
|
auth_flow_type: AuthFlowType
|
||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Authorizer:
|
class Authorizer:
|
||||||
authorize: bool = False
|
authorize: bool = False
|
||||||
context: dict | None = None
|
context: dict[str, Any] | None = None
|
||||||
|
auth_flow_type: AuthFlowType = AuthFlowType.USER_AUTH
|
||||||
|
|
||||||
|
def asdict(self) -> dict:
|
||||||
|
data = asdict(self)
|
||||||
|
auth_flow_type = data.pop('auth_flow_type')
|
||||||
|
|
||||||
|
# If authorization is enabled, add `auth_flow_type` to the context
|
||||||
|
if self.authorize:
|
||||||
|
data['context'].update(auth_flow_type=auth_flow_type)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _get_apikey(token: str) -> dict[str, dict | str]:
|
||||||
|
return collect.get_item(KeyPair('apikey', token))
|
||||||
|
|
||||||
|
|
||||||
def _authorizer(bearer: BearerToken) -> Authorizer:
|
def _authorizer(bearer: BearerToken) -> Authorizer:
|
||||||
@@ -95,22 +111,13 @@ def _authorizer(bearer: BearerToken) -> Authorizer:
|
|||||||
An Authorizer object with the appropriate authorization status and context.
|
An Authorizer object with the appropriate authorization status and context.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
match bearer.auth_type:
|
if bearer.auth_flow_type == AuthFlowType.USER_AUTH:
|
||||||
case TokenType.USER_TOKEN:
|
user = get_user(bearer.token, idp_client)
|
||||||
user = get_user(bearer.token, idp_client=idp_client)
|
return Authorizer(True, {'user': user})
|
||||||
return Authorizer(True, {'user': user})
|
|
||||||
case TokenType.API_KEY:
|
apikey = _get_apikey(bearer.token)
|
||||||
apikey = collect.get_item(KeyPair('apikey', bearer.token))
|
context = pick(('tenant', 'user'), apikey)
|
||||||
return Authorizer(
|
return Authorizer(True, context, AuthFlowType.API_AUTH)
|
||||||
True,
|
|
||||||
pick(
|
|
||||||
(
|
|
||||||
'user',
|
|
||||||
'tenant',
|
|
||||||
),
|
|
||||||
apikey,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return Authorizer()
|
return Authorizer()
|
||||||
|
|
||||||
@@ -118,14 +125,14 @@ def _authorizer(bearer: BearerToken) -> Authorizer:
|
|||||||
def _parse_bearer_token(s: str) -> BearerToken | None:
|
def _parse_bearer_token(s: str) -> BearerToken | None:
|
||||||
"""Parses and identifies a bearer token as either an API key or a user token."""
|
"""Parses and identifies a bearer token as either an API key or a user token."""
|
||||||
try:
|
try:
|
||||||
_, bearer_token = s.split(' ')
|
_, token = s.split(' ')
|
||||||
|
|
||||||
if bearer_token.startswith(APIKEY_PREFIX):
|
if token.startswith(APIKEY_PREFIX):
|
||||||
return BearerToken(
|
return BearerToken(
|
||||||
TokenType.API_KEY,
|
AuthFlowType.API_AUTH,
|
||||||
bearer_token.removeprefix(APIKEY_PREFIX),
|
token.removeprefix(APIKEY_PREFIX),
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return BearerToken(TokenType.USER_TOKEN, bearer_token)
|
return BearerToken(AuthFlowType.USER_AUTH, token)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class UnauthorizedError(Exception): ...
|
class UnauthorizedError(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
def get_user(access_token: str, *, idp_client) -> dict | None:
|
def get_user(access_token: str, /, idp_client) -> dict[str, str]:
|
||||||
"""Gets the user attributes and metadata for a user."""
|
"""Gets the user attributes and metadata for a user."""
|
||||||
try:
|
try:
|
||||||
user = idp_client.get_user(AccessToken=access_token)
|
user = idp_client.get_user(AccessToken=access_token)
|
||||||
|
|||||||
@@ -12,15 +12,21 @@ from aws_lambda_powertools.shared.functions import (
|
|||||||
from layercake.dateutils import now, ttl
|
from layercake.dateutils import now, ttl
|
||||||
from layercake.dynamodb import ComposeKey, DynamoDBCollection, KeyPair
|
from layercake.dynamodb import ComposeKey, DynamoDBCollection, KeyPair
|
||||||
from layercake.funcs import pick
|
from layercake.funcs import pick
|
||||||
from pydantic import UUID4, BaseModel, Field
|
from pydantic import UUID4, BaseModel, EmailStr, Field
|
||||||
|
|
||||||
|
from auth import AuthFlowType
|
||||||
|
|
||||||
LOG_RETENTION_DAYS = 365 * 2 # 2 years
|
LOG_RETENTION_DAYS = 365 * 2 # 2 years
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatedUser(BaseModel):
|
class User(BaseModel):
|
||||||
id: str = Field(alias='custom:user_id')
|
id: str
|
||||||
name: str
|
name: str
|
||||||
email: str
|
email: EmailStr
|
||||||
|
|
||||||
|
|
||||||
|
class CognitoUser(User):
|
||||||
|
id: str = Field(alias='custom:user_id')
|
||||||
email_verified: bool
|
email_verified: bool
|
||||||
sub: UUID4
|
sub: UUID4
|
||||||
|
|
||||||
@@ -33,12 +39,31 @@ class AuthorizerMiddleware(BaseMiddlewareHandler):
|
|||||||
) -> Response:
|
) -> Response:
|
||||||
# Gets the Lambda authorizer associated with the current API Gateway event.
|
# Gets the Lambda authorizer associated with the current API Gateway event.
|
||||||
# You can check the file `auth.py` for more details.
|
# You can check the file `auth.py` for more details.
|
||||||
authorizer = app.current_event.request_context.authorizer.get_lambda
|
context = app.current_event.request_context.authorizer.get_lambda
|
||||||
|
auth_flow_type = context.get('auth_flow_type')
|
||||||
|
|
||||||
if 'user' in authorizer:
|
if not auth_flow_type:
|
||||||
user = authorizer['user']
|
return next_middleware(app)
|
||||||
app.append_context(authenticated_user=AuthenticatedUser(**user))
|
|
||||||
|
|
||||||
|
cls = {
|
||||||
|
AuthFlowType.USER_AUTH: CognitoUser,
|
||||||
|
AuthFlowType.API_AUTH: User,
|
||||||
|
}.get(auth_flow_type)
|
||||||
|
|
||||||
|
if cls:
|
||||||
|
app.append_context(user=cls(**context['user']))
|
||||||
|
|
||||||
|
return next_middleware(app)
|
||||||
|
|
||||||
|
|
||||||
|
class TenantMiddleware(BaseMiddlewareHandler):
|
||||||
|
def handler(
|
||||||
|
self,
|
||||||
|
app: APIGatewayHttpResolver,
|
||||||
|
next_middleware: NextMiddleware,
|
||||||
|
) -> Response:
|
||||||
|
context = app.current_event.request_context.authorizer.get_lambda
|
||||||
|
auth_flow_type = context.get('auth_flow_type')
|
||||||
return next_middleware(app)
|
return next_middleware(app)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,11 +103,10 @@ class AuditLogMiddleware(BaseMiddlewareHandler):
|
|||||||
app: APIGatewayHttpResolver,
|
app: APIGatewayHttpResolver,
|
||||||
next_middleware: NextMiddleware,
|
next_middleware: NextMiddleware,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
collect = self.collect
|
user = app.context.get('user')
|
||||||
|
req_context = app.current_event.request_context
|
||||||
|
ip_addr = req_context.http.source_ip
|
||||||
response = next_middleware(app)
|
response = next_middleware(app)
|
||||||
user = app.context.get('authenticated_user')
|
|
||||||
request_ctx = app.current_event.request_context
|
|
||||||
ip_addr = request_ctx.http.source_ip
|
|
||||||
|
|
||||||
# Successful request
|
# Successful request
|
||||||
if 200 <= response.status_code < 300 and user:
|
if 200 <= response.status_code < 300 and user:
|
||||||
@@ -98,7 +122,7 @@ class AuditLogMiddleware(BaseMiddlewareHandler):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
collect.put_item(
|
self.collect.put_item(
|
||||||
key=KeyPair(
|
key=KeyPair(
|
||||||
pk=ComposeKey(user.id, prefix='logs'),
|
pk=ComposeKey(user.id, prefix='logs'),
|
||||||
sk=now_.isoformat(),
|
sk=now_.isoformat(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from layercake.dynamodb import (
|
|||||||
|
|
||||||
import konviva
|
import konviva
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from middlewares import AuthenticatedUser
|
from middlewares import User
|
||||||
from settings import USER_TABLE
|
from settings import USER_TABLE
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -21,7 +21,7 @@ LIMIT = 25
|
|||||||
|
|
||||||
@router.get('/', include_in_schema=False)
|
@router.get('/', include_in_schema=False)
|
||||||
def me():
|
def me():
|
||||||
user: AuthenticatedUser = router.context['authenticated_user']
|
user: User = router.context['user']
|
||||||
acls = collect.get_items(
|
acls = collect.get_items(
|
||||||
KeyPair(user.id, PrefixKey('acls')),
|
KeyPair(user.id, PrefixKey('acls')),
|
||||||
limit=LIMIT,
|
limit=LIMIT,
|
||||||
@@ -39,7 +39,7 @@ def me():
|
|||||||
|
|
||||||
@router.get('/konviva', include_in_schema=False)
|
@router.get('/konviva', include_in_schema=False)
|
||||||
def konviva_():
|
def konviva_():
|
||||||
user: AuthenticatedUser = router.context['authenticated_user']
|
user: User = router.context['user']
|
||||||
token = konviva.token(user.email)
|
token = konviva.token(user.email)
|
||||||
|
|
||||||
return {'redirect_uri': konviva.redirect_uri(token)}
|
return {'redirect_uri': konviva.redirect_uri(token)}
|
||||||
|
|||||||
@@ -59,6 +59,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',
|
||||||
},
|
},
|
||||||
'jwt': {
|
'jwt': {
|
||||||
'claims': {'claim1': 'value1', 'claim2': 'value2'},
|
'claims': {'claim1': 'value1', 'claim2': 'value2'},
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ def test_bearer_jwt(lambda_context: LambdaContext):
|
|||||||
'sub': '58efed8d-d276-41a8-8502-4ab8b5a6415e',
|
'sub': '58efed8d-d276-41a8-8502-4ab8b5a6415e',
|
||||||
'name': 'pytest',
|
'name': 'pytest',
|
||||||
'custom:user_id': '5OxmMjL-ujoR5IMGegQz',
|
'custom:user_id': '5OxmMjL-ujoR5IMGegQz',
|
||||||
}
|
},
|
||||||
|
'auth_flow_type': 'USER_AUTH',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +56,11 @@ def test_bearer_apikey(
|
|||||||
'name': 'Sérgio R Siqueira',
|
'name': 'Sérgio R Siqueira',
|
||||||
'email': 'sergio@somosbeta.com.br',
|
'email': 'sergio@somosbeta.com.br',
|
||||||
},
|
},
|
||||||
|
'auth_flow_type': 'API_AUTH',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# # This data was added from seeds
|
# This data was added from seeds
|
||||||
assert app.lambda_handler(
|
assert app.lambda_handler(
|
||||||
{
|
{
|
||||||
'headers': {
|
'headers': {
|
||||||
@@ -75,11 +77,11 @@ def test_parse_bearer_token_api_key():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert bearer.token == '35433970-6857-4062-bb43-f71683b2f68e' # type: ignore
|
assert bearer.token == '35433970-6857-4062-bb43-f71683b2f68e' # type: ignore
|
||||||
assert bearer.auth_type == 'API_KEY' # type: ignore
|
assert bearer.auth_flow_type == 'API_AUTH' # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def test_parse_bearer_token_user_token():
|
def test_parse_bearer_token_user_token():
|
||||||
bearer = _parse_bearer_token('Bearer d977f5a2-0302-4dd2-87c7-57414264d27a')
|
bearer = _parse_bearer_token('Bearer d977f5a2-0302-4dd2-87c7-57414264d27a')
|
||||||
|
|
||||||
assert bearer.token == 'd977f5a2-0302-4dd2-87c7-57414264d27a' # type: ignore
|
assert bearer.token == 'd977f5a2-0302-4dd2-87c7-57414264d27a' # type: ignore
|
||||||
assert bearer.auth_type == 'USER_TOKEN' # type: ignore
|
assert bearer.auth_flow_type == 'USER_AUTH' # type: ignore
|
||||||
|
|||||||
2
http-api/uv.lock
generated
2
http-api/uv.lock
generated
@@ -444,7 +444,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.1.9"
|
version = "0.1.11"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aws-lambda-powertools", extra = ["all"] },
|
{ name = "aws-lambda-powertools", extra = ["all"] },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -21,7 +21,6 @@ dependencies = [
|
|||||||
"pytz>=2025.1",
|
"pytz>=2025.1",
|
||||||
"shortuuid>=1.0.13",
|
"shortuuid>=1.0.13",
|
||||||
"requests>=2.32.3",
|
"requests>=2.32.3",
|
||||||
"dataclasses-json>=0.6.7",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
51
layercake/uv.lock
generated
51
layercake/uv.lock
generated
@@ -279,19 +279,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
|
{ url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dataclasses-json"
|
|
||||||
version = "0.6.7"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "marshmallow" },
|
|
||||||
{ name = "typing-inspect" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dnspython"
|
name = "dnspython"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@@ -478,12 +465,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.1.9"
|
version = "0.1.10"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aws-lambda-powertools", extra = ["all"] },
|
{ name = "aws-lambda-powertools", extra = ["all"] },
|
||||||
{ name = "boto3" },
|
{ name = "boto3" },
|
||||||
{ name = "dataclasses-json" },
|
|
||||||
{ name = "elasticsearch" },
|
{ name = "elasticsearch" },
|
||||||
{ name = "elasticsearch-dsl" },
|
{ name = "elasticsearch-dsl" },
|
||||||
{ name = "ftfy" },
|
{ name = "ftfy" },
|
||||||
@@ -510,7 +496,6 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ 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 = "dataclasses-json", specifier = ">=0.6.7" },
|
|
||||||
{ 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" },
|
||||||
@@ -580,18 +565,6 @@ 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 = "marshmallow"
|
|
||||||
version = "3.26.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "packaging" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mergedeep"
|
name = "mergedeep"
|
||||||
version = "1.3.4"
|
version = "1.3.4"
|
||||||
@@ -689,15 +662,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/67/d0/ef6e82f7a68c7ac02e1a01815fbe88773f4f9e40728ed35bd1664a5d76f2/mkdocstrings_python-1.16.8-py3-none-any.whl", hash = "sha256:211b7aaf776cd45578ecb531e5ad0d3a35a8be9101a6bfa10de38a69af9d8fd8", size = 124116 },
|
{ url = "https://files.pythonhosted.org/packages/67/d0/ef6e82f7a68c7ac02e1a01815fbe88773f4f9e40728ed35bd1664a5d76f2/mkdocstrings_python-1.16.8-py3-none-any.whl", hash = "sha256:211b7aaf776cd45578ecb531e5ad0d3a35a8be9101a6bfa10de38a69af9d8fd8", size = 124116 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orjson"
|
name = "orjson"
|
||||||
version = "3.10.15"
|
version = "3.10.15"
|
||||||
@@ -1079,19 +1043,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-inspect"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "mypy-extensions" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user