feat: add Sha256Str type
This commit is contained in:
1 parent
49b84acf1f
commit
dc5b09048d
4 files changed
+110
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
## Latest Changes
|
||||
|
||||
* feat(sha256): add `Sha256Str` for hex-encoded SHA-256 digests, normalized to lowercase.
|
||||
* fix(pendulum_dt): stop `DateTime` defaulting a timezone-naive input to UTC; it now stays naive, matching pydantic's stdlib `datetime` contract.
|
||||
* fix(mac_address): reject MAC octets with a sign or whitespace (e.g. `-a:...`, `+a:...`, ` a:...`) that `int(..., 16)` accepted.
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""The `pydantic_extra_types.sha256` module provides the `Sha256Str` data type.
|
||||
|
||||
A SHA-256 digest is 32 bytes long and is usually written as 64 hexadecimal characters,
|
||||
like the output of `hashlib.sha256(...).hexdigest()` or `sha256sum`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic_core import core_schema
|
||||
|
||||
|
||||
class Sha256Str(str):
|
||||
"""A hex-encoded SHA-256 digest, normalized to lowercase.
|
||||
|
||||
```py
|
||||
from pydantic import BaseModel
|
||||
|
||||
from pydantic_extra_types.sha256 import Sha256Str
|
||||
|
||||
|
||||
class Artifact(BaseModel):
|
||||
checksum: Sha256Str
|
||||
|
||||
|
||||
artifact = Artifact(checksum='E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855')
|
||||
print(artifact)
|
||||
# > checksum='e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
```
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(cls, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
|
||||
"""Return a Pydantic CoreSchema with the SHA-256 digest validation.
|
||||
|
||||
Args:
|
||||
source: The source type to be converted.
|
||||
handler: The handler to get the CoreSchema.
|
||||
|
||||
Returns:
|
||||
A Pydantic CoreSchema with the SHA-256 digest validation.
|
||||
"""
|
||||
return core_schema.no_info_after_validator_function(
|
||||
cls,
|
||||
core_schema.str_schema(pattern=r'^[0-9a-fA-F]{64}$', strip_whitespace=True, to_lower=True),
|
||||
)
|
||||
@@ -27,6 +27,7 @@ from pydantic_extra_types.s3 import S3Path
|
||||
from pydantic_extra_types.script_code import ISO_15924
|
||||
from pydantic_extra_types.semantic_version import SemanticVersion
|
||||
from pydantic_extra_types.semver import _VersionPydanticAnnotation
|
||||
from pydantic_extra_types.sha256 import Sha256Str
|
||||
from pydantic_extra_types.timezone_name import TimeZoneName
|
||||
from pydantic_extra_types.ulid import ULID
|
||||
|
||||
@@ -318,6 +319,21 @@ USNumberE164 = Annotated[
|
||||
'type': 'object',
|
||||
},
|
||||
),
|
||||
(
|
||||
Sha256Str,
|
||||
{
|
||||
'properties': {
|
||||
'x': {
|
||||
'pattern': '^[0-9a-fA-F]{64}$',
|
||||
'title': 'X',
|
||||
'type': 'string',
|
||||
}
|
||||
},
|
||||
'required': ['x'],
|
||||
'title': 'Model',
|
||||
'type': 'object',
|
||||
},
|
||||
),
|
||||
(
|
||||
DateTime,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from pydantic_extra_types.sha256 import Sha256Str
|
||||
|
||||
EMPTY_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
|
||||
|
||||
class Artifact(BaseModel):
|
||||
checksum: Sha256Str
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', [EMPTY_SHA256, EMPTY_SHA256.upper(), f' {EMPTY_SHA256}\n'])
|
||||
def test_valid_sha256(value: str) -> None:
|
||||
checksum = Artifact(checksum=value).checksum
|
||||
assert checksum == EMPTY_SHA256
|
||||
assert isinstance(checksum, Sha256Str)
|
||||
|
||||
|
||||
def test_sha256_from_json() -> None:
|
||||
artifact = Artifact.model_validate_json(f'{{"checksum": "{EMPTY_SHA256.upper()}"}}')
|
||||
assert artifact.checksum == EMPTY_SHA256
|
||||
assert artifact.model_dump_json() == f'{{"checksum":"{EMPTY_SHA256}"}}'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value',
|
||||
[
|
||||
'',
|
||||
EMPTY_SHA256[:-1],
|
||||
EMPTY_SHA256 + '0',
|
||||
'g' + EMPTY_SHA256[1:],
|
||||
'0x' + EMPTY_SHA256[2:],
|
||||
EMPTY_SHA256[:30] + ' ' + EMPTY_SHA256[31:],
|
||||
EMPTY_SHA256[:-1] + '\u0663',
|
||||
],
|
||||
)
|
||||
def test_invalid_sha256(value: str) -> None:
|
||||
with pytest.raises(ValidationError, match='string_pattern_mismatch'):
|
||||
Artifact(checksum=value)
|
||||
|
||||
|
||||
def test_sha256_not_a_string() -> None:
|
||||
with pytest.raises(ValidationError, match='string_type'):
|
||||
Artifact(checksum=1234)
|
||||
Reference in new issue
Block a user