Getting Started with Pydapter¶
Pydapter is a powerful adapter library that lets you easily convert between Pydantic models and various data formats. With the new field system (v0.3.0), creating robust models is easier than ever!
Installation¶
First, let's install pydapter and its dependencies:
# Create a virtual environment (optional but recommended)
python -m venv pydapter-demo
source pydapter-demo/bin/activate # On Windows: pydapter-demo\Scripts\activate
# Install pydapter and dependencies
uv pip install pydapter
uv pip install pandas # For DataFrameAdapter and SeriesAdapter
uv pip install xlsxwriter # For ExcelAdapter
uv pip install openpyxl # Also needed for Excel support
# Install optional modules
uv pip install "pydapter[protocols]" # For standardized model interfaces
uv pip install "pydapter[migrations-sql]" # For database schema migrations
# or install all adapters at once
uv pip install "pydapter[all]"
Creating Models with the Field System¶
Option 1: Using Field Families (New!)¶
from pydapter.fields import DomainModelBuilder, FieldTemplate
from pydapter.protocols import (
create_protocol_model_class,
IDENTIFIABLE,
TEMPORAL
)
# Build a model with field families
User = (
DomainModelBuilder("User")
.with_entity_fields() # Adds id, created_at, updated_at
.add_field("name", FieldTemplate(base_type=str))
.add_field("email", FieldTemplate(base_type=str))
.add_field("active", FieldTemplate(base_type=bool, default=True))
.add_field("tags", FieldTemplate(base_type=list[str], default_factory=list))
.build()
)
# Or create a protocol-compliant model with behaviors
User = create_protocol_model_class(
"User",
IDENTIFIABLE, # Adds id field
TEMPORAL, # Adds created_at, updated_at + update_timestamp() method
name=FieldTemplate(base_type=str),
email=FieldTemplate(base_type=str),
active=FieldTemplate(base_type=bool, default=True),
tags=FieldTemplate(base_type=list[str], default_factory=list)
)
Option 2: Traditional Pydantic Models¶
from pydantic import BaseModel, Field
from typing import List
from pydapter.adapters.json_ import JsonAdapter
# Define a traditional Pydantic model
class User(BaseModel):
id: int
name: str
email: str
active: bool = True
tags: List[str] = []
Using Adapters¶
Once you have your models, you can use pydapter's adapters to convert data:
from pydapter.adapters.json_ import JsonAdapter
# Create some test data
users = [
User(id=1, name="Alice", email="alice@example.com", tags=["admin", "staff"]),
User(id=2, name="Bob", email="bob@example.com", active=False),
User(id=3, name="Charlie", email="charlie@example.com", tags=["staff"]),
]
# If using protocol models with behaviors
if hasattr(users[0], 'update_timestamp'):
users[0].update_timestamp() # Updates the updated_at field
# Convert models to JSON
json_data = JsonAdapter.to_obj(users, many=True)
print("JSON Output:")
print(json_data)
# Convert JSON back to models
loaded_users = JsonAdapter.from_obj(User, json_data, many=True)
print("\nLoaded users:")
for user in loaded_users:
print(f"{user.name} ({user.email}): Active={user.active}, Tags={user.tags}")
Using the Adaptable Mixin for Better Ergonomics¶
Pydapter provides an Adaptable mixin that makes the API more ergonomic:
from pydantic import BaseModel
from typing import List
from pydapter.core import Adaptable
from pydapter.adapters.json_ import JsonAdapter
# Define a model with the Adaptable mixin
class Product(BaseModel, Adaptable):
id: int
name: str
price: float
in_stock: bool = True
# Register the JSON adapter
Product.register_adapter(JsonAdapter)
# Create a product
product = Product(id=101, name="Laptop", price=999.99)
# Convert to JSON using the mixin method
json_data = product.adapt_to(obj_key="json")
print("JSON Output:")
print(json_data)
# Convert back to a model
loaded_product = Product.adapt_from(json_data, obj_key="json")
print(f"\nLoaded product: {loaded_product.name} (${loaded_product.price})")
Working with CSV¶
Here's how to use the CSV adapter:
from pydantic import BaseModel
from pydapter.adapters.csv_ import CsvAdapter
# Define a Pydantic model
class Employee(Adaptable, BaseModel):
id: int
name: str
department: str
salary: float
hire_date: str
# Create some sample data
employees = [
Employee(
id=1, name="Alice", department="Engineering",
salary=85000, hire_date="2020-01-15"
),
Employee(
id=2, name="Bob", department="Marketing",
salary=75000, hire_date="2021-03-20"
),
Employee(
id=3, name="Charlie", department="Finance",
salary=95000, hire_date="2019-11-01"
),
]
csv_data = CsvAdapter.to_obj(employees, many=True)
print("CSV Output:")
print(csv_data)
# Convert CSV back to models
loaded_employees = CsvAdapter.from_obj(Employee, csv_data, many=True)
print("\nLoaded employees:")
for employee in loaded_employees:
print(f"{employee.name} - {employee.department} (${employee.salary})")
# You can also save to a file and read from a file
from pathlib import Path
# Save to file
Path("employees.csv").write_text(csv_data)
# Read from file
file_employees = CsvAdapter.from_obj(Employee, Path("employees.csv"), many=True)
Working with TOML¶
Here's how to use the TOML adapter:
from pydantic import BaseModel
from typing import List, Dict, Optional
from pydapter.adapters.toml_ import TomlAdapter
# Define a Pydantic model
class AppConfig(BaseModel):
app_name: str
version: str
debug: bool = False
database: Dict[str, str] = {}
allowed_hosts: List[str] = []
# Create a config
config = AppConfig(
app_name="MyApp",
version="1.0.0",
debug=True,
database={"host": "localhost", "port": "5432", "name": "myapp"},
allowed_hosts=["localhost", "example.com"]
)
# Convert to TOML
toml_data = TomlAdapter.to_obj(config)
print("TOML Output:")
print(toml_data)
# Convert TOML back to model
loaded_config = TomlAdapter.from_obj(AppConfig, toml_data)
print("\nLoaded config:")
print(f"App: {loaded_config.app_name} v{loaded_config.version}")
print(f"Debug mode: {loaded_config.debug}")
print(f"Database: {loaded_config.database}")
print(f"Allowed hosts: {loaded_config.allowed_hosts}")
# Save to file
Path("config.toml").write_text(toml_data)
# Read from file
file_config = TomlAdapter.from_obj(AppConfig, Path("config.toml"))
Working with Pandas DataFrame¶
Here's how to use the DataFrame adapter:
import pandas as pd
from pydantic import BaseModel
from pydapter.extras.pandas_ import DataFrameAdapter
# Define a Pydantic model
class SalesRecord(BaseModel):
id: int
product: str
quantity: int
price: float
date: str
# Create a sample DataFrame
df = pd.DataFrame([
{
"id": 1, "product": "Laptop", "quantity": 2,
"price": 999.99, "date": "2023-01-15"
},
{
"id": 2, "product": "Monitor", "quantity": 3,
"price": 249.99, "date": "2023-01-20"
},
{
"id": 3, "product": "Mouse", "quantity": 5,
"price": 29.99, "date": "2023-01-25"
}
])
# Convert DataFrame to models
sales_records = DataFrameAdapter.from_obj(SalesRecord, df, many=True)
print("DataFrame to Models:")
for record in sales_records:
print(f"{record.id}: {record.quantity} x {record.product} at ${record.price}")
# Convert models back to DataFrame
new_df = DataFrameAdapter.to_obj(sales_records, many=True)
print("\nModels to DataFrame:")
print(new_df)
Working with Excel Files¶
Here's how to use the Excel adapter:
from pydantic import BaseModel
from typing import List, Optional
from pydapter.extras.excel_ import ExcelAdapter
from pathlib import Path
# Define a Pydantic model
class Student(BaseModel):
id: int
name: str
grade: str
score: float
# Create some sample data
students = [
Student(id=1, name="Alice", grade="A", score=92.5),
Student(id=2, name="Bob", grade="B", score=85.0),
Student(id=3, name="Charlie", grade="A-", score=90.0),
]
# Convert to Excel and save to file
excel_data = ExcelAdapter.to_obj(students, many=True, sheet_name="Students")
with open("students.xlsx", "wb") as f:
f.write(excel_data)
print("Excel file saved as 'students.xlsx'")
# Read from Excel file
loaded_students = ExcelAdapter.from_obj(
Student, Path("students.xlsx"), many=True
)
print("\nLoaded students:")
for student in loaded_students:
print(f"{student.name}: {student.grade} ({student.score})")
Error Handling¶
Let's demonstrate proper error handling:
from pydantic import BaseModel, Field
from pydapter.adapters.json_ import JsonAdapter
from pydapter.exceptions import ParseError, ValidationError as AdapterValidationError
# Define a model with validation constraints
class Product(BaseModel):
id: int = Field(gt=0) # Must be greater than 0
name: str = Field(min_length=3) # Must be at least 3 characters
price: float = Field(gt=0.0) # Must be greater than 0
# Handle parsing errors
try:
# Try to parse invalid JSON
invalid_json = "{ 'id': 1, 'name': 'Laptop', price: 999.99 }" # Note the
# missing
# quotes
# around
# 'price'
product = JsonAdapter.from_obj(Product, invalid_json)
except ParseError as e:
print(f"Parsing error: {e}")
# Handle validation errors
try:
# Try to create a model with invalid data
valid_json = '{"id": 0, "name": "A", "price": -10.0}' # All fields
# violate
# constraints
product = JsonAdapter.from_obj(Product, valid_json)
except AdapterValidationError as e:
print(f"Validation error: {e}")
if hasattr(e, 'errors') and callable(e.errors):
for error in e.errors():
print(f" - {error['loc']}: {error['msg']}")
Using Protocols¶
Pydapter provides a set of standardized interfaces through the protocols module. These protocols allow you to add common capabilities to your models:
from pydapter.protocols import Identifiable, Temporal
# Define a model with standardized interfaces
class User(Identifiable, Temporal):
name: str
email: str
# Create a user
user = User(name="Alice", email="alice@example.com")
# Access standardized properties
print(f"User ID: {user.id}") # Automatically generated UUID
print(f"Created at: {user.created_at}") # Automatically set timestamp
# Update the timestamp
user.name = "Alicia"
user.update_timestamp()
print(f"Updated at: {user.updated_at}")
For more details, see the Protocols documentation and the Using Protocols tutorial.
Using Migrations¶
Pydapter provides tools for managing database schema changes through the migrations module:
from pydapter.migrations import AlembicAdapter
import mymodels # Module containing your SQLAlchemy models
# Initialize migrations
AlembicAdapter.init_migrations(
directory="migrations",
connection_string="postgresql://user:pass@localhost/mydb",
models_module=mymodels
)
# Create a migration
revision = AlembicAdapter.create_migration(
message="Create users table",
autogenerate=True,
directory="migrations",
connection_string="postgresql://user:pass@localhost/mydb"
)
# Apply migrations
AlembicAdapter.upgrade(
revision="head",
directory="migrations",
connection_string="postgresql://user:pass@localhost/mydb"
)
For more details, see the Migrations documentation and the Using Migrations tutorial.