Skip to content

Commit

Permalink
feat: Add PostgresUUID field
Browse files Browse the repository at this point in the history
  • Loading branch information
etimberg committed Aug 2, 2021
1 parent 5e7a9a3 commit 181ae7b
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 2 deletions.
29 changes: 27 additions & 2 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ jobs:
invoke lint
tests:
name: Tests
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10.0-beta.1']
fail-fast: false
steps:
Expand All @@ -51,6 +50,19 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: invoke test --coverage
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: DEV_USER
POSTGRES_PASSWORD: DEV_PASSWORD
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
version_checks:
name: Dependency Version Constraint Checks
runs-on: ubuntu-latest
Expand All @@ -74,6 +86,19 @@ jobs:
run: |
. $VENV/bin/activate
invoke test --coverage
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: DEV_USER
POSTGRES_PASSWORD: DEV_PASSWORD
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
dry_run:
name: Build
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ pkginfo==1.7.1
# via twine
pluggy==0.13.1
# via pytest
psycopg2-binary==2.9.1
# via ormar-postgres-extensions
py==1.10.0
# via pytest
py-githooks==1.1.0
Expand All @@ -112,6 +114,10 @@ pynacl==1.4.0
pyparsing==2.4.7
# via packaging
pytest==6.2.4
# via
# ormar-postgres-extensions
# pytest-asyncio
pytest-asyncio==0.15.1
# via ormar-postgres-extensions
readme-renderer==29.0
# via twine
Expand All @@ -129,6 +135,8 @@ rfc3986==1.5.0
# via twine
secretstorage==3.3.1
# via keyring
semver==2.13.0
# via ormar-postgres-extensions
six==1.16.0
# via
# bleach
Expand Down
14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: '3.6'
services:
postgres:
image: postgres:13
environment:
POSTGRES_USER: DEV_USER
POSTGRES_PASSWORD: DEV_PASSWORD
networks:
- local
ports:
- 5432:5432

networks:
local:
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"codecov",
"coverage[toml]",
"invoke",
"psycopg2-binary",
"pytest",
"pytest-asyncio",
]
dev_requires = [
"black",
Expand All @@ -27,6 +29,7 @@
"pip-tools",
"py-githooks",
"pygithub",
"semver",
"twine",
"wheel",
*test_requires,
Expand Down
1 change: 1 addition & 0 deletions src/ormar_postgres_extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .fields import PostgresUUID # noqa: F401
1 change: 1 addition & 0 deletions src/ormar_postgres_extensions/fields/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .uuid import PostgresUUID # noqa: F401
50 changes: 50 additions & 0 deletions src/ormar_postgres_extensions/fields/uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import uuid
from typing import (
Any,
Optional,
)

import ormar
from sqlalchemy.dialects import postgresql
from sqlalchemy.engine.default import DefaultDialect
from sqlalchemy.types import TypeDecorator


class PostgresUUIDTypeDecorator(TypeDecorator):
"""
Postgres specific GUID type for user with Ormar
"""

impl = postgresql.UUID

def process_literal_param(
self, value: Optional[uuid.UUID], dialect: DefaultDialect
) -> Optional[str]:
# Literal parameters for PG UUID values need to be quoted inside
# of single quotes
return f"'{value}'" if value is not None else None

def process_bind_param(
self, value: Optional[uuid.UUID], dialect: DefaultDialect
) -> Optional[str]:
return str(value) if value is not None else None

def process_result_value(
self, value: Optional[str], dialect: DefaultDialect
) -> Optional[uuid.UUID]:
if value is None:
return value
if not isinstance(value, uuid.UUID):
return uuid.UUID(value)
return value


class PostgresUUID(ormar.UUID):
"""
Custom UUID field for the schema that uses a native PG UUID type
"""

@classmethod
def get_column_type(cls, **kwargs: Any) -> PostgresUUIDTypeDecorator:
# Tell Ormar that this column should be a postgres UUID type
return PostgresUUIDTypeDecorator()
42 changes: 42 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pytest
import sqlalchemy

from .database import (
DATABASE_URL,
DB_NAME,
database,
metadata,
)


@pytest.fixture()
def root_engine():
root_engine = sqlalchemy.create_engine(
str(DATABASE_URL.replace(database="postgres")), isolation_level="AUTOCOMMIT"
)
return root_engine


@pytest.fixture()
def test_database(root_engine):
with root_engine.connect() as conn:
print(f"Creating test database '{DB_NAME}'")
conn.execute(f'DROP DATABASE IF EXISTS "{DB_NAME}";')
conn.execute(f'CREATE DATABASE "{DB_NAME}"')

yield

with root_engine.connect() as conn:
root_engine.execute(f'DROP DATABASE "{DB_NAME}"')


@pytest.fixture()
async def db(test_database):
# Ensure the DB has the schema we need for testing
engine = sqlalchemy.create_engine(str(DATABASE_URL))
metadata.create_all(engine)
engine.dispose()

await database.connect()
yield
await database.disconnect()
10 changes: 10 additions & 0 deletions tests/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import databases
import sqlalchemy

DB_HOST = "localhost"
DB_NAME = "TEST_DATABASE"
DATABASE_URL = databases.DatabaseURL(
f"postgres://DEV_USER:DEV_PASSWORD@{DB_HOST}:5432/{DB_NAME}"
)
database = databases.Database(str(DATABASE_URL))
metadata = sqlalchemy.MetaData()
Empty file added tests/fields/__init__.py
Empty file.
70 changes: 70 additions & 0 deletions tests/fields/test_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from typing import Optional
from uuid import (
UUID,
uuid4,
)

import ormar
import pytest

from ormar_postgres_extensions.fields import PostgresUUID
from tests.database import (
database,
metadata,
)


class UUIDTestModel(ormar.Model):
class Meta:
database = database
metadata = metadata

id: int = ormar.Integer(primary_key=True)
uid: UUID = PostgresUUID(default=uuid4)


class NullableUUIDTestModel(ormar.Model):
class Meta:
database = database
metadata = metadata

id: int = ormar.Integer(primary_key=True)
uid: Optional[UUID] = PostgresUUID(nullable=True)


@pytest.mark.asyncio
async def test_create_model_with_uuid_specified(db):
created = await UUIDTestModel(uid="2b077a49-0dbe-4dd1-88a1-9aebe3cb7653").save()
assert str(created.uid) == "2b077a49-0dbe-4dd1-88a1-9aebe3cb7653"
assert isinstance(created.uid, UUID)

# Confirm the model got saved to the DB by querying it back
found = await UUIDTestModel.objects.get()
assert found.uid == created.uid
assert isinstance(found.uid, UUID)


@pytest.mark.asyncio
async def test_get_model_by_uuid(db):
created = await UUIDTestModel(uid="2b077a49-0dbe-4dd1-88a1-9aebe3cb7653").save()

found = await UUIDTestModel.objects.filter(
uid="2b077a49-0dbe-4dd1-88a1-9aebe3cb7653"
).all()
assert len(found) == 1
assert found[0] == created


@pytest.mark.asyncio
async def test_create_model_with_nullable_uuid(db):
created = await NullableUUIDTestModel().save()
assert created.uid is None


@pytest.mark.asyncio
async def test_get_model_with_nullable_uuid(db):
created = await NullableUUIDTestModel().save()

# Ensure querying a model with a null UUID works
found = await NullableUUIDTestModel.objects.get()
assert found == created

0 comments on commit 181ae7b

Please sign in to comment.