fix jwks
This commit is contained in:
@@ -44,16 +44,6 @@ Resources:
|
|||||||
Authorizers:
|
Authorizers:
|
||||||
OAuth2Authorizer:
|
OAuth2Authorizer:
|
||||||
IdentitySource: "$request.header.Authorization"
|
IdentitySource: "$request.header.Authorization"
|
||||||
# AuthorizationScopes:
|
|
||||||
# - openid
|
|
||||||
# - profile
|
|
||||||
# - email
|
|
||||||
# - offline_access
|
|
||||||
# - read:users
|
|
||||||
# - read:enrollments
|
|
||||||
# - read:orders
|
|
||||||
# - read:courses
|
|
||||||
# - write:courses
|
|
||||||
JwtConfiguration:
|
JwtConfiguration:
|
||||||
issuer: "https://id.saladeaula.digital"
|
issuer: "https://id.saladeaula.digital"
|
||||||
audience:
|
audience:
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ OAUTH2_SCOPES_SUPPORTED: str = os.getenv('OAUTH2_SCOPES_SUPPORTED', '')
|
|||||||
|
|
||||||
JWT_SECRET: str = os.environ.get('JWT_SECRET') # type: ignore
|
JWT_SECRET: str = os.environ.get('JWT_SECRET') # type: ignore
|
||||||
JWT_ALGORITHM = 'HS256'
|
JWT_ALGORITHM = 'HS256'
|
||||||
JWT_EXP_SECONDS = 900 # 15 minutes
|
# JWT_EXP_SECONDS = 900 # 15 minutes
|
||||||
|
|
||||||
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 30 * 86400 # 30 days
|
SESSION_EXPIRES_IN = 86400 * 30 # 30 days
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from config import OAUTH2_REFRESH_TOKEN_EXPIRES_IN
|
|||||||
from .client import OAuth2Client
|
from .client import OAuth2Client
|
||||||
from .requests import APIGatewayJsonRequest, APIGatewayOAuth2Request
|
from .requests import APIGatewayJsonRequest, APIGatewayOAuth2Request
|
||||||
|
|
||||||
DYNAMODB_SORT_KEY = os.getenv('DYNAMODB_SORT_KEY')
|
|
||||||
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
|
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
@@ -121,7 +120,6 @@ class AuthorizationServer(oauth2.AuthorizationServer):
|
|||||||
self,
|
self,
|
||||||
client_id: str,
|
client_id: str,
|
||||||
):
|
):
|
||||||
"""Query OAuth client by client_id."""
|
|
||||||
client = self._persistence_layer.collection.get_item(
|
client = self._persistence_layer.collection.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk='OAUTH2',
|
pk='OAUTH2',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import time
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from authlib.oauth2.rfc6749 import (
|
from authlib.oauth2.rfc6749 import (
|
||||||
AuthorizationCodeMixin,
|
AuthorizationCodeMixin,
|
||||||
@@ -8,6 +9,17 @@ from authlib.oauth2.rfc6749 import (
|
|||||||
from layercake.dateutils import fromisoformat, now
|
from layercake.dateutils import fromisoformat, now
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class User:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
email_verified: bool = False
|
||||||
|
|
||||||
|
def get_user_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
|
||||||
class OAuth2AuthorizationCode(AuthorizationCodeMixin):
|
class OAuth2AuthorizationCode(AuthorizationCodeMixin):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -75,8 +87,8 @@ class OAuth2Token(TokenMixin):
|
|||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
self.refresh_token = refresh_token
|
self.refresh_token = refresh_token
|
||||||
|
|
||||||
def get_user(self) -> dict:
|
def get_user(self) -> User:
|
||||||
return self.user
|
return User(**self.user)
|
||||||
|
|
||||||
def check_client(self, client: ClientMixin) -> bool:
|
def check_client(self, client: ClientMixin) -> bool:
|
||||||
return self.client_id == client.get_client_id()
|
return self.client_id == client.get_client_id()
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from authlib.common.security import generate_token
|
from authlib.common.security import generate_token
|
||||||
from authlib.common.urls import add_params_to_uri
|
from authlib.common.urls import add_params_to_uri
|
||||||
from authlib.jose import jwt
|
from authlib.jose import JsonWebKey
|
||||||
from authlib.oauth2 import OAuth2Request, rfc7009, rfc9207
|
from authlib.oauth2 import OAuth2Request, rfc7009, rfc9207
|
||||||
from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants
|
from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants
|
||||||
from authlib.oauth2.rfc6750 import BearerTokenGenerator
|
from authlib.oauth2.rfc6750 import BearerTokenGenerator
|
||||||
@@ -31,22 +28,14 @@ from integrations.apigateway_oauth2.authorization_server import (
|
|||||||
from integrations.apigateway_oauth2.tokens import (
|
from integrations.apigateway_oauth2.tokens import (
|
||||||
OAuth2AuthorizationCode,
|
OAuth2AuthorizationCode,
|
||||||
OAuth2Token,
|
OAuth2Token,
|
||||||
|
User,
|
||||||
)
|
)
|
||||||
from util import read_file_path
|
from util import read_file_path
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||||
|
private_key = read_file_path('private.pem')
|
||||||
|
private_jwk = JsonWebKey.import_key(private_key)
|
||||||
@dataclass(frozen=True)
|
|
||||||
class User:
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
email: str
|
|
||||||
email_verified: bool = False
|
|
||||||
|
|
||||||
def get_user_id(self):
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDCode(OpenIDCode_):
|
class OpenIDCode(OpenIDCode_):
|
||||||
@@ -62,11 +51,10 @@ class OpenIDCode(OpenIDCode_):
|
|||||||
|
|
||||||
def get_jwt_config(self, grant):
|
def get_jwt_config(self, grant):
|
||||||
return {
|
return {
|
||||||
'key': read_file_path('private.pem'),
|
'key': private_key,
|
||||||
'alg': 'RS256',
|
'alg': 'RS256',
|
||||||
'kid': 'test',
|
|
||||||
'iss': ISSUER,
|
'iss': ISSUER,
|
||||||
'exp': 3600 * 2,
|
'exp': 3600,
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_user_info(self, user: User, scope: str) -> UserInfo:
|
def generate_user_info(self, user: User, scope: str) -> UserInfo:
|
||||||
@@ -234,7 +222,7 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
|
|||||||
)
|
)
|
||||||
transact.delete(
|
transact.delete(
|
||||||
key=KeyPair(
|
key=KeyPair(
|
||||||
pk=user.get('id'),
|
pk=user.get_user_id(),
|
||||||
sk=f'SESSION#REFRESH_TOKEN#{token}',
|
sk=f'SESSION#REFRESH_TOKEN#{token}',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -320,104 +308,9 @@ class IssuerParameter(rfc9207.IssuerParameter):
|
|||||||
return ISSUER
|
return ISSUER
|
||||||
|
|
||||||
|
|
||||||
class JWTBearerTokenGenerator(JWTBearerTokenGenerator_):
|
|
||||||
def get_jwks(self):
|
|
||||||
return read_file_path('private.pem')
|
|
||||||
|
|
||||||
def access_token_generator(self, client, grant_type, user, scope):
|
|
||||||
now = int(time.time())
|
|
||||||
expires_in = now + self._get_expires_in(client, grant_type)
|
|
||||||
|
|
||||||
token_data = {
|
|
||||||
'iss': self.issuer,
|
|
||||||
'exp': expires_in,
|
|
||||||
'client_id': client.get_client_id(),
|
|
||||||
'iat': now,
|
|
||||||
'jti': self.get_jti(client, grant_type, user, scope),
|
|
||||||
'scope': scope,
|
|
||||||
}
|
|
||||||
|
|
||||||
# In cases of access tokens obtained through grants where a resource owner is
|
|
||||||
# involved, such as the authorization code grant, the value of 'sub' SHOULD
|
|
||||||
# correspond to the subject identifier of the resource owner.
|
|
||||||
|
|
||||||
if user:
|
|
||||||
token_data['sub'] = user.get_user_id()
|
|
||||||
|
|
||||||
# In cases of access tokens obtained through grants where no resource owner is
|
|
||||||
# involved, such as the client credentials grant, the value of 'sub' SHOULD
|
|
||||||
# correspond to an identifier the authorization server uses to indicate the
|
|
||||||
# client application.
|
|
||||||
|
|
||||||
else:
|
|
||||||
token_data['sub'] = client.get_client_id()
|
|
||||||
|
|
||||||
# If the request includes a 'resource' parameter (as defined in [RFC8707]), the
|
|
||||||
# resulting JWT access token 'aud' claim SHOULD have the same value as the
|
|
||||||
# 'resource' parameter in the request.
|
|
||||||
|
|
||||||
# TODO: Implement this with RFC8707
|
|
||||||
if False: # pragma: no cover
|
|
||||||
...
|
|
||||||
|
|
||||||
# If the request does not include a 'resource' parameter, the authorization
|
|
||||||
# server MUST use a default resource indicator in the 'aud' claim. If a 'scope'
|
|
||||||
# parameter is present in the request, the authorization server SHOULD use it to
|
|
||||||
# infer the value of the default resource indicator to be used in the 'aud'
|
|
||||||
# claim. The mechanism through which scopes are associated with default resource
|
|
||||||
# indicator values is outside the scope of this specification.
|
|
||||||
|
|
||||||
else:
|
|
||||||
token_data['aud'] = self.get_audiences(client, user, scope)
|
|
||||||
|
|
||||||
# If the values in the 'scope' parameter refer to different default resource
|
|
||||||
# indicator values, the authorization server SHOULD reject the request with
|
|
||||||
# 'invalid_scope' as described in Section 4.1.2.1 of [RFC6749].
|
|
||||||
# TODO: Implement this with RFC8707
|
|
||||||
|
|
||||||
if auth_time := self.get_auth_time(user):
|
|
||||||
token_data['auth_time'] = auth_time
|
|
||||||
|
|
||||||
# The meaning and processing of acr Claim Values is out of scope for this
|
|
||||||
# specification.
|
|
||||||
|
|
||||||
if acr := self.get_acr(user):
|
|
||||||
token_data['acr'] = acr
|
|
||||||
|
|
||||||
# The definition of particular values to be used in the amr Claim is beyond the
|
|
||||||
# scope of this specification.
|
|
||||||
|
|
||||||
if amr := self.get_amr(user):
|
|
||||||
token_data['amr'] = amr
|
|
||||||
|
|
||||||
# Authorization servers MAY return arbitrary attributes not defined in any
|
|
||||||
# existing specification, as long as the corresponding claim names are collision
|
|
||||||
# resistant or the access tokens are meant to be used only within a private
|
|
||||||
# subsystem. Please refer to Sections 4.2 and 4.3 of [RFC7519] for details.
|
|
||||||
|
|
||||||
token_data.update(self.get_extra_claims(client, grant_type, user, scope))
|
|
||||||
|
|
||||||
# This specification registers the 'application/at+jwt' media type, which can
|
|
||||||
# be used to indicate that the content is a JWT access token. JWT access tokens
|
|
||||||
# MUST include this media type in the 'typ' header parameter to explicitly
|
|
||||||
# declare that the JWT represents an access token complying with this profile.
|
|
||||||
# Per the definition of 'typ' in Section 4.1.9 of [RFC7515], it is RECOMMENDED
|
|
||||||
# that the 'application/' prefix be omitted. Therefore, the 'typ' value used
|
|
||||||
# SHOULD be 'at+jwt'.
|
|
||||||
|
|
||||||
header = {'alg': self.alg, 'typ': 'at+jwt', 'kid': 'k1'}
|
|
||||||
|
|
||||||
access_token = jwt.encode(
|
|
||||||
header,
|
|
||||||
token_data,
|
|
||||||
key=self.get_jwks(),
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
return access_token.decode()
|
|
||||||
|
|
||||||
|
|
||||||
GRANT_TYPES_EXPIRES_IN = {
|
GRANT_TYPES_EXPIRES_IN = {
|
||||||
'refresh_token': 600,
|
'authorization_code': 60 * 10, # 10 minutes
|
||||||
|
'refresh_token': 86_400 * 7, # 7 days
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -434,6 +327,16 @@ def create_token_generator(length: int = 42):
|
|||||||
return token_generator
|
return token_generator
|
||||||
|
|
||||||
|
|
||||||
|
class JWTBearerTokenGenerator(JWTBearerTokenGenerator_):
|
||||||
|
def get_jwks(self) -> dict[str, list]: # type: ignore
|
||||||
|
"""Return the JWKs that will be used to sign the JWT access token."""
|
||||||
|
return {
|
||||||
|
'keys': [
|
||||||
|
private_jwk.as_dict(is_private=True),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
server = AuthorizationServer(persistence_layer=dyn)
|
server = AuthorizationServer(persistence_layer=dyn)
|
||||||
server.register_grant(
|
server.register_grant(
|
||||||
AuthorizationCodeGrant,
|
AuthorizationCodeGrant,
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ from aws_lambda_powertools.event_handler.api_gateway import Router
|
|||||||
from util import read_file_path
|
from util import read_file_path
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
public_jwk = JsonWebKey.import_key(read_file_path('public.pem'))
|
||||||
|
|
||||||
public_jwk = JsonWebKey.import_key(read_file_path('public.pem'), {'kty': 'RSA'})
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/.well-known/jwks.json')
|
@router.get('/.well-known/jwks.json')
|
||||||
def jwks():
|
def jwks():
|
||||||
key = public_jwk.as_dict()
|
return {
|
||||||
key['use'] = 'sig'
|
'keys': [
|
||||||
key['kid'] = 'k1'
|
public_jwk.as_dict(),
|
||||||
return {'keys': [key]}
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ from config import (
|
|||||||
ISSUER,
|
ISSUER,
|
||||||
JWT_ALGORITHM,
|
JWT_ALGORITHM,
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
OAUTH2_REFRESH_TOKEN_EXPIRES_IN,
|
|
||||||
OAUTH2_TABLE,
|
OAUTH2_TABLE,
|
||||||
|
SESSION_EXPIRES_IN,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -46,7 +46,7 @@ def session(
|
|||||||
http_only=True,
|
http_only=True,
|
||||||
secure=True,
|
secure=True,
|
||||||
same_site=None,
|
same_site=None,
|
||||||
max_age=OAUTH2_REFRESH_TOKEN_EXPIRES_IN,
|
max_age=SESSION_EXPIRES_IN,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -80,7 +80,7 @@ def _get_user(username: str) -> tuple[str, str]:
|
|||||||
def new_session(sub: str) -> str:
|
def new_session(sub: str) -> str:
|
||||||
session_id = str(uuid4())
|
session_id = str(uuid4())
|
||||||
now_ = now()
|
now_ = now()
|
||||||
exp = ttl(start_dt=now_, seconds=OAUTH2_REFRESH_TOKEN_EXPIRES_IN)
|
exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN)
|
||||||
token = jwt.encode(
|
token = jwt.encode(
|
||||||
{
|
{
|
||||||
'sid': session_id,
|
'sid': session_id,
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import os
|
|||||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
def get_file_path(name):
|
def get_file_path(name: str) -> str:
|
||||||
return os.path.join(ROOT, name)
|
return os.path.join(ROOT, name)
|
||||||
|
|
||||||
|
|
||||||
def read_file_path(name):
|
def read_file_path(name: str) -> str:
|
||||||
with open(get_file_path(name)) as f:
|
with open(get_file_path(name)) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "auth"
|
name = "id-saladeaula-digital"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = ""
|
readme = ""
|
||||||
|
|||||||
19
id.saladeaula.digital/tests/routes/test_jwks.py
Normal file
19
id.saladeaula.digital/tests/routes/test_jwks.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from http import HTTPMethod
|
||||||
|
|
||||||
|
from ..conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
|
|
||||||
|
def test_jwks(
|
||||||
|
app,
|
||||||
|
seeds,
|
||||||
|
http_api_proxy: HttpApiProxy,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
r = app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/.well-known/jwks.json',
|
||||||
|
method=HTTPMethod.GET,
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
print(r)
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
import pprint
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from http import HTTPMethod, HTTPStatus
|
from http import HTTPMethod, HTTPStatus
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|||||||
@@ -39,45 +39,44 @@ def test_token(
|
|||||||
assert r['statusCode'] == HTTPStatus.OK
|
assert r['statusCode'] == HTTPStatus.OK
|
||||||
|
|
||||||
r = json.loads(r['body'])
|
r = json.loads(r['body'])
|
||||||
print(r)
|
assert r['expires_in'] == 600
|
||||||
# assert r['expires_in'] == 600
|
|
||||||
|
|
||||||
# tokens = dynamodb_persistence_layer.query(
|
tokens = dynamodb_persistence_layer.query(
|
||||||
# key_cond_expr='#pk = :pk',
|
key_cond_expr='#pk = :pk',
|
||||||
# expr_attr_name={
|
expr_attr_name={
|
||||||
# '#pk': 'id',
|
'#pk': 'id',
|
||||||
# },
|
},
|
||||||
# expr_attr_values={
|
expr_attr_values={
|
||||||
# ':pk': 'OAUTH2#TOKEN',
|
':pk': 'OAUTH2#TOKEN',
|
||||||
# },
|
},
|
||||||
# )
|
)
|
||||||
# assert len(tokens['items']) == 2
|
assert len(tokens['items']) == 2
|
||||||
|
|
||||||
# r = app.lambda_handler(
|
r = app.lambda_handler(
|
||||||
# http_api_proxy(
|
http_api_proxy(
|
||||||
# raw_path='/token',
|
raw_path='/token',
|
||||||
# method=HTTPMethod.POST,
|
method=HTTPMethod.POST,
|
||||||
# headers={
|
headers={
|
||||||
# 'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
# },
|
},
|
||||||
# body=urlencode(
|
body=urlencode(
|
||||||
# {
|
{
|
||||||
# 'grant_type': 'refresh_token',
|
'grant_type': 'refresh_token',
|
||||||
# 'refresh_token': r['refresh_token'],
|
'refresh_token': r['refresh_token'],
|
||||||
# 'client_id': client_id,
|
'client_id': client_id,
|
||||||
# }
|
}
|
||||||
# ),
|
),
|
||||||
# ),
|
),
|
||||||
# lambda_context,
|
lambda_context,
|
||||||
# )
|
)
|
||||||
|
|
||||||
# r = dynamodb_persistence_layer.query(
|
r = dynamodb_persistence_layer.query(
|
||||||
# key_cond_expr='#pk = :pk',
|
key_cond_expr='#pk = :pk',
|
||||||
# expr_attr_name={
|
expr_attr_name={
|
||||||
# '#pk': 'id',
|
'#pk': 'id',
|
||||||
# },
|
},
|
||||||
# expr_attr_values={
|
expr_attr_values={
|
||||||
# ':pk': 'OAUTH2#TOKEN',
|
':pk': 'OAUTH2#TOKEN',
|
||||||
# },
|
},
|
||||||
# )
|
)
|
||||||
# assert len(r['items']) == 3
|
assert len(r['items']) == 3
|
||||||
|
|||||||
54
id.saladeaula.digital/uv.lock
generated
54
id.saladeaula.digital/uv.lock
generated
@@ -33,33 +33,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
|
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "auth"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "layercake" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "jsonlines" },
|
|
||||||
{ name = "pytest" },
|
|
||||||
{ name = "pytest-cov" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [{ name = "layercake", directory = "../layercake" }]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [
|
|
||||||
{ name = "jsonlines", specifier = ">=4.0.0" },
|
|
||||||
{ name = "pytest", specifier = ">=8.4.1" },
|
|
||||||
{ name = "pytest-cov", specifier = ">=6.2.1" },
|
|
||||||
{ name = "ruff", specifier = ">=0.12.1" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "authlib"
|
name = "authlib"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -404,6 +377,33 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690, upload-time = "2024-11-02T23:17:46.468Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690, upload-time = "2024-11-02T23:17:46.468Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-saladeaula-digital"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "layercake" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "jsonlines" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "layercake", directory = "../layercake" }]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "jsonlines", specifier = ">=4.0.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.4.1" },
|
||||||
|
{ name = "pytest-cov", specifier = ">=6.2.1" },
|
||||||
|
{ name = "ruff", specifier = ">=0.12.1" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.10"
|
version = "3.10"
|
||||||
|
|||||||
Reference in New Issue
Block a user