Skip to content

Commit

Permalink
Initial implementation of saving models
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim020 committed Mar 19, 2024
1 parent a193ad4 commit 67bf681
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 5 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ from couchbase.cluster import Cluster

from couch_potato.couch_potato import CouchPotato
from couch_potato.model import KeyGenerator
from couch_potato.fields import String
from couch_potato.fields import String, Integer

cluster = Cluster(
'couchbase://localhost',
Expand All @@ -27,8 +27,14 @@ class UserModel(couch_potato.Model):
__key_generator__ = KeyGenerator("User::{name}")

name = String()
age = Integer()

if __name__ == "__main__":
# Get the model from the database
a: UserModel = UserModel.get(name="test")
print(a.name)
print(a.name, a.age)
# Update one of the instance attributes, and save the model
a.age = 30
a.save()

```
1 change: 1 addition & 0 deletions couch-potato/_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def __new__(cls, name, bases, dct):

# Set up internal attributes on the class defn
# 1. Set the __fields__ attribute on the class
# TODO: Validate that field names do not conflict with reserved named
fields: Dict[str, Field] = dict()
for name, attr in dct.items():
if isinstance(attr, Field):
Expand Down
4 changes: 3 additions & 1 deletion couch-potato/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ class BucketBind:
class Field(metaclass=ABCMeta):
__type__: Type

def __init__(self, nullable: bool = True):
def __init__(self, nullable: bool = True, read_only: bool = False):
self._nullable = nullable
self._read_only = read_only

@classmethod
def ensure_type(cls, value: Any):
Expand All @@ -32,6 +33,7 @@ def ensure_type(cls, value: Any):
f"{cls.__type__} but got {type(value)}")

def serialize(self, value):
self.ensure_type(value)
return value

def deserialize(self, value):
Expand Down
4 changes: 4 additions & 0 deletions couch-potato/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ class FieldNotFound(ModelError):

class ModelAttributeError(ModelError):
pass


class ReadOnlyError(ModelError):
pass
50 changes: 48 additions & 2 deletions couch-potato/model.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from string import Formatter
from typing import Any, TypeVar, Type
from typing import TypeVar, Type

from couchbase.collection import Collection
from couchbase.options import InsertOptions, ReplaceOptions
from couchbase.scope import Scope

from errors import ModelAttributeError, FieldNotFound
from errors import ModelAttributeError, FieldNotFound, ReadOnlyError

T = TypeVar('T', bound='BaseModel')

Expand All @@ -26,6 +27,7 @@ class BaseModel:
__scope__: str = Scope.default_name()
__collection__: str = Collection.default_name()
__key_generator__: KeyGenerator
__read_only__: bool = False

def __init__(self, **kwargs):
for key, value in kwargs.items():
Expand Down Expand Up @@ -84,3 +86,47 @@ def from_json(cls: Type[T], **kwargs) -> T:
# Create an instance of the class
instance = cls(**kwargs)
return instance

# TODO: Add kwargs for supporting InsertOptions and ReplaceOptions,
# such as timeout and expiry
def save(self):
# TODO: Move this logic inside of a __setattr__, so that any attempt to
# modify fields on the model is rejected and cannot actually modify state
if self.__read_only__:
raise ReadOnlyError("Cannot save read-only model")

if not hasattr(self, "__fields__"):
raise Exception("Unable to save model, as no fields defined")

serialized = self.to_json()
doc_key = self.__key_generator__.generate(**{
key: getattr(self, key) for key in self.__key_generator__.format_keys
})

if hasattr(self, "__cas__"):
# Updating the document
# Update the raw dictionary with the values of the fields
# defined in the model, but leave everything else which not
# a defined field as it was
raw_doc = getattr(self, "__raw__", {})
for key, value in serialized.items():
raw_doc[key] = value
serialized = raw_doc
options = ReplaceOptions(cas=self.__cas__)
# TODO: What happens if the key for this document changes?
mutation_result = self.bind().replace(doc_key, serialized, options)
else:
# Inserting the document
options = InsertOptions()
mutation_result = self.bind().insert(doc_key, serialized, options)
# Set the class attributes updated to the values in the mutation result, so that this
# instance can then be reused without needing to fetch again
setattr(self, "__raw__", serialized)
setattr(self, "__cas__", mutation_result.cas)
setattr(self, "__key__", mutation_result.key)

def to_json(self) -> dict:
ret = {}
for key, field in self.__fields__.items():
ret[key] = field.serialize(getattr(self, key))
return ret

0 comments on commit 67bf681

Please sign in to comment.