From 085735029f305c9fc6d4d2ae4d6808849e4cd963 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 31 Aug 2021 16:30:31 -0700 Subject: [PATCH] Use UUID4 --- redis_developer/orm/model.py | 55 ++++++++++++---------------- redis_developer/orm/util.py | 71 ------------------------------------ 2 files changed, 23 insertions(+), 103 deletions(-) delete mode 100644 redis_developer/orm/util.py diff --git a/redis_developer/orm/model.py b/redis_developer/orm/model.py index f10e42d..8ccba20 100644 --- a/redis_developer/orm/model.py +++ b/redis_developer/orm/model.py @@ -1,4 +1,3 @@ -import datetime from dataclasses import dataclass from enum import Enum from typing import ( @@ -14,7 +13,9 @@ from typing import ( TypeVar, Union, Sequence, ClassVar, TYPE_CHECKING, no_type_check, + Protocol ) +import uuid import redis from pydantic import BaseModel @@ -25,7 +26,6 @@ from pydantic.typing import NoArgAnyCallable, resolve_annotations from pydantic.utils import Representation from .encoders import jsonable_encoder -from .util import uuid_from_time _T = TypeVar("_T") @@ -51,6 +51,16 @@ class Expression: right_value: Any +class PrimaryKeyCreator(Protocol): + def create_pk(self, *args, **kwargs): + """Create a new primary key""" + + +class Uuid4PrimaryKey: + def create_pk(self): + return str(uuid.uuid4()) + + class ExpressionProxy: def __init__(self, field: ModelField): self.field = field @@ -82,12 +92,14 @@ class FieldInfo(PydanticFieldInfo): foreign_key = kwargs.pop("foreign_key", Undefined) index = kwargs.pop("index", Undefined) unique = kwargs.pop("unique", Undefined) + primary_key_creator_cls = kwargs.pop("primary_key_creator_cls", Undefined) super().__init__(default=default, **kwargs) self.primary_key = primary_key self.nullable = nullable self.foreign_key = foreign_key self.index = index self.unique = unique + self.primary_key_creator_cls = primary_key_creator_cls class RelationshipInfo(Representation): @@ -131,6 +143,7 @@ def Field( foreign_key: Optional[Any] = None, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, + primary_key_creator_cls: Optional[PrimaryKeyCreator] = Uuid4PrimaryKey, schema_extra: Optional[Dict[str, Any]] = None, ) -> Any: current_schema_extra = schema_extra or {} @@ -159,6 +172,7 @@ def Field( foreign_key=foreign_key, nullable=nullable, index=index, + primary_key_creator_cls=primary_key_creator_cls, **current_schema_extra, ) field_info._validate() @@ -277,25 +291,10 @@ class RedisModel(BaseModel, metaclass=RedisModelMetaclass): if not hasattr(cls.Meta, 'primary_key_pattern'): cls.Meta.primary_key_pattern = f"{cls.Meta.primary_key.name}:{{pk}}" - def __init__(__pydantic_self__, **data: Any) -> None: - # Uses something other than `self` the first arg to allow "self" as a - # settable attribute - if TYPE_CHECKING: - __pydantic_self__.__dict__: Dict[str, Any] = {} - __pydantic_self__.__fields_set__: Set[str] = set() - - values, fields_set, validation_error = validate_model( - __pydantic_self__.__class__, data - ) - - if validation_error: - raise validation_error - + super().__init__(**data) __pydantic_self__.validate_primary_key() - object.__setattr__(__pydantic_self__, '__dict__', values) - @classmethod @no_type_check def _get_value(cls, *args, **kwargs) -> Any: @@ -314,10 +313,8 @@ class RedisModel(BaseModel, metaclass=RedisModelMetaclass): """Check for a primary key. We need one (and only one).""" primary_keys = 0 for name, field in cls.__fields__.items(): - if field.field_info.primary_key: + if getattr(field.field_info, 'primary_key', None): primary_keys += 1 - - # TODO: Automatically create a primary key field instead? if primary_keys == 0: raise RedisModelError("You must define a primary key for the model") elif primary_keys > 1: @@ -330,9 +327,9 @@ class RedisModel(BaseModel, metaclass=RedisModelMetaclass): return f"{global_prefix}{model_prefix}{part}" @classmethod - def make_primary_key(self, pk: Any): + def make_primary_key(cls, pk: Any): """Return the Redis key for this model.""" - return self.make_key(self.Meta.primary_key_pattern.format(pk=pk)) + return cls.make_key(cls.Meta.primary_key_pattern.format(pk=pk)) def key(self): """Return the Redis key for this model.""" @@ -341,7 +338,7 @@ class RedisModel(BaseModel, metaclass=RedisModelMetaclass): @classmethod def get(cls, pk: Any): - # TODO: Getting related objects + # TODO: Getting related objects? document = cls.db().hgetall(cls.make_primary_key(pk)) if not document: raise NotFoundError @@ -373,7 +370,7 @@ class RedisModel(BaseModel, metaclass=RedisModelMetaclass): return cls def delete(self): - # TODO: deleting relationships + # TODO: deleting relationships? return self.db().delete(self.key()) def save(self) -> 'RedisModel': @@ -383,15 +380,9 @@ class RedisModel(BaseModel, metaclass=RedisModelMetaclass): pk = document[pk_field.name] if not pk: - pk = str(uuid_from_time(datetime.datetime.now())) + pk = pk_field.field_info.primary_key_creator_cls().create_pk() setattr(self, pk_field.name, pk) document[pk_field.name] = pk success = self.db().hset(self.key(), mapping=document) return success - - Meta = DefaultMeta - - def __init__(self, **data: Any) -> None: - """Validate that a model instance has a primary key.""" - super().__init__(**data) diff --git a/redis_developer/orm/util.py b/redis_developer/orm/util.py deleted file mode 100644 index 7e44959..0000000 --- a/redis_developer/orm/util.py +++ /dev/null @@ -1,71 +0,0 @@ -# Adapted from the Cassandra Python driver. -# -# Copyright DataStax, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import calendar -import random -import uuid - - -def uuid_from_time(time_arg, node=None, clock_seq=None): - """ - Converts a datetime or timestamp to a type 1 :class:`uuid.UUID`. - - :param time_arg: - The time to use for the timestamp portion of the UUID. - This can either be a :class:`datetime` object or a timestamp - in seconds (as returned from :meth:`time.time()`). - :type datetime: :class:`datetime` or timestamp - - :param node: - None integer for the UUID (up to 48 bits). If not specified, this - field is randomized. - :type node: long - - :param clock_seq: - Clock sequence field for the UUID (up to 14 bits). If not specified, - a random sequence is generated. - :type clock_seq: int - - :rtype: :class:`uuid.UUID` - - """ - if hasattr(time_arg, 'utctimetuple'): - seconds = int(calendar.timegm(time_arg.utctimetuple())) - microseconds = (seconds * 1e6) + time_arg.time().microsecond - else: - microseconds = int(time_arg * 1e6) - - # 0x01b21dd213814000 is the number of 100-ns intervals between the - # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. - intervals = int(microseconds * 10) + 0x01b21dd213814000 - - time_low = intervals & 0xffffffff - time_mid = (intervals >> 32) & 0xffff - time_hi_version = (intervals >> 48) & 0x0fff - - if clock_seq is None: - clock_seq = random.getrandbits(14) - else: - if clock_seq > 0x3fff: - raise ValueError('clock_seq is out of range (need a 14-bit value)') - - clock_seq_low = clock_seq & 0xff - clock_seq_hi_variant = 0x80 | ((clock_seq >> 8) & 0x3f) - - if node is None: - node = random.getrandbits(48) - - return uuid.UUID(fields=(time_low, time_mid, time_hi_version, - clock_seq_hi_variant, clock_seq_low, node), version=1)