or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mddatabase.mdindex.mdirc-protocol.mdplugin-development.mdutilities.md

utilities.mddocs/

0

# Utilities and Tools

1

2

Sopel provides comprehensive utility functions and classes for IRC formatting, time handling, web operations, mathematical calculations, logging, and identifier management. These tools simplify common bot development tasks and provide robust functionality for plugin developers.

3

4

## Capabilities

5

6

### IRC Text Formatting

7

8

Functions and constants for applying IRC formatting codes to text messages.

9

10

```python { .api }

11

# Formatting functions

12

def bold(text: str) -> str:

13

"""

14

Apply bold formatting to text.

15

16

Args:

17

text (str): Text to make bold

18

19

Returns:

20

Text with bold formatting codes

21

"""

22

23

def italic(text: str) -> str:

24

"""

25

Apply italic formatting to text.

26

27

Args:

28

text (str): Text to make italic

29

30

Returns:

31

Text with italic formatting codes

32

"""

33

34

def underline(text: str) -> str:

35

"""

36

Apply underline formatting to text.

37

38

Args:

39

text (str): Text to underline

40

41

Returns:

42

Text with underline formatting codes

43

"""

44

45

def strikethrough(text: str) -> str:

46

"""

47

Apply strikethrough formatting to text.

48

49

Args:

50

text (str): Text to strikethrough

51

52

Returns:

53

Text with strikethrough formatting codes

54

"""

55

56

def monospace(text: str) -> str:

57

"""

58

Apply monospace formatting to text.

59

60

Args:

61

text (str): Text to make monospace

62

63

Returns:

64

Text with monospace formatting codes

65

"""

66

67

def reverse(text: str) -> str:

68

"""

69

Apply reverse formatting to text.

70

71

Args:

72

text (str): Text to reverse colors

73

74

Returns:

75

Text with reverse formatting codes

76

"""

77

78

def color(text: str, fg: int = None, bg: int = None) -> str:

79

"""

80

Apply color formatting to text.

81

82

Args:

83

text (str): Text to colorize

84

fg (int): Foreground color code (0-15)

85

bg (int): Background color code (0-15)

86

87

Returns:

88

Text with color formatting codes

89

"""

90

91

def hex_color(text: str, fg: str = None, bg: str = None) -> str:

92

"""

93

Apply hexadecimal color formatting to text.

94

95

Args:

96

text (str): Text to colorize

97

fg (str): Foreground hex color (e.g., "#FF0000")

98

bg (str): Background hex color

99

100

Returns:

101

Text with hex color formatting codes

102

"""

103

104

def plain(text: str) -> str:

105

"""

106

Remove all formatting codes from text.

107

108

Args:

109

text (str): Text to strip formatting from

110

111

Returns:

112

Plain text without formatting codes

113

"""

114

115

def hex_color(text: str, fg: str = None, bg: str = None) -> str:

116

"""

117

Apply hex color formatting to text.

118

119

Args:

120

text (str): Text to color

121

fg (str, optional): Foreground hex color (e.g., '#FF0000')

122

bg (str, optional): Background hex color (e.g., '#00FF00')

123

124

Returns:

125

Text with hex color formatting codes

126

"""

127

128

def strikethrough(text: str) -> str:

129

"""

130

Apply strikethrough formatting to text.

131

132

Args:

133

text (str): Text to strike through

134

135

Returns:

136

Text with strikethrough formatting codes

137

"""

138

139

def monospace(text: str) -> str:

140

"""

141

Apply monospace formatting to text.

142

143

Args:

144

text (str): Text to make monospace

145

146

Returns:

147

Text with monospace formatting codes

148

"""

149

150

def reverse(text: str) -> str:

151

"""

152

Apply reverse-color formatting to text.

153

154

Args:

155

text (str): Text to reverse colors

156

157

Returns:

158

Text with reverse-color formatting codes

159

"""

160

161

# Formatting constants

162

CONTROL_NORMAL: str # Reset all formatting

163

CONTROL_COLOR: str # Color control code

164

CONTROL_HEX_COLOR: str # Hex color control code

165

CONTROL_BOLD: str # Bold control code

166

CONTROL_ITALIC: str # Italic control code

167

CONTROL_UNDERLINE: str # Underline control code

168

CONTROL_STRIKETHROUGH: str # Strikethrough control code

169

CONTROL_MONOSPACE: str # Monospace control code

170

CONTROL_REVERSE: str # Reverse control code

171

172

# Color enumeration

173

class colors(enum.Enum):

174

"""Standard IRC color codes."""

175

WHITE: int = 0

176

BLACK: int = 1

177

BLUE: int = 2

178

GREEN: int = 3

179

RED: int = 4

180

BROWN: int = 5

181

PURPLE: int = 6

182

ORANGE: int = 7

183

YELLOW: int = 8

184

LIGHT_GREEN: int = 9

185

TEAL: int = 10

186

LIGHT_CYAN: int = 11

187

LIGHT_BLUE: int = 12

188

PINK: int = 13

189

GREY: int = 14

190

LIGHT_GREY: int = 15

191

```

192

193

### Time and Duration Utilities

194

195

Functions for time formatting, timezone handling, and duration calculations.

196

197

```python { .api }

198

def validate_timezone(zone: str) -> str:

199

"""

200

Validate and normalize timezone string.

201

202

Args:

203

zone (str): Timezone identifier

204

205

Returns:

206

Normalized timezone string

207

208

Raises:

209

ValueError: If timezone is invalid

210

"""

211

212

def validate_format(tformat: str) -> str:

213

"""

214

Validate time format string.

215

216

Args:

217

tformat (str): Time format string

218

219

Returns:

220

Validated format string

221

222

Raises:

223

ValueError: If format is invalid

224

"""

225

226

def get_nick_timezone(db: 'SopelDB', nick: str) -> str | None:

227

"""

228

Get timezone preference for a user.

229

230

Args:

231

db (SopelDB): Database connection

232

nick (str): User nickname

233

234

Returns:

235

User's timezone or None if not set

236

"""

237

238

def get_channel_timezone(db: 'SopelDB', channel: str) -> str | None:

239

"""

240

Get timezone preference for a channel.

241

242

Args:

243

db (SopelDB): Database connection

244

channel (str): Channel name

245

246

Returns:

247

Channel's timezone or None if not set

248

"""

249

250

def get_timezone(db: 'SopelDB' = None, config: 'Config' = None, zone: str = None,

251

nick: str = None, channel: str = None) -> str:

252

"""

253

Get timezone from various sources with fallback logic.

254

255

Args:

256

db (SopelDB): Database connection

257

config (Config): Bot configuration

258

zone (str): Explicit timezone

259

nick (str): User nickname to check

260

channel (str): Channel name to check

261

262

Returns:

263

Resolved timezone string

264

"""

265

266

def format_time(dt=None, zone=None, format=None) -> str:

267

"""

268

Format datetime with timezone and format options.

269

270

Args:

271

dt: Datetime object (defaults to now)

272

zone (str): Timezone for formatting

273

format (str): Time format string

274

275

Returns:

276

Formatted time string

277

"""

278

279

def seconds_to_split(seconds: int) -> 'Duration':

280

"""

281

Convert seconds to Duration namedtuple.

282

283

Args:

284

seconds (int): Number of seconds

285

286

Returns:

287

Duration with years, days, hours, minutes, seconds

288

"""

289

290

def get_time_unit(unit: str) -> int:

291

"""

292

Get number of seconds in a time unit.

293

294

Args:

295

unit (str): Time unit name (second, minute, hour, day, week, etc.)

296

297

Returns:

298

Number of seconds in the unit

299

"""

300

301

def seconds_to_human(seconds: int, precision: int = 2) -> str:

302

"""

303

Convert seconds to human-readable duration string.

304

305

Args:

306

seconds (int): Number of seconds

307

precision (int): Number of time units to include

308

309

Returns:

310

Human-readable duration (e.g., "2 hours, 30 minutes")

311

"""

312

313

# Duration namedtuple

314

Duration = namedtuple('Duration', ['years', 'days', 'hours', 'minutes', 'seconds'])

315

```

316

317

### Web Utilities

318

319

Functions for web requests, user agent management, and HTTP operations.

320

321

```python { .api }

322

def get_user_agent() -> str:

323

"""

324

Get default User-Agent string for web requests.

325

326

Returns:

327

User-Agent string identifying Sopel

328

"""

329

330

def get_session() -> 'requests.Session':

331

"""

332

Get configured requests session with appropriate headers.

333

334

Returns:

335

Requests session object with Sopel User-Agent

336

"""

337

338

# Additional web utilities available in sopel.tools.web module

339

```

340

341

### Mathematical Calculations

342

343

Safe mathematical expression evaluation and utility functions.

344

345

```python { .api }

346

def eval_equation(equation: str) -> float:

347

"""

348

Safely evaluate mathematical expression.

349

350

Args:

351

equation (str): Mathematical expression to evaluate

352

353

Returns:

354

Result of the calculation

355

356

Raises:

357

ValueError: If expression is invalid or unsafe

358

"""

359

360

def guarded_mul(left: float, right: float) -> float:

361

"""

362

Multiply two numbers with overflow protection.

363

364

Args:

365

left (float): First number

366

right (float): Second number

367

368

Returns:

369

Product of the numbers

370

371

Raises:

372

ValueError: If result would overflow

373

"""

374

375

def guarded_pow(num: float, exp: float) -> float:

376

"""

377

Raise number to power with complexity protection.

378

379

Args:

380

num (float): Base number

381

exp (float): Exponent

382

383

Returns:

384

Result of num ** exp

385

386

Raises:

387

ValueError: If operation is too complex

388

"""

389

390

def pow_complexity(num: int, exp: int) -> float:

391

"""

392

Calculate complexity score for power operation.

393

394

Args:

395

num (int): Base number

396

exp (int): Exponent

397

398

Returns:

399

Complexity score for the operation

400

"""

401

```

402

403

### Identifier Handling

404

405

IRC identifier management with proper case-insensitive comparison.

406

407

```python { .api }

408

class Identifier(str):

409

"""

410

IRC identifier with RFC1459 case-insensitive comparison.

411

412

Handles IRC nickname and channel name comparison properly.

413

"""

414

415

def __new__(cls, identifier: str) -> 'Identifier':

416

"""Create new identifier instance."""

417

418

def lower(self) -> str:

419

"""Get RFC1459 lowercase version of identifier."""

420

421

class IdentifierFactory:

422

"""Factory for creating Identifier instances."""

423

424

def __init__(self, case_mapping: str = 'rfc1459'):

425

"""

426

Initialize identifier factory.

427

428

Args:

429

case_mapping (str): Case mapping to use ('rfc1459' or 'ascii')

430

"""

431

432

def __call__(self, identifier: str) -> Identifier:

433

"""Create identifier using this factory's case mapping."""

434

435

def ascii_lower(text: str) -> str:

436

"""

437

Convert text to lowercase using ASCII rules.

438

439

Args:

440

text (str): Text to convert

441

442

Returns:

443

Lowercase text using ASCII case mapping

444

"""

445

```

446

447

### Memory Storage Classes

448

449

Utility classes for storing data with identifier-based access.

450

451

```python { .api }

452

class SopelMemory(dict):

453

"""

454

Dictionary subclass for storing bot data.

455

456

Provides additional utility methods for data management.

457

"""

458

459

def __init__(self):

460

"""Initialize empty memory storage."""

461

462

def lock(self, key: str) -> 'threading.Lock':

463

"""

464

Get thread lock for a specific key.

465

466

Args:

467

key (str): Key to get lock for

468

469

Returns:

470

Threading lock object

471

"""

472

473

class SopelIdentifierMemory(SopelMemory):

474

"""

475

Memory storage using IRC identifiers as keys.

476

477

Provides case-insensitive access using IRC identifier rules.

478

"""

479

480

def __init__(self, identifier_factory: IdentifierFactory = None):

481

"""

482

Initialize identifier memory.

483

484

Args:

485

identifier_factory (IdentifierFactory): Factory for creating identifiers

486

"""

487

488

class SopelMemoryWithDefault(SopelMemory):

489

"""

490

Memory storage with default value factory.

491

492

Automatically creates default values for missing keys.

493

"""

494

495

def __init__(self, default_factory: callable = None):

496

"""

497

Initialize memory with default factory.

498

499

Args:

500

default_factory (callable): Function to create default values

501

"""

502

```

503

504

### Logging Utilities

505

506

Functions for plugin logging and message output.

507

508

```python { .api }

509

def get_logger(plugin_name: str) -> 'logging.Logger':

510

"""

511

Get logger instance for a plugin.

512

513

Args:

514

plugin_name (str): Name of the plugin

515

516

Returns:

517

Logger configured for the plugin

518

"""

519

520

def get_sendable_message(text: str, max_length: int = 400) -> tuple[str, str]:

521

"""

522

Split text into sendable message and excess.

523

524

Args:

525

text (str): Text to split

526

max_length (int): Maximum message length in bytes

527

528

Returns:

529

Tuple of (sendable_text, excess_text)

530

"""

531

532

def get_hostmask_regex(mask: str) -> 'Pattern':

533

"""

534

Create regex pattern for IRC hostmask matching.

535

536

Args:

537

mask (str): Hostmask pattern with wildcards

538

539

Returns:

540

Compiled regex pattern for matching

541

"""

542

543

def chain_loaders(*lazy_loaders) -> callable:

544

"""

545

Chain multiple lazy loader functions together.

546

547

Args:

548

*lazy_loaders: Lazy loader functions

549

550

Returns:

551

Combined lazy loader function

552

"""

553

```

554

555

## Usage Examples

556

557

### IRC Text Formatting

558

559

```python

560

from sopel import plugin

561

from sopel.formatting import bold, color, italic, plain

562

563

@plugin.command('format')

564

@plugin.example('.format Hello World')

565

def format_example(bot, trigger):

566

"""Demonstrate text formatting."""

567

text = trigger.group(2) or "Sample Text"

568

569

# Apply various formatting

570

formatted_examples = [

571

bold(text),

572

italic(text),

573

color(text, colors.RED),

574

color(text, colors.WHITE, colors.BLUE),

575

bold(color(text, colors.GREEN))

576

]

577

578

for example in formatted_examples:

579

bot.say(example)

580

581

# Show plain text version

582

bot.say(f"Plain: {plain(formatted_examples[0])}")

583

584

@plugin.command('rainbow')

585

def rainbow_text(bot, trigger):

586

"""Create rainbow-colored text."""

587

text = trigger.group(2) or "RAINBOW"

588

rainbow_colors = [colors.RED, colors.ORANGE, colors.YELLOW,

589

colors.GREEN, colors.BLUE, colors.PURPLE]

590

591

colored_chars = []

592

for i, char in enumerate(text):

593

if char != ' ':

594

color_code = rainbow_colors[i % len(rainbow_colors)]

595

colored_chars.append(color(char, color_code))

596

else:

597

colored_chars.append(char)

598

599

bot.say(''.join(colored_chars))

600

```

601

602

### Time and Duration Utilities

603

604

```python

605

from sopel import plugin

606

from sopel.tools.time import format_time, seconds_to_human, get_time_unit

607

608

@plugin.command('time')

609

@plugin.example('.time UTC')

610

def time_command(bot, trigger):

611

"""Show current time in specified timezone."""

612

zone = trigger.group(2) or 'UTC'

613

614

try:

615

current_time = format_time(zone=zone, format='%Y-%m-%d %H:%M:%S %Z')

616

bot.reply(f"Current time in {zone}: {current_time}")

617

except ValueError as e:

618

bot.reply(f"Invalid timezone: {e}")

619

620

@plugin.command('uptime')

621

def uptime_command(bot, trigger):

622

"""Show bot uptime."""

623

import time

624

625

# Calculate uptime (this would need to be tracked elsewhere)

626

uptime_seconds = int(time.time() - bot.start_time) # Hypothetical

627

uptime_human = seconds_to_human(uptime_seconds)

628

629

bot.reply(f"Bot uptime: {uptime_human}")

630

631

@plugin.command('remind')

632

@plugin.example('.remind 30m Check the logs')

633

def remind_command(bot, trigger):

634

"""Set a reminder with time parsing."""

635

args = trigger.group(2)

636

if not args:

637

bot.reply("Usage: .remind <time> <message>")

638

return

639

640

parts = args.split(' ', 1)

641

if len(parts) < 2:

642

bot.reply("Usage: .remind <time> <message>")

643

return

644

645

time_str, message = parts

646

647

# Parse time string (simplified)

648

try:

649

if time_str.endswith('m'):

650

minutes = int(time_str[:-1])

651

seconds = minutes * get_time_unit('minute')

652

elif time_str.endswith('h'):

653

hours = int(time_str[:-1])

654

seconds = hours * get_time_unit('hour')

655

else:

656

seconds = int(time_str)

657

658

# Schedule reminder (would need actual scheduling)

659

bot.reply(f"Reminder set for {seconds_to_human(seconds)}: {message}")

660

661

except ValueError:

662

bot.reply("Invalid time format. Use: 30m, 2h, or seconds")

663

```

664

665

### Mathematical Calculations

666

667

```python

668

from sopel import plugin

669

from sopel.tools.calculation import eval_equation

670

671

@plugin.command('calc')

672

@plugin.example('.calc 2 + 2 * 3')

673

def calc_command(bot, trigger):

674

"""Safely calculate mathematical expressions."""

675

expression = trigger.group(2)

676

if not expression:

677

bot.reply("Usage: .calc <expression>")

678

return

679

680

try:

681

result = eval_equation(expression)

682

bot.reply(f"{expression} = {result}")

683

except ValueError as e:

684

bot.reply(f"Calculation error: {e}")

685

except Exception as e:

686

bot.reply(f"Invalid expression: {e}")

687

688

@plugin.command('convert')

689

@plugin.example('.convert 100 F to C')

690

def convert_command(bot, trigger):

691

"""Temperature conversion with calculations."""

692

args = trigger.group(2)

693

if not args:

694

bot.reply("Usage: .convert <temp> <F|C> to <C|F>")

695

return

696

697

parts = args.split()

698

if len(parts) != 4 or parts[2].lower() != 'to':

699

bot.reply("Usage: .convert <temp> <F|C> to <C|F>")

700

return

701

702

try:

703

temp = float(parts[0])

704

from_unit = parts[1].upper()

705

to_unit = parts[3].upper()

706

707

if from_unit == 'F' and to_unit == 'C':

708

result = (temp - 32) * 5/9

709

bot.reply(f"{temp}°F = {result:.2f}°C")

710

elif from_unit == 'C' and to_unit == 'F':

711

result = temp * 9/5 + 32

712

bot.reply(f"{temp}°C = {result:.2f}°F")

713

else:

714

bot.reply("Supported conversions: F to C, C to F")

715

except ValueError:

716

bot.reply("Invalid temperature value")

717

```

718

719

### Identifier and Memory Usage

720

721

```python

722

from sopel import plugin

723

from sopel.tools import Identifier, SopelMemory

724

725

# Plugin-level memory storage

726

user_scores = SopelMemory()

727

728

@plugin.command('score')

729

def score_command(bot, trigger):

730

"""Show or modify user scores."""

731

args = trigger.group(2)

732

733

if not args:

734

# Show current user's score

735

nick = Identifier(trigger.nick)

736

score = user_scores.get(nick, 0)

737

bot.reply(f"Your score: {score}")

738

return

739

740

parts = args.split()

741

if len(parts) == 1:

742

# Show specified user's score

743

nick = Identifier(parts[0])

744

score = user_scores.get(nick, 0)

745

bot.reply(f"Score for {nick}: {score}")

746

elif len(parts) == 2:

747

# Set score (admin only)

748

if trigger.nick not in bot.settings.core.admins:

749

bot.reply("Only admins can set scores")

750

return

751

752

nick = Identifier(parts[0])

753

try:

754

new_score = int(parts[1])

755

user_scores[nick] = new_score

756

bot.reply(f"Set score for {nick} to {new_score}")

757

except ValueError:

758

bot.reply("Score must be a number")

759

760

@plugin.command('leaderboard')

761

def leaderboard_command(bot, trigger):

762

"""Show score leaderboard."""

763

if not user_scores:

764

bot.reply("No scores recorded yet")

765

return

766

767

# Sort by score (descending)

768

sorted_scores = sorted(user_scores.items(), key=lambda x: x[1], reverse=True)

769

top_5 = sorted_scores[:5]

770

771

leaderboard = ["🏆 Leaderboard:"]

772

for i, (nick, score) in enumerate(top_5, 1):

773

leaderboard.append(f"{i}. {nick}: {score}")

774

775

bot.reply(" | ".join(leaderboard))

776

```

777

778

### Logging and Debugging

779

780

```python

781

from sopel import plugin

782

from sopel.tools import get_logger

783

784

# Get plugin-specific logger

785

LOGGER = get_logger('my_plugin')

786

787

@plugin.command('debug')

788

@plugin.require_admin()

789

def debug_command(bot, trigger):

790

"""Show debug information."""

791

args = trigger.group(2)

792

793

if args == 'memory':

794

# Show memory usage information

795

import psutil

796

process = psutil.Process()

797

memory_mb = process.memory_info().rss / 1024 / 1024

798

bot.reply(f"Memory usage: {memory_mb:.1f} MB")

799

800

elif args == 'channels':

801

# Show channel information

802

channel_count = len(bot.channels)

803

channel_list = list(bot.channels.keys())[:5] # First 5

804

bot.reply(f"In {channel_count} channels: {', '.join(channel_list)}")

805

806

elif args == 'users':

807

# Show user information

808

user_count = len(bot.users)

809

bot.reply(f"Tracking {user_count} users")

810

811

else:

812

bot.reply("Debug options: memory, channels, users")

813

814

# Log debug information

815

LOGGER.info(f"Debug command used by {trigger.nick}: {args}")

816

817

@plugin.command('log')

818

@plugin.require_owner()

819

def log_test(bot, trigger):

820

"""Test logging at different levels."""

821

message = trigger.group(2) or "Test message"

822

823

LOGGER.debug(f"Debug: {message}")

824

LOGGER.info(f"Info: {message}")

825

LOGGER.warning(f"Warning: {message}")

826

LOGGER.error(f"Error: {message}")

827

828

bot.reply("Log messages sent at all levels")

829

```

830

831

## Types

832

833

### Formatting Types

834

835

```python { .api }

836

# Control character constants

837

CONTROL_FORMATTING: list # List of all formatting control chars

838

CONTROL_NON_PRINTING: list # List of non-printing control chars

839

```

840

841

### Time Types

842

843

```python { .api }

844

Duration = namedtuple('Duration', ['years', 'days', 'hours', 'minutes', 'seconds'])

845

```

846

847

### Memory Types

848

849

```python { .api }

850

# Type aliases for memory storage

851

IdentifierMemory = SopelIdentifierMemory

852

Memory = SopelMemory

853

MemoryWithDefault = SopelMemoryWithDefault

854

```