or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-data.mdconfiguration.mdcore-parsing.mdcustom-parsers.mdfile-secrets.mdframework-integration.mdindex.mdspecialized-types.mdvalidation.md

custom-parsers.mddocs/

0

# Custom Parsers and Validation

1

2

Registration and management of custom parsing methods with marshmallow field integration, validation customization, and extensible parser patterns for specialized data types.

3

4

## Capabilities

5

6

### Custom Parser Registration

7

8

Register custom parsing functions to extend environs with domain-specific data types and validation logic.

9

10

```python { .api }

11

def add_parser(self, name: str, func: Callable):

12

"""

13

Register a new parser method with the given name.

14

15

Parameters:

16

- name: str, method name for the parser (must not conflict with existing methods)

17

- func: callable, parser function that receives the raw string value

18

19

Raises:

20

ParserConflictError: If name conflicts with existing method

21

22

Notes:

23

The parser function should accept a string value and return the parsed result.

24

It can raise EnvError or ValidationError for invalid input.

25

"""

26

```

27

28

Usage examples:

29

30

```python

31

import os

32

from environs import env, EnvError

33

import re

34

35

# Custom email validator parser

36

def parse_email(value):

37

"""Parse and validate email addresses."""

38

email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

39

if not re.match(email_pattern, value):

40

raise EnvError(f"Invalid email format: {value}")

41

return value.lower()

42

43

# Register the custom parser

44

env.add_parser("email", parse_email)

45

46

# Use the custom parser

47

os.environ["ADMIN_EMAIL"] = "ADMIN@EXAMPLE.COM"

48

admin_email = env.email("ADMIN_EMAIL") # => "admin@example.com"

49

50

# Custom URL slug parser

51

def parse_slug(value):

52

"""Parse and validate URL slugs."""

53

slug_pattern = r'^[a-z0-9]+(?:-[a-z0-9]+)*$'

54

if not re.match(slug_pattern, value):

55

raise EnvError(f"Invalid slug format: {value}")

56

return value

57

58

env.add_parser("slug", parse_slug)

59

60

os.environ["ARTICLE_SLUG"] = "my-awesome-article"

61

slug = env.slug("ARTICLE_SLUG") # => "my-awesome-article"

62

63

# Custom coordinate parser

64

def parse_coordinates(value):

65

"""Parse latitude,longitude coordinates."""

66

try:

67

lat_str, lon_str = value.split(',')

68

lat, lon = float(lat_str.strip()), float(lon_str.strip())

69

if not (-90 <= lat <= 90):

70

raise ValueError("Latitude must be between -90 and 90")

71

if not (-180 <= lon <= 180):

72

raise ValueError("Longitude must be between -180 and 180")

73

return (lat, lon)

74

except (ValueError, TypeError) as e:

75

raise EnvError(f"Invalid coordinates format: {value}") from e

76

77

env.add_parser("coordinates", parse_coordinates)

78

79

os.environ["LOCATION"] = "40.7128, -74.0060"

80

location = env.coordinates("LOCATION") # => (40.7128, -74.0060)

81

```

82

83

### Parser Decorator

84

85

Use a decorator to register custom parsers for cleaner syntax and better organization.

86

87

```python { .api }

88

def parser_for(self, name: str):

89

"""

90

Decorator that registers a new parser method with the given name.

91

92

Parameters:

93

- name: str, method name for the parser

94

95

Returns:

96

Decorator function that registers the decorated function as a parser

97

98

Notes:

99

The decorated function should accept a string value and return parsed result.

100

"""

101

```

102

103

Usage examples:

104

105

```python

106

import os

107

from environs import env, EnvError

108

import ipaddress

109

110

# Register IP address parser using decorator

111

@env.parser_for("ip_address")

112

def parse_ip_address(value):

113

"""Parse and validate IP addresses."""

114

try:

115

return ipaddress.ip_address(value)

116

except ipaddress.AddressValueError as e:

117

raise EnvError(f"Invalid IP address: {value}") from e

118

119

# Register network parser

120

@env.parser_for("network")

121

def parse_network(value):

122

"""Parse and validate network CIDR notation."""

123

try:

124

return ipaddress.ip_network(value, strict=False)

125

except ipaddress.NetmaskValueError as e:

126

raise EnvError(f"Invalid network: {value}") from e

127

128

# Use custom parsers

129

os.environ["SERVER_IP"] = "192.168.1.100"

130

os.environ["ALLOWED_NETWORK"] = "10.0.0.0/8"

131

132

server_ip = env.ip_address("SERVER_IP") # => IPv4Address('192.168.1.100')

133

allowed_net = env.network("ALLOWED_NETWORK") # => IPv4Network('10.0.0.0/8')

134

135

print(f"Server IP: {server_ip}")

136

print(f"Network contains server: {server_ip in allowed_net}")

137

138

# Complex data structure parser

139

@env.parser_for("connection_string")

140

def parse_connection_string(value):

141

"""Parse database connection strings."""

142

# Format: "host:port/database?param1=value1&param2=value2"

143

try:

144

# Split main parts

145

if '?' in value:

146

main_part, params_part = value.split('?', 1)

147

else:

148

main_part, params_part = value, ""

149

150

# Parse host:port/database

151

host_port, database = main_part.split('/', 1)

152

if ':' in host_port:

153

host, port = host_port.split(':', 1)

154

port = int(port)

155

else:

156

host, port = host_port, 5432

157

158

# Parse parameters

159

params = {}

160

if params_part:

161

for param in params_part.split('&'):

162

key, val = param.split('=', 1)

163

params[key] = val

164

165

return {

166

'host': host,

167

'port': port,

168

'database': database,

169

'params': params

170

}

171

except (ValueError, TypeError) as e:

172

raise EnvError(f"Invalid connection string: {value}") from e

173

174

# Use complex parser

175

os.environ["DATABASE_URL"] = "localhost:5432/myapp?ssl=require&timeout=30"

176

db_config = env.connection_string("DATABASE_URL")

177

# => {'host': 'localhost', 'port': 5432, 'database': 'myapp',

178

# 'params': {'ssl': 'require', 'timeout': '30'}}

179

```

180

181

### Marshmallow Field Integration

182

183

Register parsers from marshmallow fields for advanced validation and serialization capabilities.

184

185

```python { .api }

186

def add_parser_from_field(self, name: str, field_cls: Type[Field]):

187

"""

188

Register a new parser method from a marshmallow Field class.

189

190

Parameters:

191

- name: str, method name for the parser

192

- field_cls: marshmallow Field class or subclass

193

194

Notes:

195

The field class should implement _deserialize method for parsing.

196

Enables full marshmallow validation capabilities and error handling.

197

"""

198

```

199

200

Usage examples:

201

202

```python

203

import os

204

from environs import env

205

import marshmallow as ma

206

from marshmallow import ValidationError

207

import re

208

209

# Custom marshmallow field for phone numbers

210

class PhoneNumberField(ma.fields.Field):

211

"""Field for parsing and validating phone numbers."""

212

213

def _serialize(self, value, attr, obj, **kwargs):

214

if value is None:

215

return None

216

return str(value)

217

218

def _deserialize(self, value, attr, data, **kwargs):

219

if not isinstance(value, str):

220

raise ValidationError("Phone number must be a string")

221

222

# Remove all non-digit characters

223

digits = re.sub(r'\D', '', value)

224

225

# Validate US phone number format (10 digits)

226

if len(digits) != 10:

227

raise ValidationError("Phone number must have 10 digits")

228

229

# Format as (XXX) XXX-XXXX

230

return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"

231

232

# Register marshmallow field as parser

233

env.add_parser_from_field("phone", PhoneNumberField)

234

235

# Use field-based parser

236

os.environ["SUPPORT_PHONE"] = "1-800-555-0123"

237

support_phone = env.phone("SUPPORT_PHONE") # => "(800) 555-0123"

238

239

# Custom field with validation options

240

class CreditCardField(ma.fields.Field):

241

"""Field for parsing credit card numbers with validation."""

242

243

def _deserialize(self, value, attr, data, **kwargs):

244

if not isinstance(value, str):

245

raise ValidationError("Credit card number must be a string")

246

247

# Remove spaces and dashes

248

digits = re.sub(r'[\s-]', '', value)

249

250

# Validate digits only

251

if not digits.isdigit():

252

raise ValidationError("Credit card number must contain only digits")

253

254

# Validate length (13-19 digits for most cards)

255

if not (13 <= len(digits) <= 19):

256

raise ValidationError("Credit card number must be 13-19 digits")

257

258

# Simple Luhn algorithm check

259

def luhn_check(card_num):

260

def digits_of(n):

261

return [int(d) for d in str(n)]

262

263

digits = digits_of(card_num)

264

odd_digits = digits[-1::-2]

265

even_digits = digits[-2::-2]

266

checksum = sum(odd_digits)

267

for d in even_digits:

268

checksum += sum(digits_of(d * 2))

269

return checksum % 10 == 0

270

271

if not luhn_check(digits):

272

raise ValidationError("Invalid credit card number")

273

274

# Mask all but last 4 digits

275

masked = '*' * (len(digits) - 4) + digits[-4:]

276

return masked

277

278

env.add_parser_from_field("credit_card", CreditCardField)

279

280

# Use with validation

281

os.environ["PAYMENT_CARD"] = "4532 1488 0343 6467" # Valid test number

282

card = env.credit_card("PAYMENT_CARD") # => "************6467"

283

```

284

285

### Validation Helpers

286

287

Use environs' built-in validation functions and marshmallow validators for robust input validation.

288

289

```python { .api }

290

# Import validation functions

291

from environs import validate

292

293

# Common validators available:

294

# validate.Length(min=None, max=None)

295

# validate.Range(min=None, max=None)

296

# validate.OneOf(choices)

297

# validate.Regexp(regex)

298

# validate.URL()

299

# validate.Email()

300

```

301

302

Usage examples:

303

304

```python

305

import os

306

from environs import env, validate

307

308

# String length validation

309

os.environ["USERNAME"] = "john_doe"

310

username = env.str("USERNAME", validate=validate.Length(min=3, max=20))

311

312

# Numeric range validation

313

os.environ["PORT"] = "8080"

314

port = env.int("PORT", validate=validate.Range(min=1024, max=65535))

315

316

# Choice validation

317

os.environ["LOG_LEVEL"] = "INFO"

318

log_level = env.str("LOG_LEVEL", validate=validate.OneOf(["DEBUG", "INFO", "WARNING", "ERROR"]))

319

320

# Regular expression validation

321

os.environ["VERSION"] = "1.2.3"

322

version = env.str("VERSION", validate=validate.Regexp(r'^\d+\.\d+\.\d+$'))

323

324

# Multiple validators

325

os.environ["API_KEY"] = "sk_live_123456789abcdef"

326

api_key = env.str("API_KEY", validate=[

327

validate.Length(min=20),

328

validate.Regexp(r'^sk_(live|test)_[a-zA-Z0-9]+$')

329

])

330

331

# Custom validation function

332

def validate_positive_even(value):

333

"""Validate that number is positive and even."""

334

if value <= 0:

335

raise ValidationError("Value must be positive")

336

if value % 2 != 0:

337

raise ValidationError("Value must be even")

338

return value

339

340

os.environ["BATCH_SIZE"] = "100"

341

batch_size = env.int("BATCH_SIZE", validate=validate_positive_even)

342

343

# Combining built-in and custom validators

344

os.environ["THREAD_COUNT"] = "8"

345

thread_count = env.int("THREAD_COUNT", validate=[

346

validate.Range(min=1, max=32),

347

validate_positive_even

348

])

349

```

350

351

## Error Handling

352

353

Handle parser conflicts and validation errors appropriately in custom parser registration.

354

355

```python

356

from environs import env, ParserConflictError, EnvValidationError

357

358

# Handle parser name conflicts

359

try:

360

env.add_parser("str", lambda x: x) # Conflicts with built-in

361

except ParserConflictError as e:

362

print(f"Parser conflict: {e}")

363

# Use a different name

364

env.add_parser("custom_str", lambda x: x.upper())

365

366

# Handle validation errors in custom parsers

367

@env.parser_for("positive_int")

368

def parse_positive_int(value):

369

try:

370

num = int(value)

371

if num <= 0:

372

raise EnvError("Value must be positive")

373

return num

374

except ValueError as e:

375

raise EnvError(f"Invalid integer: {value}") from e

376

377

# Use with error handling

378

os.environ["INVALID_POSITIVE"] = "-5"

379

try:

380

value = env.positive_int("INVALID_POSITIVE")

381

except EnvValidationError as e:

382

print(f"Validation failed: {e}")

383

```

384

385

## Types

386

387

```python { .api }

388

from typing import Callable, Any, Type

389

from marshmallow.fields import Field

390

from marshmallow import ValidationError

391

392

ParserFunction = Callable[[str], Any]

393

ValidatorFunction = Callable[[Any], Any]

394

```