Skip to content

Commit

Permalink
Add is_formula field option
Browse files Browse the repository at this point in the history
  • Loading branch information
edocsss committed Jan 24, 2025
1 parent 34c1f04 commit 3e5d1c6
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/pyfreedb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""PyFreeDB is a Python library that provides common and simple database abstractions on top of Google Sheets."""

__version__ = "1.0.3"
__version__ = "1.0.4"
23 changes: 20 additions & 3 deletions src/pyfreedb/row/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,25 @@ class _Field(Generic[T]):
_typ: Type[T]
_column_name: str
_field_name: str

def __init__(self, column_name: str = "") -> None:
_is_formula: bool

def __init__(
self,
column_name: str = "",
is_formula: bool = False,
) -> None:
"""Defines the internal representation of the model fields.
This is where we can put per field custom config as well.
Args:
column_name: an alias of the field name to represent the actual column name in the sheets.
is_formula: a boolean indicating if the field is a formula or not. Only applicable for StringField.
"""
self._column_name = column_name
self._is_formula = is_formula

def __set_name__(self, _: Any, name: str) -> None:
self._field_name = name

if self._column_name == "":
self._column_name = name

Expand All @@ -41,8 +53,13 @@ def __set__(self, obj: Any, value: Optional[T]) -> None:
# as float by Google Sheet's API.
value = self._typ(value) # type: ignore [call-arg]

self._ensure_is_formula()
return setattr(obj._data, self._field_name, value)

def _ensure_is_formula(self):
if self._is_formula and self._typ is not str:
raise TypeError(f"Field {self._field_name} must be a StringField when is_formula is true")

def _ensure_type(self, value: Any) -> None:
if value is None or value is NotSet:
return
Expand Down
22 changes: 13 additions & 9 deletions src/pyfreedb/row/stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,10 @@ def _get_raw_values(self) -> List[List[str]]:
# Set _rid value according to the insert protocol.
raw = ["=ROW()"]

for field_name in row._fields:
value = _escape_val(getattr(row, field_name))
for field_name, field in row._fields.items():
field_is_formula = field._is_formula
raw_value = getattr(row, field_name)
value = raw_value if field_is_formula else _escape_val(raw_value)
raw.append(value)

raw_values.append(raw)
Expand Down Expand Up @@ -246,7 +248,8 @@ def _update_rows(self, indices: List[int]) -> None:
if col not in self._update_values:
continue

value = _escape_val(self._update_values[col])
field_is_formula = self._store._object_cls._fields[col]._is_formula
value = self._update_values[col] if field_is_formula else _escape_val(self._update_values[col])
cell_selector = _A1CellSelector.from_rc(col_idx + 2, row_idx)
update_range = _A1Range(self._store._sheet_name, cell_selector, cell_selector)
requests.append(_BatchUpdateRowsRequest(update_range, [[value]]))
Expand Down Expand Up @@ -320,9 +323,10 @@ def _escape_val(val: Any) -> Any:
return val


__pdoc__ = {}
__pdoc__["CountStmt"] = CountStmt.__init__.__doc__
__pdoc__["SelectStmt"] = SelectStmt.__init__.__doc__
__pdoc__["InsertStmt"] = InsertStmt.__init__.__doc__
__pdoc__["DeleteStmt"] = DeleteStmt.__init__.__doc__
__pdoc__["UpdateStmt"] = UpdateStmt.__init__.__doc__
__pdoc__ = {
"CountStmt": CountStmt.__init__.__doc__,
"SelectStmt": SelectStmt.__init__.__doc__,
"InsertStmt": InsertStmt.__init__.__doc__,
"DeleteStmt": DeleteStmt.__init__.__doc__,
"UpdateStmt": UpdateStmt.__init__.__doc__,
}
37 changes: 37 additions & 0 deletions tests/integration/test_gsheet_row_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,40 @@ def test_gsheet_row_number_boundaries(config: IntegrationTestConfig) -> None:

returned_rows = row_store.select().limit(1).execute()
assert [expected_rows[1]] == returned_rows


class InsertModel(models.Model):
value = models.StringField(is_formula=True)


class ReadModel(models.Model):
value = models.IntegerField()


@pytest.mark.integration
def test_gsheet_row_formula(config: IntegrationTestConfig) -> None:
insert_store = GoogleSheetRowStore(
config.auth_client,
spreadsheet_id=config.spreadsheet_id,
sheet_name="row_store_formula",
object_cls=InsertModel,
)
read_store = GoogleSheetRowStore(
config.auth_client,
spreadsheet_id=config.spreadsheet_id,
sheet_name="row_store_formula",
object_cls=ReadModel,
)

rows = [InsertModel(value="=ROW()-1")]
insert_store.insert(rows).execute()

expected_rows = [ReadModel(value=1)]
returned_rows = read_store.select().execute()
assert expected_rows == returned_rows

insert_store.update({"value": "=ROW()"}).execute()

expected_rows = [ReadModel(value=2)]
returned_rows = read_store.select().execute()
assert expected_rows == returned_rows
11 changes: 11 additions & 0 deletions tests/row/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,14 @@ def test_is_ieee754_safe_integer() -> None:
assert not models._is_ieee754_safe_integer(9007199254740993)

assert models._is_ieee754_safe_integer(1 << 54)


class FormulaTest(models.Model):
string_no_formula = models.StringField(is_formula=False)
string_with_formula = models.StringField(is_formula=True)


def test_field_is_formula() -> None:
f = FormulaTest(string_no_formula="", string_with_formula="")
assert not f._fields["string_no_formula"]._is_formula
assert f._fields["string_with_formula"]._is_formula

0 comments on commit 3e5d1c6

Please sign in to comment.