This commit is contained in:
2025-03-20 21:26:04 -03:00
parent 85cbc9269c
commit 1f19380f5c
20 changed files with 293 additions and 54 deletions

View File

@@ -93,7 +93,7 @@ def _keys(
} }
def _ttl_kwargs(ttl: datetime | int | None = None, tz=None): def _ttl_kwargs(ttl: datetime | int | None = None, tz=None) -> dict:
if not ttl: if not ttl:
return {} return {}
@@ -109,7 +109,7 @@ def _ttl_kwargs(ttl: datetime | int | None = None, tz=None):
} }
class MissingError(ValueError): class MissingRecordError(ValueError):
pass pass
@@ -120,7 +120,7 @@ def get_record(
glom_spec: str | None = None, glom_spec: str | None = None,
raise_on_missing: bool = True, raise_on_missing: bool = True,
default_on_missing: Any = None, default_on_missing: Any = None,
missing_cls: Type[Exception] = MissingError, missing_cls: Type[Exception] = MissingRecordError,
delimiter: str = DELIMITER, delimiter: str = DELIMITER,
persistence_layer: DynamoDBPersistenceLayer, persistence_layer: DynamoDBPersistenceLayer,
) -> Any: ) -> Any:

View File

@@ -28,13 +28,13 @@ def search(
r = s.execute() r = s.execute()
except Exception: except Exception:
return { return {
'total_hits': 0, 'total_items': 0,
'total_pages': 0, 'total_pages': 0,
'hits': [], 'items': [],
} }
else: else:
return { return {
'total_hits': r.hits.total.value, # type: ignore 'total_items': r.hits.total.value, # type: ignore
'total_pages': math.ceil(r.hits.total.value / page_size), # type: ignore 'total_pages': math.ceil(r.hits.total.value / page_size), # type: ignore
'hits': [hit.to_dict() for hit in r], 'hits': [hit.to_dict() for hit in r],
} }

View File

@@ -2,9 +2,9 @@ from pydantic import BaseModel
class SearchResponse(BaseModel): class SearchResponse(BaseModel):
total_hits: int total_items: int
total_pages: int total_pages: int
hits: list[dict] items: list[dict]
class RecordResponse(BaseModel): class RecordResponse(BaseModel):

View File

@@ -1,23 +1,40 @@
from typing import Annotated from typing import Annotated
from uuid import uuid4
import shortuuid from layercake.extra_types import CnpjStr, CpfStr, NameStr
from layercake.extra_types import CnpjStr from pydantic import (
from pydantic import BaseModel, Field, StringConstraints UUID4,
BaseModel,
ConfigDict,
EmailStr,
Field,
StringConstraints,
)
class Org(BaseModel): class Org(BaseModel):
id: str id: UUID4 | str = Field(default_factory=uuid4)
name: Annotated[str, StringConstraints(strip_whitespace=True)] name: Annotated[str, StringConstraints(strip_whitespace=True)]
cnpj: CnpjStr | None = None cnpj: CnpjStr | None = None
class User(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
id: UUID4 | str = Field(default_factory=uuid4)
name: NameStr
email: EmailStr
email_verified: bool = False
cpf: CpfStr | None = None
class Cert(BaseModel): class Cert(BaseModel):
id: str id: UUID4 | str = Field(default_factory=uuid4)
exp_interval: int exp_interval: int
class Course(BaseModel): class Course(BaseModel):
id: str = Field(default_factory=shortuuid.uuid) id: UUID4 | str = Field(default_factory=uuid4)
name: str name: str
cert: Cert | None = None cert: Cert | None = None
access_period: int | None = None access_period: int | None = None

View File

@@ -14,7 +14,7 @@ router = Router()
elastic_client = Elasticsearch(**ELASTIC_CONN) elastic_client = Elasticsearch(**ELASTIC_CONN)
@router.get('/', compress=True) @router.get('/', compress=True, tags=['Course'])
def get_courses() -> SearchResponse: def get_courses() -> SearchResponse:
event = router.current_event event = router.current_event
query = event.get_query_string_value('query', '{}') query = event.get_query_string_value('query', '{}')
@@ -28,11 +28,11 @@ def get_courses() -> SearchResponse:
) )
@router.post('/', compress=True) @router.post('/', compress=True, tags=['Course'])
def post_course(payload: Course): def post_course(payload: Course):
return Response(status_code=HTTPStatus.CREATED) return Response(status_code=HTTPStatus.CREATED)
@router.get('/<id>') @router.get('/<id>', compress=True, tags=['Course'])
def get_course(id: str): def get_course(id: str):
return {} return {}

View File

@@ -13,7 +13,7 @@ router = Router()
elastic_client = Elasticsearch(**ELASTIC_CONN) elastic_client = Elasticsearch(**ELASTIC_CONN)
@router.get('/') @router.get('/', compress=True, tags=['Enrollment'])
def get_enrollments() -> SearchResponse: def get_enrollments() -> SearchResponse:
event = router.current_event event = router.current_event
query = event.get_query_string_value('query', '{}') query = event.get_query_string_value('query', '{}')
@@ -27,7 +27,7 @@ def get_enrollments() -> SearchResponse:
) )
@router.get('/<id>') @router.get('/<id>', compress=True, tags=['Enrollment'])
def get_enrollment(id: str): def get_enrollment(id: str):
return {} return {}
@@ -36,11 +36,11 @@ class CancelPayload(BaseModel):
status: Literal['CANCELED'] = 'CANCELED' status: Literal['CANCELED'] = 'CANCELED'
@router.patch('/<id>') @router.patch('/<id>', compress=True, tags=['Enrollment'])
def cancel(id: str, payload: CancelPayload): def cancel(id: str, payload: CancelPayload):
return {} return {}
@router.post('/') @router.post('/', compress=True, tags=['Enrollment'])
def enroll(): def enroll():
return {} return {}

View File

@@ -11,7 +11,7 @@ router = Router()
elastic_client = Elasticsearch(**ELASTIC_CONN) elastic_client = Elasticsearch(**ELASTIC_CONN)
@router.get('/') @router.get('/', compress=True, tags=['Order'])
def get_orders() -> SearchResponse: def get_orders() -> SearchResponse:
event = router.current_event event = router.current_event
query = event.get_query_string_value('query', '{}') query = event.get_query_string_value('query', '{}')

View File

@@ -8,12 +8,13 @@ from aws_lambda_powertools.event_handler.api_gateway import (
Router, Router,
) )
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
from pydantic import UUID4, BaseModel, StringConstraints
from layercake.dynamodb import DynamoDBPersistenceLayer from layercake.dynamodb import DynamoDBPersistenceLayer
from dynamodb import KeyLoc, get_records from pydantic import UUID4, BaseModel, StringConstraints
import elastic import elastic
from dynamodb import KeyLoc, get_records
from http_models import RecordResponse, SearchResponse from http_models import RecordResponse, SearchResponse
from models import User
from settings import ELASTIC_CONN, USER_TABLE from settings import ELASTIC_CONN, USER_TABLE
router = Router() router = Router()
@@ -36,6 +37,11 @@ def get_users() -> SearchResponse:
) )
@router.post('/', compress=True, tags=['User'], summary='Create user')
def post_user(payload: User):
return Response(status_code=HTTPStatus.CREATED)
class ResetPasswordPayload(BaseModel): class ResetPasswordPayload(BaseModel):
cognito_sub: UUID4 cognito_sub: UUID4
new_password: Annotated[str, StringConstraints(min_length=6)] new_password: Annotated[str, StringConstraints(min_length=6)]

View File

@@ -23,7 +23,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:10 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:13
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -2,8 +2,14 @@ import base64
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from http import HTTPMethod from http import HTTPMethod
from typing import Generator
import boto3
import pytest import pytest
from layercake.dynamodb import DynamoDBPersistenceLayer
table_name = 'pytest'
dynamodb_endpoint_url = 'http://127.0.0.1:8000'
@dataclass @dataclass
@@ -86,6 +92,36 @@ def http_api_proxy():
return HttpApiProxy() return HttpApiProxy()
@pytest.fixture
def dynamodb_client():
return boto3.client('dynamodb', endpoint_url=dynamodb_endpoint_url)
@pytest.fixture()
def dynamodb_persistence_layer(
dynamodb_client,
) -> Generator[DynamoDBPersistenceLayer, None, None]:
dynamodb_client.create_table(
AttributeDefinitions=[
{'AttributeName': 'id', 'AttributeType': 'S'},
{'AttributeName': 'sk', 'AttributeType': 'S'},
],
TableName=table_name,
KeySchema=[
{'AttributeName': 'id', 'KeyType': 'HASH'},
{'AttributeName': 'sk', 'KeyType': 'RANGE'},
],
ProvisionedThroughput={
'ReadCapacityUnits': 123,
'WriteCapacityUnits': 123,
},
)
yield DynamoDBPersistenceLayer(table_name, dynamodb_client)
dynamodb_client.delete_table(TableName=table_name)
@pytest.fixture @pytest.fixture
def mock_app(monkeypatch): def mock_app(monkeypatch):
monkeypatch.setattr('settings.ELASTIC_CONN', {'hosts': 'http://127.0.0.1:9200'}) monkeypatch.setattr('settings.ELASTIC_CONN', {'hosts': 'http://127.0.0.1:9200'})

View File

@@ -3,7 +3,11 @@ from http import HTTPMethod, HTTPStatus
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_courses(mock_app, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext): def test_post_course(
mock_app,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = mock_app.lambda_handler( r = mock_app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/courses', raw_path='/courses',

View File

@@ -0,0 +1,24 @@
from http import HTTPMethod, HTTPStatus
from ..conftest import HttpApiProxy, LambdaContext
def test_post_user(
mock_app,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = mock_app.lambda_handler(
http_api_proxy(
raw_path='/users',
method=HTTPMethod.POST,
body={
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.CREATED

40
http-api/uv.lock generated
View File

@@ -232,6 +232,15 @@ 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 = "dnspython"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
]
[[package]] [[package]]
name = "elastic-transport" name = "elastic-transport"
version = "8.17.1" version = "8.17.1"
@@ -272,6 +281,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/b4/5e707bca39062ba0b5227696a767db09767e5f09e869c6cb14aeb36e4b9d/elasticsearch_dsl-8.17.1-py3-none-any.whl", hash = "sha256:49ee12a6a8d43fcfc0af42b49649531a6ef228c9e4795325de27f6b309b62b6d", size = 158294 }, { url = "https://files.pythonhosted.org/packages/ad/b4/5e707bca39062ba0b5227696a767db09767e5f09e869c6cb14aeb36e4b9d/elasticsearch_dsl-8.17.1-py3-none-any.whl", hash = "sha256:49ee12a6a8d43fcfc0af42b49649531a6ef228c9e4795325de27f6b309b62b6d", size = 158294 },
] ]
[[package]]
name = "email-validator"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 },
]
[[package]] [[package]]
name = "face" name = "face"
version = "24.0.0" version = "24.0.0"
@@ -344,6 +366,15 @@ dev = [
{ name = "ruff", specifier = ">=0.9.1" }, { name = "ruff", specifier = ">=0.9.1" },
] ]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.0.0" version = "2.0.0"
@@ -387,7 +418,7 @@ dependencies = [
{ name = "glom" }, { name = "glom" },
{ name = "orjson" }, { name = "orjson" },
{ name = "pycpfcnpj" }, { name = "pycpfcnpj" },
{ name = "pydantic" }, { name = "pydantic", extra = ["email"] },
{ name = "pydantic-extra-types" }, { name = "pydantic-extra-types" },
{ name = "pytz" }, { name = "pytz" },
{ name = "shortuuid" }, { name = "shortuuid" },
@@ -403,7 +434,7 @@ requires-dist = [
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
{ name = "orjson", specifier = ">=3.10.15" }, { name = "orjson", specifier = ">=3.10.15" },
{ name = "pycpfcnpj", specifier = ">=1.8" }, { name = "pycpfcnpj", specifier = ">=1.8" },
{ name = "pydantic", specifier = ">=2.10.6" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
{ name = "pydantic-extra-types", specifier = ">=2.10.3" }, { name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "pytz", specifier = ">=2025.1" }, { name = "pytz", specifier = ">=2025.1" },
{ name = "shortuuid", specifier = ">=1.0.13" }, { name = "shortuuid", specifier = ">=1.0.13" },
@@ -509,6 +540,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
] ]
[package.optional-dependencies]
email = [
{ name = "email-validator" },
]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.27.2" version = "2.27.2"

View File

@@ -9,31 +9,49 @@ from botocore.exceptions import ClientError
logger = Logger(__name__) logger = Logger(__name__)
def _serialize(v): def _serialize_python_types(obj: Any) -> str | dict | list:
if isinstance(v, datetime): match obj:
return v.isoformat() case datetime():
return obj.isoformat()
if isinstance(v, IPv4Address): case IPv4Address():
return str(v) return str(obj)
case list() | tuple():
if isinstance(v, (list, tuple)): return [_serialize_python_types(v) for v in obj]
return [_serialize(x) for x in v] case dict():
return {k: _serialize_python_types(v) for k, v in obj.items()}
if isinstance(v, dict): case _:
return {k: _serialize(dv) for k, dv in v.items()} return obj
return v
def serialize(obj: dict) -> dict: def serialize(obj: dict) -> dict:
return {k: TypeSerializer().serialize(_serialize(v)) for k, v in obj.items()} serializer = TypeSerializer()
return {k: serializer.serialize(_serialize_python_types(v)) for k, v in obj.items()}
def deserialize(obj: dict) -> dict: def deserialize(obj: dict) -> dict:
return {k: TypeDeserializer().deserialize(v) for k, v in obj.items()} deserializer = TypeDeserializer()
return {k: deserializer.deserialize(v) for k, v in obj.items()}
def Key(pk: str, sk: str) -> dict[str, str]: def Key(
val: str | tuple[str, ...],
*,
prefix: str | None = None,
delimiter: str = '#',
) -> str:
if not prefix and not isinstance(val, tuple):
return val
if isinstance(val, str):
val = (val,)
if prefix:
val = (prefix,) + val
return delimiter.join(val)
def KeyPair(pk: str, sk: str) -> dict[str, str]:
return { return {
'id': pk, 'id': pk,
'sk': sk, 'sk': sk,

View File

@@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Annotated, Any
import ftfy import ftfy
from pycpfcnpj import cpfcnpj from pycpfcnpj import cpfcnpj
from pydantic import BaseModel, Field, GetCoreSchemaHandler from pydantic import BaseModel, Field, GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema, core_schema from pydantic_core import CoreSchema, core_schema
from pydantic_extra_types.payment import PaymentCardNumber from pydantic_extra_types.payment import PaymentCardNumber
@@ -47,6 +48,14 @@ else:
return name return name
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
field_schema = handler(core_schema)
field_schema.update(type='string', format='name')
return field_schema
class PaymentCardValidation: class PaymentCardValidation:
""" """
@@ -145,11 +154,9 @@ if TYPE_CHECKING:
CnpjStr = Annotated[str, ...] CnpjStr = Annotated[str, ...]
else: else:
class CpfStr(CpfCnpj): class CpfStr(CpfCnpj): ...
...
class CnpjStr(CpfCnpj): class CnpjStr(CpfCnpj): ...
...
if __name__ == '__main__': if __name__ == '__main__':

View File

View File

@@ -16,7 +16,7 @@ dependencies = [
"glom>=24.11.0", "glom>=24.11.0",
"orjson>=3.10.15", "orjson>=3.10.15",
"pycpfcnpj>=1.8", "pycpfcnpj>=1.8",
"pydantic>=2.10.6", "pydantic[email]>=2.10.6",
"pydantic-extra-types>=2.10.3", "pydantic-extra-types>=2.10.3",
"pytz>=2025.1", "pytz>=2025.1",
"shortuuid>=1.0.13", "shortuuid>=1.0.13",

View File

@@ -1,7 +1,40 @@
from datetime import datetime
from ipaddress import IPv4Address
import pytest import pytest
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from layercake.dynamodb import DynamoDBPersistenceLayer, TransactItems from layercake.dynamodb import (
DynamoDBPersistenceLayer,
Key,
KeyPair,
TransactItems,
serialize,
)
def test_serialize():
assert serialize(
{
'id': '123',
'sk': 'abc',
'date': datetime.fromisoformat('2025-03-20T18:29:10.713994'),
'ip': IPv4Address('127.0.0.1'),
}
) == {
'id': {'S': '123'},
'sk': {'S': 'abc'},
'date': {'S': '2025-03-20T18:29:10.713994'},
'ip': {'S': '127.0.0.1'},
}
def test_key():
assert Key(('122', 'abc'), prefix='schedules') == 'schedules#122#abc'
def test_keypair():
assert KeyPair('123', 'abc') == {'id': '123', 'sk': 'abc'}
def test_transact_write_items(dynamodb_client): def test_transact_write_items(dynamodb_client):
@@ -33,5 +66,6 @@ def test_transact_write_items(dynamodb_client):
}, },
cond_expr='attribute_not_exists(sk)', cond_expr='attribute_not_exists(sk)',
) )
with pytest.raises(ClientError): with pytest.raises(ClientError):
user_layer.transact_write_items(transact) user_layer.transact_write_items(transact)

View File

@@ -0,0 +1,21 @@
from layercake.funcs import omit, pick
def test_omit():
values = {'indigo': '#4b0082', 'navy': '#000080'}
assert omit(['indigo'], values) == {'navy': '#000080'}
assert omit(['test'], values) == values
def test_pick():
values = {'indigo': '#4b0082', 'navy': '#000080'}
assert pick(['navy'], values) == {'navy': '#000080'}
assert pick(['test'], values) == {}
def test_pick_default_val():
values = {'name': 'test'}
assert pick(['name', 'surname'], values, exclude_none=False, default=False) == {
'name': 'test',
'surname': False,
}

40
layercake/uv.lock generated
View File

@@ -232,6 +232,15 @@ 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 = "dnspython"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
]
[[package]] [[package]]
name = "elastic-transport" name = "elastic-transport"
version = "8.17.1" version = "8.17.1"
@@ -272,6 +281,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/b4/5e707bca39062ba0b5227696a767db09767e5f09e869c6cb14aeb36e4b9d/elasticsearch_dsl-8.17.1-py3-none-any.whl", hash = "sha256:49ee12a6a8d43fcfc0af42b49649531a6ef228c9e4795325de27f6b309b62b6d", size = 158294 }, { url = "https://files.pythonhosted.org/packages/ad/b4/5e707bca39062ba0b5227696a767db09767e5f09e869c6cb14aeb36e4b9d/elasticsearch_dsl-8.17.1-py3-none-any.whl", hash = "sha256:49ee12a6a8d43fcfc0af42b49649531a6ef228c9e4795325de27f6b309b62b6d", size = 158294 },
] ]
[[package]]
name = "email-validator"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 },
]
[[package]] [[package]]
name = "face" name = "face"
version = "24.0.0" version = "24.0.0"
@@ -319,6 +341,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690 }, { url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690 },
] ]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.1.0" version = "2.1.0"
@@ -362,7 +393,7 @@ dependencies = [
{ name = "glom" }, { name = "glom" },
{ name = "orjson" }, { name = "orjson" },
{ name = "pycpfcnpj" }, { name = "pycpfcnpj" },
{ name = "pydantic" }, { name = "pydantic", extra = ["email"] },
{ name = "pydantic-extra-types" }, { name = "pydantic-extra-types" },
{ name = "pytz" }, { name = "pytz" },
{ name = "shortuuid" }, { name = "shortuuid" },
@@ -385,7 +416,7 @@ requires-dist = [
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
{ name = "orjson", specifier = ">=3.10.15" }, { name = "orjson", specifier = ">=3.10.15" },
{ name = "pycpfcnpj", specifier = ">=1.8" }, { name = "pycpfcnpj", specifier = ">=1.8" },
{ name = "pydantic", specifier = ">=2.10.6" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
{ name = "pydantic-extra-types", specifier = ">=2.10.3" }, { name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "pytz", specifier = ">=2025.1" }, { name = "pytz", specifier = ">=2025.1" },
{ name = "shortuuid", specifier = ">=1.0.13" }, { name = "shortuuid", specifier = ">=1.0.13" },
@@ -491,6 +522,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
] ]
[package.optional-dependencies]
email = [
{ name = "email-validator" },
]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.27.2" version = "2.27.2"