Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/declaring_models.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ See `TypeSystem` for [type-specific validation keyword arguments][typesystem-fie
* `orm.Enum()`
* `orm.Float()`
* `orm.Integer()`
* `orm.IPAddress()`
* `orm.String(max_length)`
* `orm.Text()`
* `orm.Time()`
Expand Down
2 changes: 2 additions & 0 deletions orm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Float,
ForeignKey,
Integer,
IPAddress,
OneToOne,
String,
Text,
Expand All @@ -37,6 +38,7 @@
"Float",
"ForeignKey",
"Integer",
"IPAddress",
"JSON",
"OneToOne",
"String",
Expand Down
10 changes: 9 additions & 1 deletion orm/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sqlalchemy
import typesystem

from orm.sqlalchemy_fields import GUID
from orm.sqlalchemy_fields import GUID, GenericIP


class ModelField:
Expand Down Expand Up @@ -242,3 +242,11 @@ def get_validator(self, **kwargs) -> typesystem.Field:

def get_column_type(self):
return sqlalchemy.String(length=self.validator.max_length)


class IPAddress(ModelField):
def get_validator(self, **kwargs) -> typesystem.Field:
return typesystem.IPAddress(**kwargs)

def get_column_type(self):
return GenericIP()
43 changes: 38 additions & 5 deletions orm/sqlalchemy_fields.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import ipaddress
import uuid

import sqlalchemy


class GUID(sqlalchemy.TypeDecorator):
"""Platform-independent GUID type.
"""
Platform-independent GUID type.

Uses PostgreSQL's UUID type, otherwise uses
CHAR(32), storing as stringified hex values.
Expand All @@ -22,15 +24,46 @@ def load_dialect_impl(self, dialect):
def process_bind_param(self, value, dialect):
if value is None:
return value
elif dialect.name == "postgresql":

if dialect.name == "postgresql":
return str(value)
else:
return "%.32x" % value.int
return value.hex

def process_result_value(self, value, dialect):
if value is None:
return value

if not isinstance(value, uuid.UUID):
value = uuid.UUID(value)
return value


class GenericIP(sqlalchemy.TypeDecorator):
"""
Platform-independent IP Address type.

Uses PostgreSQL's INET type, otherwise uses
CHAR(45), storing as stringified values.
"""

impl = sqlalchemy.CHAR
cache_ok = True

def load_dialect_impl(self, dialect):
if dialect.name == "postgresql":
return dialect.type_descriptor(sqlalchemy.dialects.postgresql.INET())
else:
if not isinstance(value, uuid.UUID):
value = uuid.UUID(value)
return dialect.type_descriptor(sqlalchemy.CHAR(45))

def process_bind_param(self, value, dialect):
if value is not None:
return str(value)

def process_result_value(self, value, dialect):
if value is None:
return value

if not isinstance(value, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
value = ipaddress.ip_address(value)
return value
14 changes: 12 additions & 2 deletions tests/test_columns.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import decimal
import ipaddress
import uuid
from enum import Enum

Expand Down Expand Up @@ -47,6 +48,7 @@ class User(orm.Model):
"id": orm.UUID(primary_key=True, default=uuid.uuid4),
"name": orm.String(allow_null=True, max_length=16),
"email": orm.Email(allow_null=True, max_length=256),
"ipaddress": orm.IPAddress(allow_null=True),
}


Expand Down Expand Up @@ -93,6 +95,14 @@ async def test_model_crud():
assert product.price == decimal.Decimal("999.99")
assert product.uuid == uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b")

user = await User.objects.create(name="Chris", email="chirs@encode.io")
user = await User.objects.create()
assert isinstance(user.pk, uuid.UUID)
assert await User.objects.get(pk=user.pk) == user

user = await User.objects.get()
assert user.email is None
assert user.ipaddress is None

await user.update(ipaddress="192.168.1.1", name="Chris", email="chirs@encode.io")

user = await User.objects.get()
assert isinstance(user.ipaddress, (ipaddress.IPv4Address, ipaddress.IPv6Address))