Skip to content

Commit afe72e3

Browse files
feat: add advanced validators - IP, MAC, dates, storage, and more
1 parent 6d81cfe commit afe72e3

File tree

8 files changed

+1941
-144
lines changed

8 files changed

+1941
-144
lines changed

README.md

Lines changed: 424 additions & 140 deletions
Large diffs are not rendered by default.

examples/07_advanced_validators.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
"""Example demonstrating advanced validators.
2+
3+
This example shows advanced validators:
4+
- IPv4Address, IPv6Address, IPvAnyAddress for IP validation
5+
- MacAddress for MAC address validation
6+
- ConStr for constrained strings
7+
- ByteSize for storage size parsing
8+
- PastDate, FutureDate for date validation
9+
"""
10+
11+
import os
12+
from datetime import date, timedelta
13+
14+
from msgspec_ext import (
15+
BaseSettings,
16+
ByteSize,
17+
ConStr,
18+
FutureDate,
19+
IPv4Address,
20+
IPv6Address,
21+
IPvAnyAddress,
22+
MacAddress,
23+
PastDate,
24+
SettingsConfigDict,
25+
)
26+
27+
28+
# Example 1: IP Address validation
29+
class NetworkSettings(BaseSettings):
30+
"""Settings with IP address validation."""
31+
32+
model_config = SettingsConfigDict(env_prefix="NET_")
33+
34+
server_ipv4: IPv4Address
35+
server_ipv6: IPv6Address
36+
proxy_ip: IPvAnyAddress # Accepts both IPv4 and IPv6
37+
38+
39+
# Example 2: MAC Address validation
40+
class DeviceSettings(BaseSettings):
41+
"""Settings with MAC address validation."""
42+
43+
model_config = SettingsConfigDict(env_prefix="DEVICE_")
44+
45+
primary_mac: MacAddress
46+
backup_mac: MacAddress
47+
48+
49+
# Example 3: Constrained String validation
50+
class UsernameSettings(BaseSettings):
51+
"""Settings with constrained strings."""
52+
53+
model_config = SettingsConfigDict(env_prefix="USER_")
54+
55+
username: ConStr # Can add min_length, max_length, pattern constraints
56+
57+
58+
# Example 4: ByteSize validation
59+
class StorageSettings(BaseSettings):
60+
"""Settings with storage size validation."""
61+
62+
model_config = SettingsConfigDict(env_prefix="STORAGE_")
63+
64+
max_file_size: ByteSize
65+
cache_size: ByteSize
66+
upload_limit: ByteSize
67+
68+
69+
# Example 5: Date validation
70+
class EventSettings(BaseSettings):
71+
"""Settings with date validation."""
72+
73+
model_config = SettingsConfigDict(env_prefix="EVENT_")
74+
75+
launch_date: FutureDate # Must be in the future
76+
founding_date: PastDate # Must be in the past
77+
78+
79+
# Example 6: Combined advanced validators
80+
class AppSettings(BaseSettings):
81+
"""Real-world app settings with advanced validators."""
82+
83+
# Network
84+
api_server: IPv4Address
85+
dns_server: IPvAnyAddress
86+
87+
# MAC Address
88+
server_mac: MacAddress
89+
90+
# Storage
91+
max_upload: ByteSize
92+
cache_limit: ByteSize
93+
94+
# Dates
95+
release_date: FutureDate
96+
97+
98+
def main(): # noqa: PLR0915
99+
print("=" * 60)
100+
print("msgspec-ext Advanced Validators Demo")
101+
print("=" * 60)
102+
103+
# Example 1: IP Address validation
104+
print("\n1. IP Address Validation")
105+
print("-" * 60)
106+
107+
os.environ.update(
108+
{
109+
"NET_SERVER_IPV4": "192.168.1.100",
110+
"NET_SERVER_IPV6": "2001:db8::1",
111+
"NET_PROXY_IP": "10.0.0.1", # Can be IPv4 or IPv6
112+
}
113+
)
114+
115+
net_settings = NetworkSettings()
116+
print(f"Server IPv4: {net_settings.server_ipv4}")
117+
print(f"Server IPv6: {net_settings.server_ipv6}")
118+
print(f"Proxy IP: {net_settings.proxy_ip}")
119+
120+
# Try invalid IPv4
121+
try:
122+
IPv4Address("256.1.1.1")
123+
except ValueError as e:
124+
print(f"✓ IPv4 validation works: {e}")
125+
126+
# Try invalid IPv6
127+
try:
128+
IPv6Address("gggg::1")
129+
except ValueError as e:
130+
print(f"✓ IPv6 validation works: {e}")
131+
132+
# Example 2: MAC Address validation
133+
print("\n2. MAC Address Validation")
134+
print("-" * 60)
135+
136+
os.environ.update(
137+
{
138+
"DEVICE_PRIMARY_MAC": "00:1B:44:11:3A:B7",
139+
"DEVICE_BACKUP_MAC": "001B.4411.3AB7", # Different format
140+
}
141+
)
142+
143+
device_settings = DeviceSettings()
144+
print(f"Primary MAC: {device_settings.primary_mac}")
145+
print(f"Backup MAC: {device_settings.backup_mac}")
146+
147+
# Try invalid MAC
148+
try:
149+
MacAddress("GG:1B:44:11:3A:B7")
150+
except ValueError as e:
151+
print(f"✓ MAC validation works: {e}")
152+
153+
# Example 3: Constrained String
154+
print("\n3. Constrained String Validation")
155+
print("-" * 60)
156+
157+
# ConStr with no constraints
158+
username1 = ConStr("john_doe")
159+
print(f"Username (no constraints): {username1}")
160+
161+
# ConStr with min/max length
162+
username2 = ConStr("alice", min_length=3, max_length=20)
163+
print(f"Username (with length): {username2}")
164+
165+
# ConStr with pattern
166+
username3 = ConStr("bob123", pattern=r"^[a-z0-9]+$")
167+
print(f"Username (with pattern): {username3}")
168+
169+
# Try too short
170+
try:
171+
ConStr("ab", min_length=5)
172+
except ValueError as e:
173+
print(f"✓ Min length validation works: {e}")
174+
175+
# Try pattern mismatch
176+
try:
177+
ConStr("ABC", pattern=r"^[a-z]+$")
178+
except ValueError as e:
179+
print(f"✓ Pattern validation works: {e}")
180+
181+
# Example 4: ByteSize validation
182+
print("\n4. Byte Size Validation")
183+
print("-" * 60)
184+
185+
os.environ.update(
186+
{
187+
"STORAGE_MAX_FILE_SIZE": "10MB",
188+
"STORAGE_CACHE_SIZE": "500MB",
189+
"STORAGE_UPLOAD_LIMIT": "1GB",
190+
}
191+
)
192+
193+
storage_settings = StorageSettings()
194+
print(f"Max File Size: {storage_settings.max_file_size} bytes = 10MB")
195+
print(f"Cache Size: {storage_settings.cache_size} bytes = 500MB")
196+
print(f"Upload Limit: {storage_settings.upload_limit} bytes = 1GB")
197+
198+
# Different units
199+
print(f"\n1KB = {ByteSize('1KB')} bytes")
200+
print(f"1MB = {ByteSize('1MB')} bytes")
201+
print(f"1GB = {ByteSize('1GB')} bytes")
202+
print(f"1KiB = {ByteSize('1KiB')} bytes (binary)")
203+
print(f"1MiB = {ByteSize('1MiB')} bytes (binary)")
204+
205+
# Try invalid size
206+
try:
207+
ByteSize("100XB")
208+
except ValueError as e:
209+
print(f"✓ ByteSize validation works: {e}")
210+
211+
# Example 5: Date validation
212+
print("\n5. Past/Future Date Validation")
213+
print("-" * 60)
214+
215+
yesterday = date.today() - timedelta(days=1) # noqa: DTZ011
216+
tomorrow = date.today() + timedelta(days=1) # noqa: DTZ011
217+
218+
os.environ.update(
219+
{
220+
"EVENT_FOUNDING_DATE": yesterday.isoformat(),
221+
"EVENT_LAUNCH_DATE": tomorrow.isoformat(),
222+
}
223+
)
224+
225+
event_settings = EventSettings()
226+
print(f"Founding Date (past): {event_settings.founding_date}")
227+
print(f"Launch Date (future): {event_settings.launch_date}")
228+
229+
# Try future date as past (invalid)
230+
try:
231+
PastDate(tomorrow)
232+
except ValueError as e:
233+
print(f"✓ PastDate validation works: {e}")
234+
235+
# Try past date as future (invalid)
236+
try:
237+
FutureDate(yesterday)
238+
except ValueError as e:
239+
print(f"✓ FutureDate validation works: {e}")
240+
241+
# Example 6: Real-World Combined validators
242+
print("\n6. Real-World App Settings")
243+
print("-" * 60)
244+
245+
os.environ.update(
246+
{
247+
"API_SERVER": "192.168.1.50",
248+
"DNS_SERVER": "8.8.8.8",
249+
"SERVER_MAC": "AA:BB:CC:DD:EE:FF",
250+
"MAX_UPLOAD": "50MB",
251+
"CACHE_LIMIT": "1GB",
252+
"RELEASE_DATE": tomorrow.isoformat(),
253+
}
254+
)
255+
256+
app_settings = AppSettings()
257+
print(f"API Server: {app_settings.api_server}")
258+
print(f"DNS Server: {app_settings.dns_server}")
259+
print(f"Server MAC: {app_settings.server_mac}")
260+
print(f"Max Upload: {app_settings.max_upload} bytes")
261+
print(f"Cache Limit: {app_settings.cache_limit} bytes")
262+
print(f"Release Date: {app_settings.release_date}")
263+
264+
print("\n" + "=" * 60)
265+
print("All advanced validator examples completed successfully!")
266+
print("=" * 60)
267+
268+
269+
if __name__ == "__main__":
270+
main()

pyproject.toml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
55
[project]
66
name = "msgspec-ext"
77
dynamic = ["version"]
8-
description = "Fast settings management using msgspec"
8+
description = "High-performance settings management and validation library extending msgspec"
99
readme = "README.md"
1010
license = "MIT"
1111
authors = [
@@ -15,10 +15,24 @@ requires-python = ">=3.10"
1515
dependencies = [
1616
"msgspec>=0.19.0",
1717
]
18+
keywords = [
19+
"msgspec",
20+
"settings",
21+
"configuration",
22+
"validation",
23+
"validators",
24+
"pydantic",
25+
"environment-variables",
26+
"dotenv",
27+
"type-validation",
28+
"fast",
29+
"performance"
30+
]
1831
classifiers = [
1932
"Development Status :: 4 - Beta",
2033
"Intended Audience :: Developers",
2134
"Intended Audience :: Science/Research",
35+
"Intended Audience :: System Administrators",
2236
"License :: OSI Approved :: MIT License",
2337
"Operating System :: MacOS :: MacOS X",
2438
"Operating System :: POSIX :: Linux",
@@ -29,7 +43,9 @@ classifiers = [
2943
"Programming Language :: Python :: 3.12",
3044
"Programming Language :: Python :: 3.13",
3145
"Topic :: Software Development :: Libraries",
32-
"Topic :: Software Development :: Libraries :: Python Modules"
46+
"Topic :: Software Development :: Libraries :: Python Modules",
47+
"Topic :: System :: Systems Administration",
48+
"Typing :: Typed"
3349
]
3450

3551
[tool.hatch.version]
@@ -101,6 +117,7 @@ ignore = [
101117
"TID252", # Allow relative imports
102118
"UP007", # Allow `from typing import Optional` instead of `X | None`
103119
"UP035", # Allow `from typing import Sequence` instead of `Sequence[X]`
120+
"UP038", # Allow isinstance with tuple instead of `X | Y`
104121
"PLR0911", # Allow more than 6 return statements
105122
"PLR0913",
106123
"B904", # Allow raise in except without from
@@ -132,3 +149,4 @@ ban-relative-imports = "all"
132149
"examples/**/*" = ["D", "S101", "S104", "S105", "T201", "F401"]
133150
"benchmark.py" = ["D", "S101", "S105", "T201", "PLC0415", "F841", "C901", "PLR0915"]
134151
"benchmark/**/*" = ["D", "S101", "S104", "S105", "T201", "F401", "S603", "S607", "PLC0415", "ARG001"]
152+
"test_readme_examples.py" = ["D", "S101", "T201", "PLR2004"]

0 commit comments

Comments
 (0)