diff --git a/http-api/dynamodb.py b/http-api/dynamodb.py index f5d27fc..135d114 100644 --- a/http-api/dynamodb.py +++ b/http-api/dynamodb.py @@ -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: return {} @@ -109,7 +109,7 @@ def _ttl_kwargs(ttl: datetime | int | None = None, tz=None): } -class MissingError(ValueError): +class MissingRecordError(ValueError): pass @@ -120,7 +120,7 @@ def get_record( glom_spec: str | None = None, raise_on_missing: bool = True, default_on_missing: Any = None, - missing_cls: Type[Exception] = MissingError, + missing_cls: Type[Exception] = MissingRecordError, delimiter: str = DELIMITER, persistence_layer: DynamoDBPersistenceLayer, ) -> Any: diff --git a/http-api/elastic.py b/http-api/elastic.py index 5c8c0bd..0166303 100644 --- a/http-api/elastic.py +++ b/http-api/elastic.py @@ -28,13 +28,13 @@ def search( r = s.execute() except Exception: return { - 'total_hits': 0, + 'total_items': 0, 'total_pages': 0, - 'hits': [], + 'items': [], } else: 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 'hits': [hit.to_dict() for hit in r], } diff --git a/http-api/http_models.py b/http-api/http_models.py index a190f6e..4443052 100644 --- a/http-api/http_models.py +++ b/http-api/http_models.py @@ -2,9 +2,9 @@ from pydantic import BaseModel class SearchResponse(BaseModel): - total_hits: int + total_items: int total_pages: int - hits: list[dict] + items: list[dict] class RecordResponse(BaseModel): diff --git a/http-api/models.py b/http-api/models.py index b75134e..4663e17 100644 --- a/http-api/models.py +++ b/http-api/models.py @@ -1,23 +1,40 @@ from typing import Annotated +from uuid import uuid4 -import shortuuid -from layercake.extra_types import CnpjStr -from pydantic import BaseModel, Field, StringConstraints +from layercake.extra_types import CnpjStr, CpfStr, NameStr +from pydantic import ( + UUID4, + BaseModel, + ConfigDict, + EmailStr, + Field, + StringConstraints, +) class Org(BaseModel): - id: str + id: UUID4 | str = Field(default_factory=uuid4) name: Annotated[str, StringConstraints(strip_whitespace=True)] 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): - id: str + id: UUID4 | str = Field(default_factory=uuid4) exp_interval: int class Course(BaseModel): - id: str = Field(default_factory=shortuuid.uuid) + id: UUID4 | str = Field(default_factory=uuid4) name: str cert: Cert | None = None access_period: int | None = None diff --git a/http-api/routes/courses/__init__.py b/http-api/routes/courses/__init__.py index 93e8f86..e62981b 100644 --- a/http-api/routes/courses/__init__.py +++ b/http-api/routes/courses/__init__.py @@ -14,7 +14,7 @@ router = Router() elastic_client = Elasticsearch(**ELASTIC_CONN) -@router.get('/', compress=True) +@router.get('/', compress=True, tags=['Course']) def get_courses() -> SearchResponse: event = router.current_event 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): return Response(status_code=HTTPStatus.CREATED) -@router.get('/') +@router.get('/', compress=True, tags=['Course']) def get_course(id: str): return {} diff --git a/http-api/routes/enrollments/__init__.py b/http-api/routes/enrollments/__init__.py index c95855d..e5e608d 100644 --- a/http-api/routes/enrollments/__init__.py +++ b/http-api/routes/enrollments/__init__.py @@ -13,7 +13,7 @@ router = Router() elastic_client = Elasticsearch(**ELASTIC_CONN) -@router.get('/') +@router.get('/', compress=True, tags=['Enrollment']) def get_enrollments() -> SearchResponse: event = router.current_event query = event.get_query_string_value('query', '{}') @@ -27,7 +27,7 @@ def get_enrollments() -> SearchResponse: ) -@router.get('/') +@router.get('/', compress=True, tags=['Enrollment']) def get_enrollment(id: str): return {} @@ -36,11 +36,11 @@ class CancelPayload(BaseModel): status: Literal['CANCELED'] = 'CANCELED' -@router.patch('/') +@router.patch('/', compress=True, tags=['Enrollment']) def cancel(id: str, payload: CancelPayload): return {} -@router.post('/') +@router.post('/', compress=True, tags=['Enrollment']) def enroll(): return {} diff --git a/http-api/routes/orders/__init__.py b/http-api/routes/orders/__init__.py index 7e1b172..dfb6583 100644 --- a/http-api/routes/orders/__init__.py +++ b/http-api/routes/orders/__init__.py @@ -11,7 +11,7 @@ router = Router() elastic_client = Elasticsearch(**ELASTIC_CONN) -@router.get('/') +@router.get('/', compress=True, tags=['Order']) def get_orders() -> SearchResponse: event = router.current_event query = event.get_query_string_value('query', '{}') diff --git a/http-api/routes/users/__init__.py b/http-api/routes/users/__init__.py index 79771de..ae788c4 100644 --- a/http-api/routes/users/__init__.py +++ b/http-api/routes/users/__init__.py @@ -8,12 +8,13 @@ from aws_lambda_powertools.event_handler.api_gateway import ( Router, ) from elasticsearch import Elasticsearch -from pydantic import UUID4, BaseModel, StringConstraints from layercake.dynamodb import DynamoDBPersistenceLayer -from dynamodb import KeyLoc, get_records +from pydantic import UUID4, BaseModel, StringConstraints import elastic +from dynamodb import KeyLoc, get_records from http_models import RecordResponse, SearchResponse +from models import User from settings import ELASTIC_CONN, USER_TABLE 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): cognito_sub: UUID4 new_password: Annotated[str, StringConstraints(min_length=6)] diff --git a/http-api/template.yaml b/http-api/template.yaml index cc631b1..9eb4450 100644 --- a/http-api/template.yaml +++ b/http-api/template.yaml @@ -23,7 +23,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:10 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:13 Environment: Variables: TZ: America/Sao_Paulo diff --git a/http-api/tests/conftest.py b/http-api/tests/conftest.py index f894cdf..57d0a67 100644 --- a/http-api/tests/conftest.py +++ b/http-api/tests/conftest.py @@ -2,8 +2,14 @@ import base64 import json from dataclasses import dataclass from http import HTTPMethod +from typing import Generator +import boto3 import pytest +from layercake.dynamodb import DynamoDBPersistenceLayer + +table_name = 'pytest' +dynamodb_endpoint_url = 'http://127.0.0.1:8000' @dataclass @@ -86,6 +92,36 @@ def http_api_proxy(): 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 def mock_app(monkeypatch): monkeypatch.setattr('settings.ELASTIC_CONN', {'hosts': 'http://127.0.0.1:9200'}) diff --git a/http-api/tests/routes/test_courses.py b/http-api/tests/routes/test_courses.py index fd34cd3..5bd2b4c 100644 --- a/http-api/tests/routes/test_courses.py +++ b/http-api/tests/routes/test_courses.py @@ -3,7 +3,11 @@ from http import HTTPMethod, HTTPStatus 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( http_api_proxy( raw_path='/courses', diff --git a/http-api/tests/routes/test_users.py b/http-api/tests/routes/test_users.py new file mode 100644 index 0000000..dcdc44d --- /dev/null +++ b/http-api/tests/routes/test_users.py @@ -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 diff --git a/http-api/uv.lock b/http-api/uv.lock index 0115c9b..a515fbb 100644 --- a/http-api/uv.lock +++ b/http-api/uv.lock @@ -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 }, ] +[[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]] name = "elastic-transport" 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 }, ] +[[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]] name = "face" version = "24.0.0" @@ -344,6 +366,15 @@ dev = [ { 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]] name = "iniconfig" version = "2.0.0" @@ -387,7 +418,7 @@ dependencies = [ { name = "glom" }, { name = "orjson" }, { name = "pycpfcnpj" }, - { name = "pydantic" }, + { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, { name = "pytz" }, { name = "shortuuid" }, @@ -403,7 +434,7 @@ requires-dist = [ { name = "glom", specifier = ">=24.11.0" }, { name = "orjson", specifier = ">=3.10.15" }, { 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 = "pytz", specifier = ">=2025.1" }, { 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 }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.27.2" diff --git a/layercake/layercake/dynamodb.py b/layercake/layercake/dynamodb.py index f56f9ca..3d2ed77 100644 --- a/layercake/layercake/dynamodb.py +++ b/layercake/layercake/dynamodb.py @@ -9,31 +9,49 @@ from botocore.exceptions import ClientError logger = Logger(__name__) -def _serialize(v): - if isinstance(v, datetime): - return v.isoformat() - - if isinstance(v, IPv4Address): - return str(v) - - if isinstance(v, (list, tuple)): - return [_serialize(x) for x in v] - - if isinstance(v, dict): - return {k: _serialize(dv) for k, dv in v.items()} - - return v +def _serialize_python_types(obj: Any) -> str | dict | list: + match obj: + case datetime(): + return obj.isoformat() + case IPv4Address(): + return str(obj) + case list() | tuple(): + return [_serialize_python_types(v) for v in obj] + case dict(): + return {k: _serialize_python_types(v) for k, v in obj.items()} + case _: + return obj 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: - 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 { 'id': pk, 'sk': sk, diff --git a/layercake/layercake/extra_types.py b/layercake/layercake/extra_types.py index 6be4c39..f9df46e 100644 --- a/layercake/layercake/extra_types.py +++ b/layercake/layercake/extra_types.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Annotated, Any import ftfy 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_extra_types.payment import PaymentCardNumber @@ -47,6 +48,14 @@ else: 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: """ @@ -145,11 +154,9 @@ if TYPE_CHECKING: CnpjStr = Annotated[str, ...] else: - class CpfStr(CpfCnpj): - ... + class CpfStr(CpfCnpj): ... - class CnpjStr(CpfCnpj): - ... + class CnpjStr(CpfCnpj): ... if __name__ == '__main__': diff --git a/layercake/layercake/py.typed b/layercake/layercake/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index 274e1c8..83b1948 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "glom>=24.11.0", "orjson>=3.10.15", "pycpfcnpj>=1.8", - "pydantic>=2.10.6", + "pydantic[email]>=2.10.6", "pydantic-extra-types>=2.10.3", "pytz>=2025.1", "shortuuid>=1.0.13", diff --git a/layercake/tests/test_dynamodb.py b/layercake/tests/test_dynamodb.py index 06ca31a..b4535d1 100644 --- a/layercake/tests/test_dynamodb.py +++ b/layercake/tests/test_dynamodb.py @@ -1,7 +1,40 @@ +from datetime import datetime +from ipaddress import IPv4Address + import pytest 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): @@ -33,5 +66,6 @@ def test_transact_write_items(dynamodb_client): }, cond_expr='attribute_not_exists(sk)', ) + with pytest.raises(ClientError): user_layer.transact_write_items(transact) diff --git a/layercake/tests/test_funcs.py b/layercake/tests/test_funcs.py new file mode 100644 index 0000000..34a5d0f --- /dev/null +++ b/layercake/tests/test_funcs.py @@ -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, + } diff --git a/layercake/uv.lock b/layercake/uv.lock index 24e4748..182d071 100644 --- a/layercake/uv.lock +++ b/layercake/uv.lock @@ -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 }, ] +[[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]] name = "elastic-transport" 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 }, ] +[[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]] name = "face" 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 }, ] +[[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]] name = "iniconfig" version = "2.1.0" @@ -362,7 +393,7 @@ dependencies = [ { name = "glom" }, { name = "orjson" }, { name = "pycpfcnpj" }, - { name = "pydantic" }, + { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, { name = "pytz" }, { name = "shortuuid" }, @@ -385,7 +416,7 @@ requires-dist = [ { name = "glom", specifier = ">=24.11.0" }, { name = "orjson", specifier = ">=3.10.15" }, { 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 = "pytz", specifier = ">=2025.1" }, { 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 }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.27.2"