dhi 1.1.1__cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dhi/__init__.py +173 -0
- dhi/_dhi_native.cpython-313-aarch64-linux-gnu.so +0 -0
- dhi/_native.c +379 -0
- dhi/batch.py +236 -0
- dhi/constraints.py +358 -0
- dhi/datetime_types.py +108 -0
- dhi/fields.py +187 -0
- dhi/functional_validators.py +108 -0
- dhi/libsatya.so +0 -0
- dhi/model.py +658 -0
- dhi/networks.py +290 -0
- dhi/secret.py +105 -0
- dhi/special_types.py +359 -0
- dhi/types.py +345 -0
- dhi/validator.py +212 -0
- dhi-1.1.1.dist-info/METADATA +115 -0
- dhi-1.1.1.dist-info/RECORD +21 -0
- dhi-1.1.1.dist-info/WHEEL +6 -0
- dhi-1.1.1.dist-info/licenses/LICENSE +21 -0
- dhi-1.1.1.dist-info/top_level.txt +1 -0
- dhi.libs/libsatya-a22d98f4.so +0 -0
dhi/networks.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Network types for dhi - Pydantic v2 compatible.
|
|
3
|
+
|
|
4
|
+
Provides URL, email, IP address, and DSN validation types
|
|
5
|
+
matching Pydantic's network type system.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from dhi import BaseModel, EmailStr, HttpUrl, IPvAnyAddress
|
|
9
|
+
|
|
10
|
+
class Server(BaseModel):
|
|
11
|
+
url: HttpUrl
|
|
12
|
+
admin_email: EmailStr
|
|
13
|
+
ip: IPvAnyAddress
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import ipaddress
|
|
18
|
+
from typing import Annotated, Any, Optional, List
|
|
19
|
+
|
|
20
|
+
from .constraints import MaxLength
|
|
21
|
+
from .validator import ValidationError, HAS_NATIVE_EXT
|
|
22
|
+
|
|
23
|
+
# Try to get native extension for fast validation
|
|
24
|
+
_native = None
|
|
25
|
+
if HAS_NATIVE_EXT:
|
|
26
|
+
try:
|
|
27
|
+
from . import _dhi_native
|
|
28
|
+
_native = _dhi_native
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ============================================================
|
|
34
|
+
# Internal Validator Classes (used as Annotated metadata)
|
|
35
|
+
# ============================================================
|
|
36
|
+
|
|
37
|
+
class _EmailValidator:
|
|
38
|
+
"""Validates email format."""
|
|
39
|
+
|
|
40
|
+
_EMAIL_REGEX = re.compile(
|
|
41
|
+
r'^[a-zA-Z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}'
|
|
42
|
+
r'[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return "EmailValidator()"
|
|
47
|
+
|
|
48
|
+
def validate(self, value: Any, field_name: str = "value") -> str:
|
|
49
|
+
if not isinstance(value, str):
|
|
50
|
+
raise ValidationError(field_name, f"Expected str, got {type(value).__name__}")
|
|
51
|
+
# Use native extension for fast validation when available
|
|
52
|
+
if _native is not None:
|
|
53
|
+
if not _native.validate_email(value):
|
|
54
|
+
raise ValidationError(field_name, f"Invalid email address: {value!r}")
|
|
55
|
+
return value
|
|
56
|
+
# Pure Python fallback
|
|
57
|
+
if not self._EMAIL_REGEX.match(value):
|
|
58
|
+
raise ValidationError(field_name, f"Invalid email address: {value!r}")
|
|
59
|
+
# Must have at least one dot in domain
|
|
60
|
+
local, domain = value.rsplit('@', 1)
|
|
61
|
+
if '.' not in domain:
|
|
62
|
+
raise ValidationError(field_name, f"Invalid email domain: {domain!r}")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _NameEmailValidator:
|
|
67
|
+
"""Validates 'Display Name <email>' format."""
|
|
68
|
+
|
|
69
|
+
_NAME_EMAIL_REGEX = re.compile(
|
|
70
|
+
r'^(.+?)\s*<([^>]+)>$'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return "NameEmailValidator()"
|
|
75
|
+
|
|
76
|
+
def validate(self, value: Any, field_name: str = "value") -> str:
|
|
77
|
+
if not isinstance(value, str):
|
|
78
|
+
raise ValidationError(field_name, f"Expected str, got {type(value).__name__}")
|
|
79
|
+
# Try "Name <email>" format
|
|
80
|
+
match = self._NAME_EMAIL_REGEX.match(value)
|
|
81
|
+
if match:
|
|
82
|
+
email = match.group(2)
|
|
83
|
+
_EmailValidator().validate(email, field_name)
|
|
84
|
+
return value
|
|
85
|
+
# Try plain email format
|
|
86
|
+
_EmailValidator().validate(value, field_name)
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _UrlValidator:
|
|
91
|
+
"""Validates URL format with optional scheme/length constraints."""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
allowed_schemes: Optional[List[str]] = None,
|
|
96
|
+
max_length: Optional[int] = None,
|
|
97
|
+
host_required: bool = True,
|
|
98
|
+
default_scheme: Optional[str] = None,
|
|
99
|
+
):
|
|
100
|
+
self.allowed_schemes = allowed_schemes
|
|
101
|
+
self.max_length = max_length
|
|
102
|
+
self.host_required = host_required
|
|
103
|
+
self.default_scheme = default_scheme
|
|
104
|
+
# Build regex for URL validation
|
|
105
|
+
self._url_regex = re.compile(
|
|
106
|
+
r'^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?' # scheme
|
|
107
|
+
r'(?://)?' # authority indicator
|
|
108
|
+
r'([^/?#]*)' # authority (host:port)
|
|
109
|
+
r'([^?#]*)' # path
|
|
110
|
+
r'(?:\?([^#]*))?' # query
|
|
111
|
+
r'(?:#(.*))?$' # fragment
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
return f"UrlValidator(allowed_schemes={self.allowed_schemes})"
|
|
116
|
+
|
|
117
|
+
def validate(self, value: Any, field_name: str = "value") -> str:
|
|
118
|
+
if not isinstance(value, str):
|
|
119
|
+
raise ValidationError(field_name, f"Expected str, got {type(value).__name__}")
|
|
120
|
+
|
|
121
|
+
if self.max_length and len(value) > self.max_length:
|
|
122
|
+
raise ValidationError(
|
|
123
|
+
field_name,
|
|
124
|
+
f"URL length {len(value)} exceeds maximum {self.max_length}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
match = self._url_regex.match(value)
|
|
128
|
+
if not match:
|
|
129
|
+
raise ValidationError(field_name, f"Invalid URL: {value!r}")
|
|
130
|
+
|
|
131
|
+
scheme = match.group(1)
|
|
132
|
+
authority = match.group(2)
|
|
133
|
+
|
|
134
|
+
# Validate scheme
|
|
135
|
+
if self.allowed_schemes:
|
|
136
|
+
if not scheme:
|
|
137
|
+
if self.default_scheme:
|
|
138
|
+
scheme = self.default_scheme
|
|
139
|
+
else:
|
|
140
|
+
raise ValidationError(
|
|
141
|
+
field_name,
|
|
142
|
+
f"URL must have a scheme from: {self.allowed_schemes}"
|
|
143
|
+
)
|
|
144
|
+
if scheme.lower() not in [s.lower() for s in self.allowed_schemes]:
|
|
145
|
+
raise ValidationError(
|
|
146
|
+
field_name,
|
|
147
|
+
f"URL scheme '{scheme}' not in allowed schemes: {self.allowed_schemes}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Validate host
|
|
151
|
+
if self.host_required and not authority:
|
|
152
|
+
raise ValidationError(field_name, f"URL must have a host: {value!r}")
|
|
153
|
+
|
|
154
|
+
return value
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class _IPAddressValidator:
|
|
158
|
+
"""Validates IPv4 or IPv6 address."""
|
|
159
|
+
|
|
160
|
+
def __repr__(self) -> str:
|
|
161
|
+
return "IPAddressValidator()"
|
|
162
|
+
|
|
163
|
+
def validate(self, value: Any, field_name: str = "value") -> str:
|
|
164
|
+
if not isinstance(value, str):
|
|
165
|
+
raise ValidationError(field_name, f"Expected str, got {type(value).__name__}")
|
|
166
|
+
try:
|
|
167
|
+
ipaddress.ip_address(value)
|
|
168
|
+
except ValueError:
|
|
169
|
+
raise ValidationError(field_name, f"Invalid IP address: {value!r}")
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class _IPInterfaceValidator:
|
|
174
|
+
"""Validates IPv4 or IPv6 interface (address/prefix)."""
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
return "IPInterfaceValidator()"
|
|
178
|
+
|
|
179
|
+
def validate(self, value: Any, field_name: str = "value") -> str:
|
|
180
|
+
if not isinstance(value, str):
|
|
181
|
+
raise ValidationError(field_name, f"Expected str, got {type(value).__name__}")
|
|
182
|
+
try:
|
|
183
|
+
ipaddress.ip_interface(value)
|
|
184
|
+
except ValueError:
|
|
185
|
+
raise ValidationError(field_name, f"Invalid IP interface: {value!r}")
|
|
186
|
+
return value
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class _IPNetworkValidator:
|
|
190
|
+
"""Validates IPv4 or IPv6 network (CIDR notation)."""
|
|
191
|
+
|
|
192
|
+
def __repr__(self) -> str:
|
|
193
|
+
return "IPNetworkValidator()"
|
|
194
|
+
|
|
195
|
+
def validate(self, value: Any, field_name: str = "value") -> str:
|
|
196
|
+
if not isinstance(value, str):
|
|
197
|
+
raise ValidationError(field_name, f"Expected str, got {type(value).__name__}")
|
|
198
|
+
try:
|
|
199
|
+
ipaddress.ip_network(value, strict=False)
|
|
200
|
+
except ValueError:
|
|
201
|
+
raise ValidationError(field_name, f"Invalid IP network: {value!r}")
|
|
202
|
+
return value
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ============================================================
|
|
206
|
+
# Public Type Aliases - Pydantic v2 compatible
|
|
207
|
+
# ============================================================
|
|
208
|
+
|
|
209
|
+
# Email types
|
|
210
|
+
EmailStr = Annotated[str, _EmailValidator()]
|
|
211
|
+
NameEmail = Annotated[str, _NameEmailValidator()]
|
|
212
|
+
|
|
213
|
+
# URL types
|
|
214
|
+
AnyUrl = Annotated[str, _UrlValidator(allowed_schemes=None, host_required=False)]
|
|
215
|
+
AnyHttpUrl = Annotated[str, _UrlValidator(allowed_schemes=['http', 'https'], host_required=True)]
|
|
216
|
+
HttpUrl = Annotated[str, _UrlValidator(allowed_schemes=['http', 'https'], max_length=2083, host_required=True)]
|
|
217
|
+
FileUrl = Annotated[str, _UrlValidator(allowed_schemes=['file'], host_required=False)]
|
|
218
|
+
FtpUrl = Annotated[str, _UrlValidator(allowed_schemes=['ftp'], host_required=True)]
|
|
219
|
+
WebsocketUrl = Annotated[str, _UrlValidator(allowed_schemes=['ws', 'wss'], max_length=2083, host_required=True)]
|
|
220
|
+
AnyWebsocketUrl = Annotated[str, _UrlValidator(allowed_schemes=['ws', 'wss'], host_required=True)]
|
|
221
|
+
|
|
222
|
+
# DSN types (Database connection strings)
|
|
223
|
+
PostgresDsn = Annotated[str, _UrlValidator(
|
|
224
|
+
allowed_schemes=['postgres', 'postgresql', 'postgresql+asyncpg', 'postgresql+pg8000',
|
|
225
|
+
'postgresql+psycopg', 'postgresql+psycopg2', 'postgresql+psycopg2cffi',
|
|
226
|
+
'postgresql+py-postgresql', 'postgresql+pygresql'],
|
|
227
|
+
host_required=True,
|
|
228
|
+
)]
|
|
229
|
+
CockroachDsn = Annotated[str, _UrlValidator(
|
|
230
|
+
allowed_schemes=['cockroachdb', 'cockroachdb+psycopg2', 'cockroachdb+asyncpg'],
|
|
231
|
+
host_required=True,
|
|
232
|
+
)]
|
|
233
|
+
MySQLDsn = Annotated[str, _UrlValidator(
|
|
234
|
+
allowed_schemes=['mysql', 'mysql+mysqlconnector', 'mysql+aiomysql',
|
|
235
|
+
'mysql+asyncmy', 'mysql+mysqldb', 'mysql+pymysql',
|
|
236
|
+
'mysql+cymysql', 'mysql+pyodbc'],
|
|
237
|
+
host_required=True,
|
|
238
|
+
)]
|
|
239
|
+
MariaDBDsn = Annotated[str, _UrlValidator(
|
|
240
|
+
allowed_schemes=['mariadb', 'mariadb+mariadbconnector', 'mariadb+pymysql'],
|
|
241
|
+
host_required=True,
|
|
242
|
+
)]
|
|
243
|
+
ClickHouseDsn = Annotated[str, _UrlValidator(
|
|
244
|
+
allowed_schemes=['clickhouse+native', 'clickhouse+asynch'],
|
|
245
|
+
host_required=True,
|
|
246
|
+
)]
|
|
247
|
+
MongoDsn = Annotated[str, _UrlValidator(
|
|
248
|
+
allowed_schemes=['mongodb', 'mongodb+srv'],
|
|
249
|
+
host_required=True,
|
|
250
|
+
)]
|
|
251
|
+
RedisDsn = Annotated[str, _UrlValidator(
|
|
252
|
+
allowed_schemes=['redis', 'rediss'],
|
|
253
|
+
host_required=True,
|
|
254
|
+
)]
|
|
255
|
+
AmqpDsn = Annotated[str, _UrlValidator(
|
|
256
|
+
allowed_schemes=['amqp', 'amqps'],
|
|
257
|
+
host_required=True,
|
|
258
|
+
)]
|
|
259
|
+
KafkaDsn = Annotated[str, _UrlValidator(
|
|
260
|
+
allowed_schemes=['kafka', 'kafka+ssl'],
|
|
261
|
+
host_required=True,
|
|
262
|
+
)]
|
|
263
|
+
NatsDsn = Annotated[str, _UrlValidator(
|
|
264
|
+
allowed_schemes=['nats', 'tls', 'ws'],
|
|
265
|
+
host_required=True,
|
|
266
|
+
)]
|
|
267
|
+
SnowflakeDsn = Annotated[str, _UrlValidator(
|
|
268
|
+
allowed_schemes=['snowflake'],
|
|
269
|
+
host_required=True,
|
|
270
|
+
)]
|
|
271
|
+
|
|
272
|
+
# IP Address types
|
|
273
|
+
IPvAnyAddress = Annotated[str, _IPAddressValidator()]
|
|
274
|
+
IPvAnyInterface = Annotated[str, _IPInterfaceValidator()]
|
|
275
|
+
IPvAnyNetwork = Annotated[str, _IPNetworkValidator()]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
__all__ = [
|
|
279
|
+
# Email
|
|
280
|
+
"EmailStr", "NameEmail",
|
|
281
|
+
# URLs
|
|
282
|
+
"AnyUrl", "AnyHttpUrl", "HttpUrl", "FileUrl", "FtpUrl",
|
|
283
|
+
"WebsocketUrl", "AnyWebsocketUrl",
|
|
284
|
+
# DSNs
|
|
285
|
+
"PostgresDsn", "CockroachDsn", "MySQLDsn", "MariaDBDsn",
|
|
286
|
+
"ClickHouseDsn", "MongoDsn", "RedisDsn", "AmqpDsn",
|
|
287
|
+
"KafkaDsn", "NatsDsn", "SnowflakeDsn",
|
|
288
|
+
# IP
|
|
289
|
+
"IPvAnyAddress", "IPvAnyInterface", "IPvAnyNetwork",
|
|
290
|
+
]
|
dhi/secret.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secret types for dhi - Pydantic v2 compatible.
|
|
3
|
+
|
|
4
|
+
Provides types that hide sensitive values in repr/str output.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from dhi import BaseModel, SecretStr, SecretBytes
|
|
8
|
+
|
|
9
|
+
class Config(BaseModel):
|
|
10
|
+
api_key: SecretStr
|
|
11
|
+
token: SecretBytes
|
|
12
|
+
|
|
13
|
+
config = Config(api_key="sk-abc123", token=b"secret")
|
|
14
|
+
print(config.api_key) # SecretStr('**********')
|
|
15
|
+
print(config.api_key.get_secret_value()) # 'sk-abc123'
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .validator import ValidationError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SecretStr:
|
|
24
|
+
"""A string that hides its value in repr() and str().
|
|
25
|
+
|
|
26
|
+
Matches Pydantic's SecretStr type.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__slots__ = ('_secret_value',)
|
|
30
|
+
|
|
31
|
+
def __init__(self, value: Any) -> None:
|
|
32
|
+
if isinstance(value, SecretStr):
|
|
33
|
+
object.__setattr__(self, '_secret_value', value.get_secret_value())
|
|
34
|
+
elif isinstance(value, str):
|
|
35
|
+
object.__setattr__(self, '_secret_value', value)
|
|
36
|
+
else:
|
|
37
|
+
raise ValidationError("value", f"Expected str, got {type(value).__name__}")
|
|
38
|
+
|
|
39
|
+
def get_secret_value(self) -> str:
|
|
40
|
+
"""Get the actual secret value."""
|
|
41
|
+
return self._secret_value
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return "SecretStr('**********')"
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return '**********'
|
|
48
|
+
|
|
49
|
+
def __len__(self) -> int:
|
|
50
|
+
return len(self._secret_value)
|
|
51
|
+
|
|
52
|
+
def __eq__(self, other: object) -> bool:
|
|
53
|
+
if isinstance(other, SecretStr):
|
|
54
|
+
return self._secret_value == other._secret_value
|
|
55
|
+
return NotImplemented
|
|
56
|
+
|
|
57
|
+
def __hash__(self) -> int:
|
|
58
|
+
return hash(self._secret_value)
|
|
59
|
+
|
|
60
|
+
def __bool__(self) -> bool:
|
|
61
|
+
return bool(self._secret_value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SecretBytes:
|
|
65
|
+
"""Bytes that hide their value in repr() and str().
|
|
66
|
+
|
|
67
|
+
Matches Pydantic's SecretBytes type.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = ('_secret_value',)
|
|
71
|
+
|
|
72
|
+
def __init__(self, value: Any) -> None:
|
|
73
|
+
if isinstance(value, SecretBytes):
|
|
74
|
+
object.__setattr__(self, '_secret_value', value.get_secret_value())
|
|
75
|
+
elif isinstance(value, bytes):
|
|
76
|
+
object.__setattr__(self, '_secret_value', value)
|
|
77
|
+
else:
|
|
78
|
+
raise ValidationError("value", f"Expected bytes, got {type(value).__name__}")
|
|
79
|
+
|
|
80
|
+
def get_secret_value(self) -> bytes:
|
|
81
|
+
"""Get the actual secret value."""
|
|
82
|
+
return self._secret_value
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return "SecretBytes(b'**********')"
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
return '**********'
|
|
89
|
+
|
|
90
|
+
def __len__(self) -> int:
|
|
91
|
+
return len(self._secret_value)
|
|
92
|
+
|
|
93
|
+
def __eq__(self, other: object) -> bool:
|
|
94
|
+
if isinstance(other, SecretBytes):
|
|
95
|
+
return self._secret_value == other._secret_value
|
|
96
|
+
return NotImplemented
|
|
97
|
+
|
|
98
|
+
def __hash__(self) -> int:
|
|
99
|
+
return hash(self._secret_value)
|
|
100
|
+
|
|
101
|
+
def __bool__(self) -> bool:
|
|
102
|
+
return bool(self._secret_value)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ["SecretStr", "SecretBytes"]
|