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/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"]