Aller au contenu principal

Lab 06 — Build a Reusable Utility Package

Hands-on Lab 30 min Intermediate

Objectives

  1. Create a proper package directory structure
  2. Write multiple modules with public APIs
  3. Configure __init__.py with relative imports
  4. Create and activate a virtual environment
  5. Write a requirements.txt

Step 1 — Project Structure

mkdir ~/python-course/lab-06
cd ~/python-course/lab-06

# Create virtual environment
python3 -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\Activate.ps1 # Windows

# Create package structure
mkdir -p pyutils/formatters
mkdir -p pyutils/validators
touch pyutils/__init__.py
touch pyutils/formatters/__init__.py
touch pyutils/validators/__init__.py

Step 2 — Create the Modules

pyutils/formatters/__init__.py:

from .text import truncate, title_case, slugify
from .numbers import format_currency, format_percentage, format_bytes

__all__ = ["truncate", "title_case", "slugify",
"format_currency", "format_percentage", "format_bytes"]

pyutils/formatters/text.py:

import re

def truncate(text: str, max_len: int = 50, suffix: str = "...") -> str:
"""Truncate text to max_len characters."""
if len(text) <= max_len:
return text
return text[:max_len - len(suffix)] + suffix

def title_case(text: str) -> str:
"""Convert to title case, handling articles and prepositions."""
SMALL_WORDS = {"a", "an", "the", "and", "but", "or", "in", "on", "at", "to"}
words = text.lower().split()
result = []
for i, word in enumerate(words):
if i == 0 or word not in SMALL_WORDS:
result.append(word.capitalize())
else:
result.append(word)
return " ".join(result)

def slugify(text: str) -> str:
"""Convert text to URL-friendly slug."""
text = text.lower().strip()
text = re.sub(r"[^\w\s-]", "", text)
return re.sub(r"[\s_-]+", "-", text).strip("-")

pyutils/formatters/numbers.py:

def format_currency(amount: float, symbol: str = "$", decimals: int = 2) -> str:
return f"{symbol}{amount:,.{decimals}f}"

def format_percentage(value: float, decimals: int = 1) -> str:
return f"{value:.{decimals}%}"

def format_bytes(num_bytes: int) -> str:
for unit in ["B", "KB", "MB", "GB", "TB"]:
if num_bytes < 1024:
return f"{num_bytes:.1f} {unit}"
num_bytes /= 1024
return f"{num_bytes:.1f} PB"

pyutils/validators/__init__.py:

from .validators import validate_email, validate_url, validate_phone

__all__ = ["validate_email", "validate_url", "validate_phone"]

pyutils/validators/validators.py:

import re

def validate_email(email: str) -> bool:
pattern = r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, email))

def validate_url(url: str) -> bool:
pattern = r"^https?://[\w.-]+(?:/[\w./?=%&-]*)?$"
return bool(re.match(pattern, url))

def validate_phone(phone: str, country: str = "CA") -> bool:
patterns = {
"CA": r"^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$",
"FR": r"^(?:\+33|0)[1-9](?:\d{2}){4}$",
}
pattern = patterns.get(country, r"^\+\d{7,15}$")
return bool(re.match(pattern, phone.strip()))

pyutils/__init__.py:

from .formatters import (truncate, title_case, slugify,
format_currency, format_percentage, format_bytes)
from .validators import validate_email, validate_url, validate_phone

__version__ = "1.0.0"
__author__ = "Your Name"

Step 3 — Test It

Create demo.py at the project root:

import pyutils as pu

# Text formatting
print(pu.truncate("This is a very long text that needs truncating", max_len=30))
print(pu.title_case("the quick brown fox and the lazy dog"))
print(pu.slugify("Hello World! This is a Test 123"))

# Number formatting
print(pu.format_currency(1234567.89))
print(pu.format_percentage(0.8567))
print(pu.format_bytes(1_073_741_824))

# Validation
print(pu.validate_email("alice@example.com")) # True
print(pu.validate_email("not-an-email")) # False
print(pu.validate_url("https://inskillboost.com")) # True
python3 demo.py

Verification ✅

This is a very long text th...
The Quick Brown Fox and the Lazy Dog
hello-world-this-is-a-test-123
$1,234,567.89
85.7%
1.0 GB
True
False
True

Summary

  • Created a multi-module package with sub-packages (formatters, validators)
  • Each sub-package has its own __init__.py exposing a clean public API
  • Used relative imports (from .text import ...) within the package
  • The top-level __init__.py re-exports everything for a flat import API