or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

data-objects.mddecoders.mdexceptions.mdform-parsing.mdindex.mdstreaming-parsers.md

data-objects.mddocs/

0

# Data Objects

1

2

Field and File objects for handling parsed form data with configurable storage options and lifecycle management. These objects provide the data layer for storing and accessing parsed form fields and uploaded files with automatic memory management.

3

4

## Capabilities

5

6

### Field Class

7

8

Represents a parsed form field with name and value, providing simple storage for text-based form data.

9

10

```python { .api }

11

class Field:

12

"""

13

Represents a form field with name and value.

14

"""

15

16

def __init__(self, name: bytes | None):

17

"""

18

Initialize Field object.

19

20

Parameters:

21

- name: Field name as bytes

22

"""

23

24

@classmethod

25

def from_value(cls, name: bytes, value: bytes | None) -> 'Field':

26

"""

27

Create Field from name and value.

28

29

Parameters:

30

- name: Field name as bytes

31

- value: Field value as bytes or None

32

33

Returns:

34

Field instance

35

"""

36

37

def write(self, data: bytes) -> int:

38

"""

39

Write data to field value.

40

41

Parameters:

42

- data: Bytes to append to field value

43

44

Returns:

45

Number of bytes written

46

"""

47

48

def on_data(self, data: bytes) -> int:

49

"""Handle data callback."""

50

51

def on_end(self) -> None:

52

"""Handle end callback."""

53

54

def finalize(self) -> None:

55

"""Finalize field processing."""

56

57

def close(self) -> None:

58

"""Close field and clean up resources."""

59

60

def set_none(self) -> None:

61

"""Set field value to None."""

62

63

def __eq__(self, other: object) -> bool:

64

"""Compare fields for equality."""

65

66

def __repr__(self) -> str:

67

"""String representation of field."""

68

69

# Properties

70

@property

71

def field_name(self) -> bytes | None:

72

"""This property returns the name of the field."""

73

74

@property

75

def value(self) -> bytes | None:

76

"""This property returns the value of the form field."""

77

```

78

79

**Usage Example:**

80

81

```python

82

from python_multipart import Field

83

84

# Create field directly

85

field = Field(b'username')

86

field.write(b'john_doe')

87

field.finalize()

88

89

print(f"Field: {field.field_name.decode('utf-8')} = {field.value.decode('utf-8')}")

90

91

# Create field from value

92

email_field = Field.from_value(b'email', b'john@example.com')

93

print(f"Email: {email_field.value.decode('utf-8')}")

94

95

# Handle empty field

96

empty_field = Field(b'optional_field')

97

empty_field.set_none()

98

print(f"Empty field value: {empty_field.value}") # None

99

```

100

101

### File Class

102

103

Handles writing file data to memory or disk with configurable thresholds and automatic spillover for large files.

104

105

```python { .api }

106

class File:

107

"""

108

Handles file uploads with configurable memory/disk storage.

109

"""

110

111

def __init__(

112

self,

113

file_name: bytes | None,

114

field_name: bytes | None = None,

115

config: FileConfig = {}

116

):

117

"""

118

Initialize File object.

119

120

Parameters:

121

- file_name: Original filename as bytes

122

- field_name: Form field name as bytes

123

- config: Configuration dict for file handling

124

"""

125

126

def flush_to_disk(self) -> None:

127

"""If the file is already on-disk, do nothing. Otherwise, copy from

128

the in-memory buffer to a disk file, and then reassign our internal

129

file object to this new disk file.

130

131

Note that if you attempt to flush a file that is already on-disk, a

132

warning will be logged to this module's logger."""

133

134

def _get_disk_file(self) -> BufferedRandom:

135

"""This function is responsible for getting a file object on-disk for us.

136

137

Creates either a named temporary file or uses configured upload directory

138

based on configuration options.

139

140

Returns:

141

BufferedRandom file object opened for writing

142

143

Raises:

144

FileError: If unable to create or open disk file

145

"""

146

147

def write(self, data: bytes) -> int:

148

"""

149

Write data to file.

150

151

Parameters:

152

- data: Bytes to write

153

154

Returns:

155

Number of bytes written

156

"""

157

158

def on_data(self, data: bytes) -> int:

159

"""This method is a callback that will be called whenever data is

160

written to the File.

161

162

Parameters:

163

- data: The data to write to the file

164

165

Returns:

166

The number of bytes written"""

167

168

def on_end(self) -> None:

169

"""This method is called whenever the File is finalized."""

170

171

def finalize(self) -> None:

172

"""Finalize the form file. This will not close the underlying file,

173

but simply signal that we are finished writing to the File."""

174

175

def close(self) -> None:

176

"""Close the File object. This will actually close the underlying

177

file object (whether it's a io.BytesIO or an actual file object)."""

178

179

def __repr__(self) -> str:

180

"""Return string representation: File(file_name=..., field_name=...)."""

181

182

# Properties

183

@property

184

def field_name(self) -> bytes | None:

185

"""The form field associated with this file. May be None if there isn't

186

one, for example when we have an application/octet-stream upload."""

187

188

@property

189

def file_name(self) -> bytes | None:

190

"""The file name given in the upload request."""

191

192

@property

193

def actual_file_name(self) -> bytes | None:

194

"""The file name that this file is saved as. Will be None if it's not

195

currently saved on disk."""

196

197

@property

198

def file_object(self) -> BytesIO | BufferedRandom:

199

"""The file object that we're currently writing to. Note that this

200

will either be an instance of a io.BytesIO, or a regular file object."""

201

202

@property

203

def size(self) -> int:

204

"""The total size of this file, counted as the number of bytes that

205

currently have been written to the file."""

206

207

@property

208

def in_memory(self) -> bool:

209

"""A boolean representing whether or not this file object is currently

210

stored in-memory or on-disk."""

211

```

212

213

**Configuration Options:**

214

215

```python { .api }

216

FileConfig = {

217

'UPLOAD_DIR': str | bytes | None, # Directory for file uploads

218

'UPLOAD_DELETE_TMP': bool, # Delete temp files automatically

219

'UPLOAD_KEEP_FILENAME': bool, # Keep original filename

220

'UPLOAD_KEEP_EXTENSIONS': bool, # Keep file extensions

221

'MAX_MEMORY_FILE_SIZE': int # Max size before writing to disk

222

}

223

```

224

225

**Usage Example:**

226

227

```python

228

from python_multipart import File

229

import os

230

import tempfile

231

232

# Configure file handling

233

config = {

234

'UPLOAD_DIR': '/tmp/uploads',

235

'UPLOAD_KEEP_FILENAME': True,

236

'UPLOAD_KEEP_EXTENSIONS': True,

237

'MAX_MEMORY_FILE_SIZE': 1024 * 1024, # 1MB

238

'UPLOAD_DELETE_TMP': False

239

}

240

241

# Create upload directory if it doesn't exist

242

os.makedirs(config['UPLOAD_DIR'], exist_ok=True)

243

244

# Create file object

245

uploaded_file = File(

246

file_name=b'document.pdf',

247

field_name=b'file_upload',

248

config=config

249

)

250

251

# Simulate writing file content

252

file_content = b'PDF content would go here...' * 1000

253

uploaded_file.write(file_content)

254

uploaded_file.finalize()

255

256

print(f"File: {uploaded_file.file_name.decode('utf-8')}")

257

print(f"Field: {uploaded_file.field_name.decode('utf-8')}")

258

print(f"Size: {uploaded_file.size} bytes")

259

print(f"In memory: {uploaded_file.in_memory}")

260

261

if not uploaded_file.in_memory:

262

print(f"Saved to: {uploaded_file.actual_file_name.decode('utf-8')}")

263

264

# Always close when done

265

uploaded_file.close()

266

```

267

268

**Memory Management Example:**

269

270

```python

271

from python_multipart import File

272

from io import BytesIO

273

274

def handle_file_upload(file_data, filename, field_name):

275

"""Demonstrate automatic memory-to-disk spillover."""

276

277

# Small files stay in memory

278

small_config = {'MAX_MEMORY_FILE_SIZE': 10 * 1024 * 1024} # 10MB

279

small_file = File(filename.encode(), field_name.encode(), small_config)

280

281

# Large files go to disk

282

large_config = {'MAX_MEMORY_FILE_SIZE': 1024} # 1KB

283

large_file = File(filename.encode(), field_name.encode(), large_config)

284

285

# Write same data to both

286

for file_obj in [small_file, large_file]:

287

file_obj.write(file_data)

288

file_obj.finalize()

289

290

print(f"File size: {file_obj.size}")

291

print(f"In memory: {file_obj.in_memory}")

292

293

if file_obj.in_memory:

294

# Access data directly from memory

295

data = file_obj.file_object.getvalue()

296

print(f"Memory data length: {len(data)}")

297

else:

298

# File is on disk

299

print(f"Disk file: {file_obj.actual_file_name}")

300

301

# Read from disk file

302

with open(file_obj.actual_file_name, 'rb') as f:

303

disk_data = f.read()

304

print(f"Disk data length: {len(disk_data)}")

305

306

file_obj.close()

307

308

# Test with sample data

309

test_data = b'x' * 2048 # 2KB of data

310

handle_file_upload(test_data, 'test.txt', 'upload')

311

```

312

313

**File Lifecycle Management:**

314

315

```python

316

from python_multipart import File

317

import tempfile

318

import os

319

320

def process_upload_with_cleanup(file_content, filename):

321

"""Demonstrate proper file lifecycle management."""

322

323

config = {

324

'UPLOAD_DIR': tempfile.gettempdir(),

325

'UPLOAD_DELETE_TMP': True, # Auto-delete temp files

326

'MAX_MEMORY_FILE_SIZE': 1024

327

}

328

329

file_obj = File(filename.encode(), b'upload', config)

330

331

try:

332

# Write file content

333

file_obj.write(file_content)

334

file_obj.finalize()

335

336

# Process the file

337

if file_obj.in_memory:

338

# Work with in-memory file

339

content = file_obj.file_object.getvalue()

340

return f"Processed {len(content)} bytes in memory"

341

else:

342

# Work with disk file

343

temp_path = file_obj.actual_file_name.decode()

344

345

# Process file on disk

346

with open(temp_path, 'rb') as f:

347

content = f.read()

348

349

return f"Processed {len(content)} bytes from disk"

350

351

finally:

352

# Always clean up

353

file_obj.close() # This will delete temp files if configured

354

355

# Usage

356

result = process_upload_with_cleanup(b'Large file content...', 'data.bin')

357

print(result)

358

```

359

360

### Integration with Parsers

361

362

Field and File objects are automatically created by FormParser and passed to callbacks:

363

364

```python

365

from python_multipart import FormParser

366

367

def handle_form_data(content_type, input_stream):

368

processed_fields = []

369

processed_files = []

370

371

def on_field(field):

372

# Field object is automatically created and populated

373

processed_fields.append({

374

'name': field.field_name.decode('utf-8'),

375

'value': field.value.decode('utf-8') if field.value else None

376

})

377

field.close() # Clean up

378

379

def on_file(file):

380

# File object is automatically created and populated

381

file_info = {

382

'field_name': file.field_name.decode('utf-8'),

383

'filename': file.file_name.decode('utf-8') if file.file_name else None,

384

'size': file.size,

385

'in_memory': file.in_memory

386

}

387

388

if file.in_memory:

389

# Small file - data is in memory

390

file_info['content'] = file.file_object.getvalue()

391

else:

392

# Large file - saved to disk

393

file_info['temp_path'] = file.actual_file_name.decode('utf-8')

394

395

processed_files.append(file_info)

396

file.close() # Important: clean up temp files

397

398

# Parser automatically creates Field/File objects

399

parser = FormParser(content_type, on_field, on_file)

400

401

# Process input stream

402

while True:

403

chunk = input_stream.read(8192)

404

if not chunk:

405

break

406

parser.write(chunk)

407

408

parser.finalize()

409

parser.close()

410

411

return {

412

'fields': processed_fields,

413

'files': processed_files

414

}

415

```