Skip to content

Commit

Permalink
Merge pull request #4 from workenvoy/0.0.7
Browse files Browse the repository at this point in the history
0.0.7
  • Loading branch information
rayattack committed Aug 5, 2019
2 parents 0564780 + 8f8ac77 commit 2cf7f92
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 33 deletions.
2 changes: 2 additions & 0 deletions firestore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
String,
Timestamp,
)
from .db import Connection


__all__ = [
"Array",
"Boolean",
"Byte",
"Collection",
"Connection",
"Datatype",
"Document",
"Float",
Expand Down
38 changes: 17 additions & 21 deletions firestore/containers/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,11 @@ class Document(object):
i.e. setting and saving, querying, and updating document instances.
"""

@staticmethod
def __constructor__(self, *args, **kwargs):
"""custom method to load document constraints"""
# Document constraints are the constraints found on
# fields that pertain to the entire document and not
# just the field.
# For instance required, unique, pk etc... These fields
# do not have any meaning without the larger document, and or
# Collection in the picture.
pass
@classmethod
def __autospector__(cls, *args, **kwargs):
return {
k: v for k, v in cls.__dict__.items() if k not in ["__module__", "__doc__"]
}

def __deref__(self, doc_ref):
"""
Expand Down Expand Up @@ -85,17 +80,16 @@ def __init__(self, *args, **kwargs):
# that exist on this document class.
# Useful for pk, unique, required and other document
# level validation.
self.fields_cache = {
k: v
for k, v in type(self).__dict__.items()
if k not in ["__module__", "__doc__"]
}
self.fields_cache = self.__autospector__()

for k in kwargs:
if k not in self.fields_cache.keys(): # on the fly access to obviate the need for gc
if (
k not in self.fields_cache.keys()
): # on the fly access to obviate the need for gc
raise UnknownFieldError(
f"Key {k} not found in document {type(self).__name__}"
)

self._data.add(k, kwargs.get(k))

def add_field(self, field, value):
Expand All @@ -110,7 +104,7 @@ def get_field(self, field):
Get a field form the internal _data store of field values
"""
return self._data.get(field._name)

def _presave(self):
"""
Validates inputs and ensures all required fields and other
Expand All @@ -125,11 +119,13 @@ def _presave(self):

if not v:
if f.default:
self._data.add(k, v.default)
if callable(v.default):
self._data.add(k, v.default())
self._data.add(k, f.default)
if callable(f.default):
self._data.add(k, f.default())
elif f.required:
raise ValidationError(f'{f._name} is a required field of {type(self).__name__}')
raise ValidationError(
f"{f._name} is a required field of {type(self).__name__}"
)

def save(self):
"""
Expand Down
1 change: 1 addition & 0 deletions firestore/datatypes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def __get__(self, instance, metadata):

def __set__(self, instance, value):
self.validate(value)
self.value = value
instance.add_field(self, value)

def __set_name__(self, cls, name):
Expand Down
56 changes: 51 additions & 5 deletions firestore/datatypes/map.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from .datatype import Datatype
from firestore.containers.document import Document
from firestore.datatypes.base import Base

from firestore.errors import ValidationError

class MapSchema(object):

class MapSchema(Document):
"""
A map schema defines a helper by which maps can be populated
so there is no need to use default python dicts"""

pass
def __init__(self, *args, **kwargs):
super(MapSchema, self).__init__(*args, **kwargs)


class Map(object):
class Map(Base):
"""Maps as defined by firestore represent an object saved within a document.
In python speak - A map is akin to a dictionary.
Expand All @@ -19,4 +23,46 @@ class Map(object):
"""

def __init__(self, *args, **kwargs):
pass
try:
self.map_ref = args[0]
except IndexError:
self.map_ref = None
super(Map, self).__init__(*args, **kwargs)

def __set__(self, instance, value):
self.validate(value)

# if a mapschema was passed in to the document
# field map descriptor field then it is expected
# that a mapping or dict will be the input value.
# If the input is a dict then convert it to a map schema
# and save otherwise save the map schema.
# If no mapschema was used then save the dict as is.
if self.map_ref:
value = self.map_ref(**value) if isinstance(value, dict) else value
self.value = value
instance.add_field(self, value)

def validate(self, value):
# If the map descriptor field has any
# children at all then it should be
# a MapSchema instance of keys and values
# expected in the map.
# Maps unfortunately can not be marked as pk, as
# a required field, or as having a default value.
# You can however map the keys in the
# map schema instance as required.
if self.map_ref:
if not isinstance(value, (MapSchema, dict)):
raise ValueError()
if isinstance(value, dict):
_schema = self.map_ref.__autospector__()

for k in _schema:
# get a local copy of the field instance
f = _schema.get(k)
v = value.get(k)
f.validate(v)
else:
# If mapschema then validation happens at map schema level
value._presave()
9 changes: 9 additions & 0 deletions firestore/datatypes/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ def __init__(self, *args, **kwargs):
super(String, self).__init__(*args, **kwargs)

def validate(self, value):
if not isinstance(value, str):
if not self.coerce:
raise ValueError(
f"Can not assign type {type(value)} to str and coerce is disabled"
)
value = str(value)

# Value is either a string after this point or has
# been coerced to a string
max_msg = f"{self._name} must have a maximum len of {self.maximum}, found {len(value)}"
min_msg = (
f"{self._name} must have minimum len of {self.minimum}, found {len(value)}"
Expand Down
4 changes: 4 additions & 0 deletions firestore/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from firestore.db.connection import Connection


__all__ = ["Connection"]
21 changes: 19 additions & 2 deletions firestore/db/connection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from google.cloud.client import Client
import os

import firebase_admin

from firebase_admin import credentials
from firebase_admin import firestore

# we know that these guys will not be imported with import * as they begin with an underscore
_dbs = {}
Expand All @@ -13,4 +18,16 @@ class Connection(object):
:param connection_string {str}:
"""

pass
def __init__(self, certificate):
_client = _connections.get("client")
if _client:
self._db = _client
else:
if certificate:
self.certificate = credentials.Certificate(certificate)
firebase_admin.initialize_app(self.certificate)
self._db = firestore.client()
_connections["client"] = self._db

def push(self):
pass
51 changes: 46 additions & 5 deletions tests/datatypes/map_test.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,71 @@
from unittest import TestCase
from pytest import mark

from firestore import Document, Integer, Map, String
from firestore.datatypes.map import MapSchema

from firestore.errors import ValidationError


class Mapping(MapSchema):
name = String(required=True)
name = String(required=True, default="Tiza")
age = Integer(minimum=4)

class AltMapping(MapSchema):
name = String(required=True)

class MapDocument(Document):
map = Map(required=True)
map = Map()

class MapDocumentMapping(Document):
map = Map(Mapping)


class MapTest(TestCase):
def setUp(self):
self.md = MapDocument()
self.mdm = MapDocumentMapping()

def tearDown(self):
pass

def test_map_schema(self):
def test_map_schema_validation(self):
"""Tests that the map schema object correctly transforms
to a python dict in the document instance and vis a vis"""
pass
with self.assertRaises(ValidationError):
self.mdm.map = {"name": "Peter", "age": 2}

def test_map_schema_value_error(self):
with self.assertRaises(ValueError):
self.mdm.map = {"name": "Peter", "age": "two"}

def test_document_presave_with_map(self):
mapping = Mapping()
self.mdm.map = mapping
self.assertEqual(self.mdm.map.name, "Tiza")

def test_mapschema_assignment_validation(self):
"""
Test that validations in mapschema are invoked if
a MapSchema object is used for assignment as opposed
to a dict loaded into a mapschema
"""
mapping = AltMapping()
with self.assertRaises(ValidationError):
self.mdm.map = mapping

def test_map_schema_assignment(self):
mapping = Mapping()
mapping.name = "Tiza"
self.mdm.map = mapping
self.assertEqual(self.mdm.map.name, "Tiza")

def test_map_in_document_instance(self):
"""Test when we create a map it get's loaded into the parent doc instance"""
self.md.map = {}
_map = {"name": "peter", "age": 5}
self.md.map = _map

# this is not testing self.md_data but testing that the map
# descriptor field has pushed it's value to the
# documents _data cache
self.assertEqual(self.md._data.get('map'), _map)
7 changes: 7 additions & 0 deletions tests/datatypes/string_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

class StringDocument(Document):
name = String(required=True, minimum=5, maximum=10)
email = String(coerce=False)


class StringTest(TestCase):
Expand All @@ -25,6 +26,12 @@ def test_string_minimum(self):
self.sd.name = "me"
with self.assertRaises(ValidationError):
self.sd.name = "very very very very long name"
with self.assertRaises(ValidationError):
self.sd.name = 5

def test_string_coerce(self):
with self.assertRaises(ValueError):
self.sd.email = 5

def test_string_in_document(self):
self.sd.name = "Whosand"
Expand Down
Empty file added tests/db/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions tests/db/connection_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from unittest import TestCase

from firestore import Connection


class TestConnection(TestCase):

def setUp(self):
pass

def tearDown(self):
pass

def test_firebase_connection(self):
pass

0 comments on commit 2cf7f92

Please sign in to comment.