Skip to content

Commit

Permalink
Fixes tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gtalarico committed Jul 26, 2021
1 parent e1a0f18 commit 70ac6ca
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 32 deletions.
25 changes: 17 additions & 8 deletions airtable/orm/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ def __set__(self, instance, value):
# TODO cast
instance._fields[self.field_name] = value

def __repr__(self):
return "<{} field_name='{}'>".format(self.__class__.__name__, self.field_name)


class TextField(Field):
"""Text Field"""
Expand Down Expand Up @@ -82,7 +85,6 @@ def __init__(self, field_name: str, model: Type[T_Linked], lazy=True) -> None:
super().__init__(field_name)

def __get__(self, instance: Any, cls=None) -> Optional[T_Linked]:

if not instance:
raise ValueError("cannot access descriptors on class")

Expand All @@ -99,20 +101,27 @@ def __get__(self, instance: Any, cls=None) -> Optional[T_Linked]:
return cached_instance

# If Lazy, create empty from model class and set id
if self._lazy:
link_instance = self._model()
link_instance.id = link_id

# If not Lazy, fetch record from airtable and create new model instance
else:
link_record = instance.get_table().get(link_id)
link_instance = self._model.from_record(link_record)
link_instance = self._model.from_id(link_id, fetch=self._lazy)

# Cache instance
instance._linked_cache[link_id] = link_instance
return link_instance

return None

def __set__(self, instance, value):
is_model = isinstance(value, self._model)
if not is_model:
if not value.startswith("rec"):
raise TypeError("LinkedField string values must be a record id")
# Store link record_id
super().__set__(instance, [value])
else:
# Store instance in cache and store id
instance._linked_cache[value.id] = value
super().__set__(instance, [value.id])


class MultipleLinkField(Field, Generic[T_Linked]):
"""Multiple Link Field"""
Expand Down
73 changes: 60 additions & 13 deletions airtable/orm/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,40 @@ class Meta:
timeout: Optional[Tuple[int, int]]
typecast: bool

def __init__(self, **kwargs):
@classmethod
def descriptor_fields(cls):
"""
{
"field_name": <TextField field_name="Field Name">,
"another_Field": <NumberField field_name="Some Number">,
}
"""
return {k: v for k, v in cls.__dict__.items() if isinstance(v, Field)}

@classmethod
def descriptor_to_field_name_map(cls):
return {v.field_name: k for k, v in cls.descriptor_fields().items()}
# return {
# "Field Name": "street",
# "Street": ""
# # Docs
# }

def record_fields_to_kwargs(self):
"""{"fields": {"Street Name": "X"}} => { "street_name": "X" }"""

def __init__(self, **fields):
# To Store Fields
self._fields = {}
self._linked_cache = {}

# Get descriptors values
descriptor_fields = {
k: v for k, v in self.__class__.__dict__.items() if isinstance(v, Field)
}
# TODO check for clashes
# disallowed_names = ("id", "crreated_time", "_fields", "_linked_cache", "_table")

# Set descriptors values
for key, value in kwargs.items():
if key not in descriptor_fields:
for key, value in fields.items():
if key not in self.descriptor_fields():
msg = "invalid kwarg '{}'".format(key)
raise ValueError(msg)
setattr(self, key, value)
Expand Down Expand Up @@ -154,23 +175,49 @@ def to_record(self) -> dict:

@classmethod
def from_record(cls: Type[T], record: dict) -> T:
"""create entity instance a record (API dict response)"""
instance = cls()
instance._fields = record["fields"]
"""Create instance from record dictionary"""
map_ = cls.descriptor_to_field_name_map()
try:
# Convert Column Names into model field names
kwargs = {map_[k]: v for k, v in record["fields"].items()}
except KeyError as exc:
raise ValueError("Invalid Field Name: {} for model {}".format(exc, cls))
instance = cls(**kwargs)
instance.created_time = record["createdTime"]
instance.id = record["id"]
return instance

@classmethod
def from_id(cls: Type[T], record_id: str) -> T:
table = cls.get_table()
record = table.get(record_id)
return cls.from_record(record)
def from_id(cls: Type[T], record_id: str, fetch=True) -> T:
"""
Create an instance from a `record_id`
Args:
record_id: |arg_record_id|
Keyward Args:
fetch: If `True`, record will be fetched from airtable and fields will be
updated. If `False`, a new instance is created with the provided `id`,
but field values are unset. Default is `True`.
"""
if fetch:
table = cls.get_table()
record = table.get(record_id)
return cls.from_record(record)
else:
instance = cls()
instance.id = record_id
return instance

def reload(self):
"""Fetches field and resets instance field values from airtable record"""
if not self.id:
raise ValueError("cannot be deleted because it does not have id")

record = self.get_table().get(self.id)
self._fields = record["fields"]
self.created_time = record["createdTime"]

def __repr__(self):
return "<Model={}>".format(self.__class__.__name__)
1 change: 0 additions & 1 deletion tests/test_integration_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class Columns:
TEXT = "text" # Text
NUM = "number" # Number, float
BOOL = "boolean" # Boolean
W_QUOTE = 'Col\' with "Quotes"' # Text

return Columns

Expand Down
2 changes: 1 addition & 1 deletion tests/test_integration_orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_integration_orm(Contact, Address):
last_name="LastName",
email="email@email.com",
is_registered=True,
address=[address.id],
address=address,
)

assert contact.first_name == "John"
Expand Down
46 changes: 37 additions & 9 deletions tests/test_orm.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from unittest import mock
from requests_mock import Mocker
import pytest
from airtable import Table
from airtable.orm import Model
Expand Down Expand Up @@ -71,7 +72,7 @@ class Meta:
assert record["fields"]["First Name"] == contact.first_name


def test_from_record(mock_response_single):
def test_from_record():
class Contact(Model):

first_name = f.TextField("First Name")
Expand All @@ -82,17 +83,44 @@ class Meta:
api_key = "fake"

with mock.patch.object(Table, "get") as m_get:
m_get.return_value = mock_response_single
m_get.return_value = {
"id": "recwnBLPIeQJoYVt4",
"createdTime": "",
"fields": {"First Name": "X"},
}
contact = Contact.from_id("recwnBLPIeQJoYVt4")

assert m_get.called
assert contact.id == mock_response_single["id"]
assert contact.id == "recwnBLPIeQJoYVt4"
assert contact.first_name == "X"


def test_linked_record():
...
def test_linked_record(mock_response_single):
class Address(Model):
street = f.TextField("Street")

class Meta:
base_id = "address_base_id"
table_name = "Address"
api_key = "fake"

class Contact(Model):
address = f.LinkField("Link", Address, lazy=True)

class Meta:
base_id = "contact_base_id"
table_name = "Contact"
api_key = "fake"

record = {"id": "recFake", "createdTime": "", "fields": {"Street": "A"}}
address = Address.from_record(record)
# TODO
# address = contact2.link
# print(address.to_record())
# address.reload()
# print(address.to_record())
# contact = Contact(address=address.id)
contact = Contact(address=address)
with Mocker() as mock:
url = address.get_table().get_record_url(address.id)
mock.get(url, status_code=200, json=record)
contact.address.reload()

address = contact.address
assert address.street == "A"

0 comments on commit 70ac6ca

Please sign in to comment.