Skip to main content

A bidirectional converter between Pydantic models, Python dictionaries and Neomodel (Neo4j) OGM models

Project description

Pydantic ↔ Neomodel (Neo4j) OGM ↔ Python Dict Converter

Ruff   MyPy   Quality Gate Status

Tests   Codecov

PyPI version   Python versions

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

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 and start neo4j container inside via docker-compose up --build.

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:

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:

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:

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:

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: <class 'datetime.datetime'>
Whole object:
 {'title': 'Conference', 'event_date': datetime.datetime(2023, 10, 15, 9, 0)}
Batch Operations

This example demonstrates batch conversion of multiple objects:

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:

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 file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_neomodel_dict-0.1.0.tar.gz (22.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_neomodel_dict-0.1.0-py3-none-any.whl (17.2 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_neomodel_dict-0.1.0.tar.gz.

File metadata

  • Download URL: pydantic_neomodel_dict-0.1.0.tar.gz
  • Upload date:
  • Size: 22.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.4

File hashes

Hashes for pydantic_neomodel_dict-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ba1f69a34afc83580c6edf04d1e8ded724d1d26be26fb0460771df931ef10aea
MD5 cc8e63fb6d0cd31d5e3b187620bff271
BLAKE2b-256 a311479729cd7fdd703bb3039b0b01da4d8193fb6f0320d10975196491144eb4

See more details on using hashes here.

File details

Details for the file pydantic_neomodel_dict-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pydantic_neomodel_dict-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e76610f8324ed36139e40a56f51ea9d3853559a0ab89731d0e7fb686661baba0
MD5 f55470beec5e9901c6b18d94f19a4f89
BLAKE2b-256 a5065e35a74ae19f6d4bac835ed8a0e2132053a5fef0901ab65565921d4ea12c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page