https://github.com/hardmax71/pydantic-neomodel-dict
Pydantic ↔ Neomodel OGM ↔ Python Dict Converter
https://github.com/hardmax71/pydantic-neomodel-dict
converter hypothesis-testing neo4j neomodel ogm orm pydantic
Last synced: 30 days ago
JSON representation
Pydantic ↔ Neomodel OGM ↔ Python Dict Converter
- Host: GitHub
- URL: https://github.com/hardmax71/pydantic-neomodel-dict
- Owner: HardMax71
- License: mit
- Created: 2025-03-12T21:26:30.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-03-17T16:49:50.000Z (over 1 year ago)
- Last Synced: 2025-03-17T16:52:41.129Z (over 1 year ago)
- Topics: converter, hypothesis-testing, neo4j, neomodel, ogm, orm, pydantic
- Language: Python
- Homepage:
- Size: 271 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
Pydantic ↔ Neomodel (Neo4j) OGM ↔ Python Dict Converter
A bidirectional converter between Pydantic models, Neomodel (Neo4j) OGM (Object Graph Mapper) models, and Python dictionaries. This
library simplifies the integration between Pydantic's data validation capabilities and Neo4j's graph database
operations.
## Features
- **Bidirectional Conversion**: Convert seamlessly between Pydantic models, Neomodel (Neo4j) OGM models, and Python dictionaries
- **Relationship Handling**: Process complex relationships at any level of nesting
- **Circular Reference Support**: Detect and properly handle circular references in object graphs
- **Custom Type Conversion**: Register custom type converters for specialized data transformations
- **Batch Operations**: Convert multiple objects efficiently with transaction support
- **Type Safety**: Full typing support with `mypy`
## Installation
```bash
pip install pydantic-neomodel-dict
```
## Requirements
- Python 3.10+
- pydantic 2.0.0+
- neomodel 5.0.0+
## Basic Usage
Here's a simple example demonstrating conversion between Pydantic and Neomodel (Neo4j) OGM models:
> [!NOTE]
> Before execution of example down here, use
> supplied [docker-compose.yml](https://github.com/HardMax71/pydantic-neomodel-dict/blob/main/docker-compose.yml)
> and start `neo4j` container inside via `docker-compose up --build`.
```python
from pydantic import BaseModel
from neomodel import StructuredNode, StringProperty, IntegerProperty, config
from pydantic_neomodel_dict import Converter
# Set up Neomodel (Neo4j) connection - this is required!
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
config.ENCRYPTED_CONNECTION = False
config.AUTO_INSTALL_LABELS = True
# Define your models
class UserPydantic(BaseModel):
name: str
email: str
age: int
class UserOGM(StructuredNode):
name = StringProperty(required=True)
email = StringProperty(unique_index=True, required=True)
age = IntegerProperty(index=True, default=0)
# Register the model mapping
Converter.register_models(UserPydantic, UserOGM)
# Convert Pydantic to OGM
user_pydantic = UserPydantic(name="John Doe", email="john@example.com", age=30)
user_ogm = Converter.to_ogm(user_pydantic)
# Convert OGM to Pydantic
user_pydantic_again = Converter.to_pydantic(user_ogm)
# Convert to/from dictionary
user_dict = {"name": "Jane Doe", "email": "jane@example.com", "age": 25}
user_ogm_from_dict = Converter.dict_to_ogm(user_dict, UserOGM)
user_dict_from_ogm = Converter.ogm_to_dict(user_ogm)
# Print results to verify
print(f"Original user: {user_pydantic}")
print(f"After round-trip conversion: {user_pydantic_again}")
print(f"Dictionary conversion result: {user_dict_from_ogm}")
```
```
$ python3 example.py
Original user: name='John Doe' email='john@example.com' age=30
After round-trip conversion: name='John Doe' email='john@example.com' age=30
Dictionary conversion result: {'name': 'John Doe', 'email': 'john@example.com', 'age': 30}
```
## Examples
Simple Model Conversion
This example demonstrates basic conversion between Pydantic models and Neomodel (Neo4j) OGM models:
```python
from pydantic import BaseModel
from neomodel import StructuredNode, StringProperty, IntegerProperty, UniqueIdProperty, config
from pydantic_neomodel_dict import Converter
# Set up Neomodel (Neo4j) connection - this is required!
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
config.ENCRYPTED_CONNECTION = False
config.AUTO_INSTALL_LABELS = True
# Define Pydantic model
class ProductPydantic(BaseModel):
uid: str
name: str
price: float
sku: str
# Define Neomodel (Neo4j) OGM model
class ProductOGM(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty(required=True)
price = IntegerProperty(required=True)
sku = StringProperty(unique_index=True, required=True)
# Register the models
Converter.register_models(ProductPydantic, ProductOGM)
# Create a Pydantic instance
product = ProductPydantic(
uid="123e4567-e89b-12d3-a456-426614174000",
name="Wireless Headphones",
price=99.99,
sku="WH-X1000"
)
# Convert to Neomodel (Neo4j) OGM model
product_ogm = Converter.to_ogm(product)
# Save to database
# product_ogm is already saved during conversion
# Query from database
retrieved_product = ProductOGM.nodes.get(sku="WH-X1000")
# Convert back to Pydantic model
product_pydantic = Converter.to_pydantic(retrieved_product)
print(f"Product: {product_pydantic.name}, Price: {product_pydantic.price}")
```
Output:
```
Product: Wireless Headphones, Price: 99
```
Nested Relationships
This example shows how to handle nested relationships between models:
```python
import random
from typing import List
from neomodel import IntegerProperty, One, RelationshipFrom, RelationshipTo, StringProperty, StructuredNode, config
from pydantic import BaseModel
from pydantic_neomodel_dict import Converter
# Set up Neomodel (Neo4j) connection - this is required!
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
config.ENCRYPTED_CONNECTION = False
config.AUTO_INSTALL_LABELS = True
# Define Pydantic models
class AddressPydantic(BaseModel):
street: str
city: str
zip_code: str
class OrderPydantic(BaseModel):
order_id: str
amount: float
class CustomerPydantic(BaseModel):
name: str
email: str
address: AddressPydantic
orders: List[OrderPydantic] = []
# Define Neomodel (Neo4j) OGM models
class AddressOGM(StructuredNode):
street = StringProperty(required=True)
city = StringProperty(required=True)
zip_code = StringProperty(required=True)
class OrderOGM(StructuredNode):
order_id = StringProperty(unique_index=True, required=True)
amount = IntegerProperty(required=True)
customer = RelationshipFrom('CustomerOGM', 'PLACED')
class CustomerOGM(StructuredNode):
name = StringProperty(required=True)
email = StringProperty(unique_index=True, required=True)
address = RelationshipTo(AddressOGM, 'HAS_ADDRESS', One)
orders = RelationshipTo(OrderOGM, 'PLACED')
# Register model mappings
Converter.register_models(AddressPydantic, AddressOGM)
Converter.register_models(OrderPydantic, OrderOGM)
Converter.register_models(CustomerPydantic, CustomerOGM)
# Create a customer with address and orders
email = f"jane{random.randint(1, 1000)}@example.com"
customer = CustomerPydantic(
name="Jane Smith",
email=email,
address=AddressPydantic(
street="123 Main St",
city="New York",
zip_code="10001"
),
orders=[
OrderPydantic(order_id="ORD-001", amount=125.50),
OrderPydantic(order_id="ORD-002", amount=75.25)
]
)
# Convert to Neomodel (Neo4j) OGM model (this will create all related nodes)
customer_ogm = Converter.to_ogm(customer)
# Retrieve and convert back
retrieved_customer = CustomerOGM.nodes.get(email=email)
customer_pydantic = Converter.to_pydantic(retrieved_customer)
print(f"Customer: {customer_pydantic.name}")
print(f"Address: {customer_pydantic.address.street}, {customer_pydantic.address.city}")
print(f"Orders: {len(customer_pydantic.orders)}")
print("Whole dict: \n", customer_pydantic.model_dump())
```
Output:
```
Customer: Jane Smith
Address: 123 Main St, New York
Orders: 2
Whole dict:
{'name': 'Jane Smith', 'email': 'jane672@example.com', 'orders': [{'order_id': 'ORD-002', 'amount': 75}, {'order_id': 'ORD-001', 'amount': 125}], 'address': {'street': '123 Main St', 'city': 'New York', 'zip_code': '10001'}}
```
Handling Circular References
This example demonstrates how the converter handles circular references in object graphs:
```python
from typing import List
from neomodel import (
StructuredNode, StringProperty, RelationshipTo, config
)
from pydantic import BaseModel
from pydantic_neomodel_dict import Converter
# Set up Neomodel (Neo4j) connection - this is required!
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
config.ENCRYPTED_CONNECTION = False
config.AUTO_INSTALL_LABELS = True
# Define Pydantic models with circular references
class PersonPydantic(BaseModel):
name: str
friends: List['PersonPydantic'] = []
# Add self-reference resolution
PersonPydantic.model_rebuild()
# Define Neomodel (Neo4j) OGM models
class PersonOGM(StructuredNode):
name = StringProperty(required=True, unique_index=True)
friends = RelationshipTo('PersonOGM', 'FRIENDS_WITH')
# Register models
Converter.register_models(PersonPydantic, PersonOGM)
# Create instances with circular references
alice = PersonPydantic(name="Alice")
bob = PersonPydantic(name="Bob")
charlie = PersonPydantic(name="Charlie")
# Create circular references
alice.friends = [bob, charlie]
bob.friends = [alice, charlie]
charlie.friends = [alice, bob]
# Convert to Neomodel (Neo4j) OGM models (handles circular references)
alice_ogm = Converter.to_ogm(alice)
# Convert back to Pydantic
alice_pydantic = Converter.to_pydantic(alice_ogm)
print(f"{alice_pydantic.name}'s friends: {[friend.name for friend in alice_pydantic.friends]}")
print(f"{alice_pydantic.friends[0].name}'s friends: {[friend.name for friend in alice_pydantic.friends[0].friends]}")
```
Output:
```
Alice's friends: ['Charlie', 'Bob']
Charlie's friends: ['Bob', 'Alice']
```
Custom Type Converters
This example shows how to use custom type converters for specialized data transformations:
```python
from datetime import datetime, date
from neomodel import (
StructuredNode, StringProperty, DateProperty
)
from neomodel import (
config
)
from pydantic import BaseModel
from pydantic_neomodel_dict import Converter
# Set up Neomodel (Neo4j) connection - this is required!
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
config.ENCRYPTED_CONNECTION = False
config.AUTO_INSTALL_LABELS = True
# Define models
class EventPydantic(BaseModel):
title: str
event_date: datetime # Using Python datetime
class EventOGM(StructuredNode):
title = StringProperty(required=True)
event_date = DateProperty(required=True) # Neomodel (Neo4j) uses date
# Register custom type converters
Converter.register_type_converter(
datetime, date, # Convert from datetime to date
lambda dt: dt.date() # Conversion function
)
Converter.register_type_converter(
date, datetime, # Convert from date to datetime
lambda d: datetime.combine(d, datetime.min.time()) # Conversion function
)
# Register models
Converter.register_models(EventPydantic, EventOGM)
# Create a Pydantic instance with datetime
event = EventPydantic(
title="Conference",
event_date=datetime(2023, 10, 15, 9, 0, 0)
)
# Convert to Neomodel (Neo4j) OGM (datetime will be converted to date)
event_ogm = Converter.to_ogm(event)
# Convert back to Pydantic (date will be converted to datetime)
event_pydantic = Converter.to_pydantic(event_ogm)
print(f"Event: {event_pydantic.title}")
print(f"Date: {event_pydantic.event_date}")
print(f"Type: {type(event_pydantic.event_date)}")
print("Whole object:\n", event_pydantic.model_dump())
```
Output:
```
Event: Conference
Date: 2023-10-15 09:00:00
Type:
Whole object:
{'title': 'Conference', 'event_date': datetime.datetime(2023, 10, 15, 9, 0)}
```
Batch Operations
This example demonstrates batch conversion of multiple objects:
```python
from neomodel import StructuredNode, StringProperty, IntegerProperty, config
from pydantic import BaseModel
from pydantic_neomodel_dict import Converter
# Set up Neomodel (Neo4j) connection - this is required!
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
config.ENCRYPTED_CONNECTION = False
config.AUTO_INSTALL_LABELS = True
# Define models
class ProductPydantic(BaseModel):
name: str
sku: str
price: float
inventory: int
class ProductOGM(StructuredNode):
name = StringProperty(required=True)
sku = StringProperty(unique_index=True, required=True)
price = IntegerProperty(required=True)
inventory = IntegerProperty(default=0)
# Register models
Converter.register_models(ProductPydantic, ProductOGM)
# Create multiple Pydantic instances
products = [
ProductPydantic(name="Laptop", sku="LT-001", price=1299.99, inventory=10),
ProductPydantic(name="Smartphone", sku="SP-002", price=899.99, inventory=15),
ProductPydantic(name="Headphones", sku="HP-003", price=199.99, inventory=25),
ProductPydantic(name="Tablet", sku="TB-004", price=499.99, inventory=8),
ProductPydantic(name="Smartwatch", sku="SW-005", price=299.99, inventory=12)
]
# Batch convert to OGM models (all in a single transaction)
product_ogms = Converter.batch_to_ogm(products)
print(f"Converted {len(product_ogms)} products to OGM models")
# Batch convert back to Pydantic models
products_pydantic = Converter.batch_to_pydantic(product_ogms)
for product in products_pydantic:
print(product.model_dump())
```
Output:
```
Converted 5 products to OGM models
{'name': 'Laptop', 'sku': 'LT-001', 'price': 1299.99, 'inventory': 10}
{'name': 'Smartphone', 'sku': 'SP-002', 'price': 899.99, 'inventory': 15}
{'name': 'Headphones', 'sku': 'HP-003', 'price': 199.99, 'inventory': 25}
{'name': 'Tablet', 'sku': 'TB-004', 'price': 499.99, 'inventory': 8}
{'name': 'Smartwatch', 'sku': 'SW-005', 'price': 299.99, 'inventory': 12}
```
Dictionary Conversion
This example shows conversions between dictionaries and OGM models:
```python
from neomodel import StructuredNode, StringProperty, IntegerProperty, config, RelationshipTo
from pydantic_neomodel_dict import Converter
# Set up Neomodel (Neo4j) connection - this is required!
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
config.ENCRYPTED_CONNECTION = False
config.AUTO_INSTALL_LABELS = True
# Define Neomodel (Neo4j) OGM models
class AddressOGM(StructuredNode):
street = StringProperty(required=True)
city = StringProperty(required=True)
zip_code = StringProperty(required=True)
class PersonOGM(StructuredNode):
name = StringProperty(required=True)
age = IntegerProperty(required=True)
address = RelationshipTo(AddressOGM, 'LIVES_AT')
# Dictionary data with nested relationship
person_dict = {
"name": "Alex Johnson",
"age": 32,
"address": {
"street": "456 Oak Avenue",
"city": "San Francisco",
"zip_code": "94102"
}
}
# Convert dictionary to OGM model
person_ogm = Converter.dict_to_ogm(person_dict, PersonOGM)
# Convert OGM model back to dictionary
person_dict_again = Converter.ogm_to_dict(person_ogm)
print(person_dict)
print(person_dict_again)
print(f"Person: {person_dict_again['name']}, Age: {person_dict_again['age']}")
print(f"Address: {person_dict_again['address']['street']}, {person_dict_again['address']['city']}")
```
Output:
```
{'name': 'Alex Johnson', 'age': 32, 'address': {'street': '456 Oak Avenue', 'city': 'San Francisco', 'zip_code': '94102'}}
{'name': 'Alex Johnson', 'age': 32, 'address': {'street': '456 Oak Avenue', 'city': 'San Francisco', 'zip_code': '94102'}}
Person: Alex Johnson, Age: 32
Address: 456 Oak Avenue, San Francisco
```
## API Reference
### Core Methods
- `Converter.register_models(pydantic_class, ogm_class)`: Register mapping between Pydantic and OGM models
- `Converter.to_ogm(pydantic_instance, ogm_class=None, max_depth=10)`: Convert Pydantic instance to OGM
- `Converter.to_pydantic(ogm_instance, pydantic_class=None, max_depth=10)`: Convert OGM instance to Pydantic
- `Converter.dict_to_ogm(data_dict, ogm_class, max_depth=10)`: Convert dictionary to OGM instance
- `Converter.ogm_to_dict(ogm_instance, max_depth=10)`: Convert OGM instance to dictionary
### Batch Operations
- `Converter.batch_to_ogm(pydantic_instances, ogm_class=None, max_depth=10)`: Convert multiple Pydantic instances to OGM
- `Converter.batch_to_pydantic(ogm_instances, pydantic_class=None, max_depth=10)`: Convert multiple OGM instances to
Pydantic
- `Converter.batch_dict_to_ogm(data_dicts, ogm_class, max_depth=10)`: Convert multiple dictionaries to OGM instances
- `Converter.batch_ogm_to_dict(ogm_instances, max_depth=10)`: Convert multiple OGM instances to dictionaries
### Custom Type Conversion
- `Converter.register_type_converter(source_type, target_type, converter_func)`: Register custom type converter function
## Limitations
- **Default Neomodel (Neo4j) Connection**: This library uses the default `db` connection from `neomodel`, so creating OGM models
not in global scope may lead to errors. Always ensure your Neomodel (Neo4j) connection is properly configured before using the
converter.
- **Depth Limit**: Conversion has a default depth limit of 10 to prevent excessive recursion in complex object graphs.
- **Transaction Management**: The converter handles transactions internally but doesn't provide explicit transaction
control features.
- **Performance**: Converting very large object graphs may impact performance, especially with deep nesting levels.
- **Pydantic Versions**: Currently supports Pydantic 2.0.0+; compatibility with older versions is not guaranteed.
- **Node Identity**: The converter uses object identity for cycle detection, which may not work correctly in all edge
cases.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see
the [LICENSE](https://github.com/HardMax71/pydantic-neomodel-dict/blob/main/LICENSE) file for details.