or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

active-directory.mdasync-frameworks.mdconnection-pooling.mdcore-ldap.mdindex.mdldif-support.mdutilities-errors.md

ldif-support.mddocs/

0

# LDIF Format Support

1

2

Read and write LDAP Data Interchange Format (LDIF) files for data import/export, backup operations, and batch processing. LDIF support includes parsing, writing, change records, and URL loading capabilities.

3

4

## Capabilities

5

6

### LDIF Reader

7

8

Parse LDIF files into LDAP entries with support for continuation lines, comments, and external URL references.

9

10

```python { .api }

11

from bonsai.ldif import LDIFReader, LDIFError

12

13

class LDIFReader:

14

def __init__(

15

self,

16

input_file: TextIO,

17

autoload: bool = True,

18

max_length: int = 76

19

) -> None:

20

"""

21

Initialize LDIF reader for parsing LDIF format files.

22

23

Parameters:

24

- input_file: File-like object in text mode containing LDIF data

25

- autoload: Allow automatic loading of external URL sources

26

- max_length: Maximum line length allowed in LDIF file

27

"""

28

29

def parse(self) -> Iterator[Tuple[str, Dict[str, List[Union[str, bytes]]]]]:

30

"""

31

Parse LDIF file and yield entries.

32

33

Yields:

34

Tuple of (dn, attributes_dict) for each entry

35

36

Where attributes_dict maps attribute names to lists of values.

37

Binary values are returned as bytes, text values as strings.

38

"""

39

40

def parse_entry_records(self) -> Iterator[LDAPEntry]:

41

"""

42

Parse LDIF file and yield LDAPEntry objects.

43

44

Yields:

45

LDAPEntry objects for each entry in the LDIF file

46

"""

47

48

def parse_change_records(self) -> Iterator[Tuple[str, str, Dict[str, Any]]]:

49

"""

50

Parse LDIF change records (add, modify, delete, modrdn operations).

51

52

Yields:

53

Tuple of (dn, changetype, change_data) for each change record

54

55

Where:

56

- dn: Distinguished name of the entry

57

- changetype: Operation type ("add", "modify", "delete", "modrdn")

58

- change_data: Dictionary with operation-specific data

59

"""

60

61

def parse_entries_and_change_records(self) -> Iterator[Union[

62

Tuple[str, Dict[str, List[Union[str, bytes]]]],

63

Tuple[str, str, Dict[str, Any]]

64

]]:

65

"""

66

Parse LDIF file yielding both entries and change records.

67

68

Yields:

69

Mixed entries and change records as tuples

70

"""

71

72

@property

73

def version(self) -> Optional[int]:

74

"""LDIF version number from version line."""

75

76

@property

77

def autoload(self) -> bool:

78

"""Whether automatic URL loading is enabled."""

79

80

@property

81

def max_length(self) -> int:

82

"""Maximum allowed line length."""

83

84

def set_resource_handler(self, scheme: str, handler: Callable[[str], bytes]) -> None:

85

"""

86

Set custom handler for loading external resources by URL scheme.

87

88

Parameters:

89

- scheme: URL scheme (e.g., "http", "ftp")

90

- handler: Function that takes URL string and returns bytes

91

"""

92

93

def get_resource_handler(self, scheme: str) -> Optional[Callable[[str], bytes]]:

94

"""Get resource handler for URL scheme."""

95

```

96

97

### LDIF Writer

98

99

Write LDAP entries and change records to LDIF format files with proper encoding and formatting.

100

101

```python { .api }

102

from bonsai.ldif import LDIFWriter

103

104

class LDIFWriter:

105

def __init__(

106

self,

107

output_file: TextIO,

108

base64_attrs: Optional[List[str]] = None,

109

cols: int = 76

110

) -> None:

111

"""

112

Initialize LDIF writer for creating LDIF format files.

113

114

Parameters:

115

- output_file: File-like object in text mode for writing LDIF data

116

- base64_attrs: List of attribute names to always base64 encode

117

- cols: Maximum column width for line wrapping

118

"""

119

120

def write_entry(self, dn: str, entry: Union[Dict[str, Any], LDAPEntry]) -> None:

121

"""

122

Write LDAP entry to LDIF file.

123

124

Parameters:

125

- dn: Distinguished name of the entry

126

- entry: Dictionary or LDAPEntry with attributes and values

127

"""

128

129

def write_entries(self, entries: Iterable[Union[LDAPEntry, Tuple[str, Dict]]]) -> None:

130

"""

131

Write multiple LDAP entries to LDIF file.

132

133

Parameters:

134

- entries: Iterable of LDAPEntry objects or (dn, attributes) tuples

135

"""

136

137

def write_change_record(

138

self,

139

dn: str,

140

changetype: str,

141

change_data: Dict[str, Any]

142

) -> None:

143

"""

144

Write LDIF change record to file.

145

146

Parameters:

147

- dn: Distinguished name

148

- changetype: Operation type ("add", "modify", "delete", "modrdn")

149

- change_data: Change-specific data dictionary

150

"""

151

152

def write_version(self, version: int = 1) -> None:

153

"""

154

Write LDIF version line.

155

156

Parameters:

157

- version: LDIF version number (default: 1)

158

"""

159

160

def write_comment(self, comment: str) -> None:

161

"""

162

Write comment line to LDIF file.

163

164

Parameters:

165

- comment: Comment text (without # prefix)

166

"""

167

168

def write_dn(self, dn: str) -> None:

169

"""

170

Write DN line to LDIF file.

171

172

Parameters:

173

- dn: Distinguished name

174

"""

175

176

def write_attribute(self, name: str, value: Union[str, bytes]) -> None:

177

"""

178

Write single attribute line to LDIF file.

179

180

Parameters:

181

- name: Attribute name

182

- value: Attribute value (string or bytes)

183

"""

184

185

def flush(self) -> None:

186

"""Flush output buffer to file."""

187

188

@property

189

def cols(self) -> int:

190

"""Maximum column width for line wrapping."""

191

192

@property

193

def base64_attrs(self) -> Optional[List[str]]:

194

"""List of attributes that are always base64 encoded."""

195

```

196

197

### LDIF Exceptions

198

199

Specialized exception for LDIF parsing and writing errors.

200

201

```python { .api }

202

from bonsai.ldif import LDIFError

203

204

class LDIFError(LDAPError):

205

"""Exception raised during LDIF file reading or writing operations."""

206

207

@property

208

def code(self) -> int:

209

"""Error code (-300 for LDIF errors)."""

210

```

211

212

## Usage Examples

213

214

### Reading LDIF Files

215

216

```python

217

from bonsai.ldif import LDIFReader

218

import io

219

220

# Read LDIF from file

221

with open('directory_export.ldif', 'r', encoding='utf-8') as f:

222

reader = LDIFReader(f)

223

224

# Parse entries

225

for dn, attributes in reader.parse():

226

print(f"Entry: {dn}")

227

for attr_name, values in attributes.items():

228

print(f" {attr_name}: {values}")

229

print()

230

231

# Read LDIF from string

232

ldif_data = '''

233

version: 1

234

235

dn: cn=John Doe,ou=people,dc=example,dc=com

236

objectClass: person

237

objectClass: organizationalPerson

238

cn: John Doe

239

sn: Doe

240

givenName: John

241

mail: john.doe@example.com

242

description: Software Engineer

243

244

dn: cn=Jane Smith,ou=people,dc=example,dc=com

245

objectClass: person

246

objectClass: organizationalPerson

247

cn: Jane Smith

248

sn: Smith

249

givenName: Jane

250

mail: jane.smith@example.com

251

telephoneNumber: +1-555-0123

252

'''

253

254

reader = LDIFReader(io.StringIO(ldif_data))

255

print(f"LDIF Version: {reader.version}")

256

257

# Parse into LDAPEntry objects

258

for entry in reader.parse_entry_records():

259

print(f"Entry DN: {entry.dn}")

260

print(f"Common Name: {entry['cn'][0]}")

261

print(f"Object Classes: {', '.join(entry['objectClass'])}")

262

if 'mail' in entry:

263

print(f"Email: {entry['mail'][0]}")

264

print()

265

```

266

267

### Writing LDIF Files

268

269

```python

270

from bonsai.ldif import LDIFWriter

271

from bonsai import LDAPEntry

272

import io

273

274

# Create LDIF writer

275

output = io.StringIO()

276

writer = LDIFWriter(output)

277

278

# Write version

279

writer.write_version(1)

280

281

# Write comment

282

writer.write_comment("Directory export created on 2024-01-15")

283

284

# Create and write entries

285

people = [

286

{

287

'dn': 'cn=Alice Johnson,ou=people,dc=example,dc=com',

288

'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson'],

289

'cn': 'Alice Johnson',

290

'sn': 'Johnson',

291

'givenName': 'Alice',

292

'mail': 'alice.johnson@example.com',

293

'employeeNumber': '12345'

294

},

295

{

296

'dn': 'cn=Bob Wilson,ou=people,dc=example,dc=com',

297

'objectClass': ['person', 'organizationalPerson'],

298

'cn': 'Bob Wilson',

299

'sn': 'Wilson',

300

'givenName': 'Bob',

301

'telephoneNumber': '+1-555-0456'

302

}

303

]

304

305

for person in people:

306

dn = person.pop('dn')

307

writer.write_entry(dn, person)

308

309

# Get LDIF output

310

ldif_output = output.getvalue()

311

print(ldif_output)

312

313

# Write to file

314

with open('people_export.ldif', 'w', encoding='utf-8') as f:

315

writer = LDIFWriter(f)

316

writer.write_version(1)

317

318

# Write LDAPEntry objects directly

319

for person in people:

320

entry = LDAPEntry(person['dn'])

321

for attr, value in person.items():

322

if attr != 'dn':

323

entry[attr] = value

324

writer.write_entry(str(entry.dn), entry)

325

```

326

327

### Processing Change Records

328

329

```python

330

from bonsai.ldif import LDIFReader, LDIFWriter

331

import io

332

333

# LDIF with change records

334

change_ldif = '''

335

version: 1

336

337

# Add new user

338

dn: cn=New User,ou=people,dc=example,dc=com

339

changetype: add

340

objectClass: person

341

objectClass: organizationalPerson

342

cn: New User

343

sn: User

344

givenName: New

345

346

# Modify existing user

347

dn: cn=John Doe,ou=people,dc=example,dc=com

348

changetype: modify

349

replace: mail

350

mail: john.doe.new@example.com

351

-

352

add: telephoneNumber

353

telephoneNumber: +1-555-9999

354

-

355

356

# Delete user

357

dn: cn=Old User,ou=people,dc=example,dc=com

358

changetype: delete

359

360

# Rename user

361

dn: cn=Jane Smith,ou=people,dc=example,dc=com

362

changetype: modrdn

363

newrdn: cn=Jane Smith-Brown

364

deleteoldrdn: 1

365

newsuperior: ou=married,ou=people,dc=example,dc=com

366

'''

367

368

# Parse change records

369

reader = LDIFReader(io.StringIO(change_ldif))

370

371

for change in reader.parse_change_records():

372

dn, changetype, change_data = change

373

print(f"Change: {changetype} on {dn}")

374

375

if changetype == 'add':

376

print(" Adding new entry with attributes:")

377

for attr, values in change_data.items():

378

print(f" {attr}: {values}")

379

380

elif changetype == 'modify':

381

print(" Modifications:")

382

for mod in change_data['modifications']:

383

op, attr, values = mod

384

print(f" {op} {attr}: {values}")

385

386

elif changetype == 'delete':

387

print(" Deleting entry")

388

389

elif changetype == 'modrdn':

390

print(f" New RDN: {change_data['newrdn']}")

391

print(f" Delete old RDN: {change_data['deleteoldrdn']}")

392

if 'newsuperior' in change_data:

393

print(f" New superior: {change_data['newsuperior']}")

394

print()

395

```

396

397

### Binary Data Handling

398

399

```python

400

from bonsai.ldif import LDIFReader, LDIFWriter

401

import base64

402

import io

403

404

# LDIF with binary data (base64 encoded)

405

binary_ldif = '''

406

version: 1

407

408

dn: cn=user-with-photo,ou=people,dc=example,dc=com

409

objectClass: person

410

objectClass: organizationalPerson

411

cn: user-with-photo

412

sn: Photo

413

jpegPhoto:: /9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNA...

414

userCertificate:: MIIDXTCCAkWgAwIBAgIJAKoK...

415

'''

416

417

# Read binary data

418

reader = LDIFReader(io.StringIO(binary_ldif))

419

420

for dn, attributes in reader.parse():

421

print(f"Entry: {dn}")

422

for attr, values in attributes.items():

423

if attr in ['jpegPhoto', 'userCertificate']:

424

# Binary attributes are returned as bytes

425

for value in values:

426

if isinstance(value, bytes):

427

print(f" {attr}: <binary data, {len(value)} bytes>")

428

else:

429

print(f" {attr}: {value}")

430

else:

431

print(f" {attr}: {values}")

432

433

# Write binary data

434

output = io.StringIO()

435

writer = LDIFWriter(output, base64_attrs=['jpegPhoto', 'userCertificate'])

436

437

# Binary data will be automatically base64 encoded

438

binary_data = b'\x89PNG\r\n\x1a\n...' # PNG image data

439

writer.write_entry('cn=test,dc=example,dc=com', {

440

'objectClass': ['person'],

441

'cn': 'test',

442

'sn': 'user',

443

'jpegPhoto': binary_data # Will be base64 encoded automatically

444

})

445

446

print(output.getvalue())

447

```

448

449

### Batch Import/Export Operations

450

451

```python

452

from bonsai import LDAPClient, LDAPEntry

453

from bonsai.ldif import LDIFReader, LDIFWriter

454

455

def export_to_ldif(client, base_dn, output_file):

456

"""Export LDAP directory tree to LDIF file."""

457

with client.connect() as conn:

458

# Search entire subtree

459

results = conn.search(base_dn, 2) # SUBTREE scope

460

461

with open(output_file, 'w', encoding='utf-8') as f:

462

writer = LDIFWriter(f)

463

writer.write_version(1)

464

writer.write_comment(f"Export of {base_dn} subtree")

465

466

for entry in results:

467

writer.write_entry(str(entry.dn), entry)

468

469

print(f"Exported {len(results)} entries to {output_file}")

470

471

def import_from_ldif(client, input_file):

472

"""Import entries from LDIF file to LDAP directory."""

473

with open(input_file, 'r', encoding='utf-8') as f:

474

reader = LDIFReader(f)

475

476

with client.connect() as conn:

477

imported_count = 0

478

479

for entry in reader.parse_entry_records():

480

try:

481

conn.add(entry)

482

imported_count += 1

483

print(f"Imported: {entry.dn}")

484

485

except Exception as e:

486

print(f"Failed to import {entry.dn}: {e}")

487

488

print(f"Successfully imported {imported_count} entries")

489

490

# Usage

491

client = LDAPClient("ldap://localhost")

492

client.set_credentials("SIMPLE", user="cn=admin,dc=example,dc=com", password="secret")

493

494

# Export directory to LDIF

495

export_to_ldif(client, "ou=people,dc=example,dc=com", "people_backup.ldif")

496

497

# Import from LDIF

498

import_from_ldif(client, "people_restore.ldif")

499

```

500

501

### Custom URL Handlers

502

503

```python

504

from bonsai.ldif import LDIFReader

505

import io

506

import requests

507

508

def http_handler(url):

509

"""Custom handler for HTTP URLs."""

510

response = requests.get(url)

511

response.raise_for_status()

512

return response.content

513

514

def ftp_handler(url):

515

"""Custom handler for FTP URLs."""

516

# Implementation for FTP URL loading

517

pass

518

519

# LDIF with external URL references

520

url_ldif = '''

521

version: 1

522

523

dn: cn=user-with-cert,ou=people,dc=example,dc=com

524

objectClass: person

525

cn: user-with-cert

526

sn: Cert

527

userCertificate:< http://example.com/certs/user.crt

528

jpegPhoto:< file:///path/to/photo.jpg

529

'''

530

531

reader = LDIFReader(io.StringIO(url_ldif), autoload=True)

532

533

# Register custom URL handlers

534

reader.set_resource_handler('http', http_handler)

535

reader.set_resource_handler('https', http_handler)

536

reader.set_resource_handler('ftp', ftp_handler)

537

538

# Parse will automatically load external resources

539

for dn, attributes in reader.parse():

540

print(f"Entry: {dn}")

541

for attr, values in attributes.items():

542

if attr in ['userCertificate', 'jpegPhoto']:

543

for value in values:

544

if isinstance(value, bytes):

545

print(f" {attr}: <loaded {len(value)} bytes from URL>")

546

else:

547

print(f" {attr}: {values}")

548

```