Skip to content

Commit

Permalink
Use Cursor class (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
cariad authored Jul 21, 2023
1 parent 64d357f commit 6e36718
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 105 deletions.
1 change: 1 addition & 0 deletions docs/source/cook-book/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ These articles describe common scenarios and solutions, starting with the simple
translating
conditional-translating
udfs
udfs-with-repeating
choosing-columns
markdown-alignment
109 changes: 109 additions & 0 deletions docs/source/cook-book/udfs-with-repeating.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.. py:module:: rolumns
:noindex:

User-Defined Fields with Repeating Values
=========================================

The Problem
-----------

Sometimes you want to include repeating static values inside a group of user-defined fields.

For example, let's say we want to extend the example in :doc:`User-Defined Fields <udfs>` to repeat the same company information with every row of staff details:

.. code-block:: json
{
"company": {
"name": "Pringles IT Services"
},
"staff": [
{
"name": "Robert Pringles",
"email": "bob@pringles.pop",
"title": "CEO"
},
{
"name": "Daniel Sausage",
"email": "danny@pringles.pop",
"title": "Head Chef"
},
{
"name": "Charlie Marmalade",
"email": "charlie@pringles.pop",
"title": "CTO"
}
]
}
We'll achieve this by passing the root column set's cursor to the new user-defined field.

Code Sample
-----------

This code is similar to :doc:`User-Defined Fields <udfs>`, but note that the new "Company" column attaches itself to the root column set's cursor. This override allows the "company.name" path to resolve to the "company" object in the root of the input data rather than a non-existant "company" object inside each staff member's record.

.. testcode::

from rolumns import ByUserDefinedFields, Columns, Source
from rolumns.renderers import RowsRenderer

data = {
"company": {
"name": "Pringles IT Services"
},
"staff": [
{
"name": "Robert Pringles",
"email": "bob@pringles.pop",
"title": "CEO"
},
{
"name": "Daniel Sausage",
"email": "danny@pringles.pop",
"title": "Head Chef"
},
{
"name": "Charlie Marmalade",
"email": "charlie@pringles.pop",
"title": "CTO"
}
]
}

columns = Columns()

staff = columns.group("staff")
staff.add("Name", "name")

by_udf = ByUserDefinedFields()
by_udf.append("Email", "email")
by_udf.append("Title", "title")
by_udf.append("Company", Source("company.name", cursor=columns.cursor))

udfs = staff.group(by_udf)
udfs.add("Property", ByUserDefinedFields.NAME)
udfs.add("Value", ByUserDefinedFields.VALUE)

renderer = RowsRenderer(columns)
rows = renderer.render(data)

print(list(rows))

Result
------

.. testoutput::
:options: +NORMALIZE_WHITESPACE

[['Name', 'Property', 'Value'],
['Robert Pringles', 'Email', 'bob@pringles.pop'],
['Robert Pringles', 'Title', 'CEO'],
['Robert Pringles', 'Company', 'Pringles IT Services'],
['Daniel Sausage', 'Email', 'danny@pringles.pop'],
['Daniel Sausage', 'Title', 'Head Chef'],
['Daniel Sausage', 'Company', 'Pringles IT Services'],
['Charlie Marmalade', 'Email', 'charlie@pringles.pop'],
['Charlie Marmalade', 'Title', 'CTO'],
['Charlie Marmalade', 'Company', 'Pringles IT Services']]
58 changes: 32 additions & 26 deletions rolumns/columns.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union

from rolumns.by_path import ByPath
from rolumns.column import Column
from rolumns.cursor import Cursor
from rolumns.exceptions import MultipleGroups
from rolumns.group import Group
from rolumns.source import Source
Expand All @@ -27,9 +27,15 @@ class Columns:
columns.add("Email", "email")
"""

def __init__(self, group: Optional[Union[Group, str]] = None) -> None:
def __init__(
self,
cursor: Optional[Union[Cursor, Group, str]] = None,
) -> None:
if isinstance(cursor, (Group, str)) or cursor is None:
cursor = Cursor(cursor)

self._columns: List[Column] = []
self._group = group if isinstance(group, Group) else ByPath(group)
self._cursor = cursor
self._grouped_set: Optional[Columns] = None

def add(
Expand Down Expand Up @@ -74,7 +80,18 @@ def add(
column = Column(name, source)
self._columns.append(column)

def group(self, group: Union[Group, str]) -> "Columns":
@property
def cursor(self) -> Cursor:
"""
Cursor.
"""

return self._cursor

def group(
self,
cursor: Union[Cursor, Group, str],
) -> "Columns":
"""
Creates and adds a grouped column set.
Expand Down Expand Up @@ -106,8 +123,10 @@ def group(self, group: Union[Group, str]) -> "Columns":
if self._grouped_set:
raise MultipleGroups()

group = group if isinstance(group, Group) else ByPath(group)
self._grouped_set = Columns(group)
if isinstance(cursor, (Group, str)):
cursor = self._cursor.group(cursor)

self._grouped_set = Columns(cursor)
return self._grouped_set

def names(self) -> List[str]:
Expand All @@ -125,15 +144,15 @@ def names(self) -> List[str]:

return names

def normalize(self, data: Any) -> List[Dict[str, Any]]:
def normalize(self) -> List[Dict[str, Any]]:
"""
Normalises `data` into a list of dictionaries describing column names
and values.
"""

result: List[Dict[str, Any]] = []

for record in self.records(data):
for record in self._cursor:
resolved: Dict[str, Any] = {}

for column in self._columns:
Expand All @@ -144,8 +163,8 @@ def normalize(self, data: Any) -> List[Dict[str, Any]]:
raise Exception("Encountered multiple values")

if self._grouped_set:
key = self._grouped_set._group.name()
resolved[key] = self._grouped_set.normalize(record)
key = self._grouped_set._cursor.cursor_group.name()
resolved[key] = self._grouped_set.normalize()

result.append(resolved)

Expand Down Expand Up @@ -199,23 +218,10 @@ def normalized_to_column_values(

return filled_columns

def records(self, data: Any) -> Iterable[Any]:
"""
Gets an iterable list of the records of `data` described by this column
set's grouping.
"""

for record in self._group.resolve(data):
if isinstance(record, list):
for d in record:
yield d
else:
yield record

def to_column_values(self, data: Any) -> Dict[str, List[Any]]:
def to_column_values(self) -> Dict[str, List[Any]]:
"""
Translates `data` to a dictionary of column names and values.
"""

normalized = self.normalize(data)
normalized = self.normalize()
return Columns.normalized_to_column_values(normalized)
4 changes: 2 additions & 2 deletions rolumns/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ class Cursor:

def __init__(
self,
group: Union[Group, str],
group: Optional[Union[Group, str]] = None,
parent: Optional[Cursor] = None,
) -> None:
if isinstance(group, str):
if isinstance(group, str) or group is None:
group = ByPath(group)

self._data: Optional[Any] = None
Expand Down
10 changes: 8 additions & 2 deletions rolumns/renderers/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ def pad(

return padding + value + " "

def render(self, data: Any) -> Iterable[str]:
def render(
self,
data: Optional[Any] = None,
) -> Iterable[str]:
"""
Translates :code:`data` into an iterable list of strings that make up a
Markdown table row-by-row.
Expand All @@ -153,7 +156,10 @@ def render(self, data: Any) -> Iterable[str]:
if index == 0:
yield self._make_header_separator(len(row))

def render_string(self, data: Any) -> str:
def render_string(
self,
data: Optional[Any] = None,
) -> str:
"""
Translates :code:`data` into a Markdown table.
"""
Expand Down
10 changes: 8 additions & 2 deletions rolumns/renderers/rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@ def append(self, column: str) -> None:

self._mask.append(column)

def render(self, data: Any) -> Iterable[List[Any]]:
def render(
self,
data: Optional[Any] = None,
) -> Iterable[List[Any]]:
"""
Translates :code:`data` into an iterable list of rows.
"""

if data is not None:
self._columns.cursor.load(data)

column_ids = self._mask or self._columns.names()
yield column_ids

columns = self._columns.to_column_values(data)
columns = self._columns.to_column_values()
height = 0

for _, value in columns.items():
Expand Down
6 changes: 6 additions & 0 deletions rolumns/source.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any, Iterable, Optional

from rolumns.cursor import Cursor
from rolumns.data_resolver import DataResolver
from rolumns.exceptions import TranslationFailed
from rolumns.translation_state import TranslationState
Expand Down Expand Up @@ -40,8 +41,10 @@ class Source:
def __init__(
self,
path: Optional[str],
cursor: Optional[Cursor] = None,
translator: Optional[Translator] = None,
) -> None:
self._cursor = cursor
self._path = path
self._translator = translator

Expand All @@ -50,6 +53,9 @@ def read(self, record: Any) -> Iterable[Any]:
Yields each prescribed value of :code:`record`.
"""

if self._cursor is not None:
record = self._cursor.current

for datum in DataResolver(record).resolve(self._path):
if self._translator:
state = TranslationState(
Expand Down
21 changes: 21 additions & 0 deletions tests/data/0013-expect-repeat-static.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
["Boss", "Device", "Number"],
["The Cheese Boss", "Fax", "+44 000 000"],
["The Cheese Boss", "VOIP", "+45 000 000"],
["The Cheese Boss", "Emergency", "+46 000 000"],
["The Cheese Boss", "Fax", "+44 000 001"],
["The Cheese Boss", "VOIP", "+45 000 001"],
["The Cheese Boss", "Emergency", "+46 000 000"],
["The Cheese Boss", "Fax", "+44 000 002"],
["The Cheese Boss", "VOIP", "+45 000 002"],
["The Cheese Boss", "Emergency", "+46 000 000"],
["The Bean Boss", "Fax", "+44 001 000"],
["The Bean Boss", "VOIP", "+45 001 000"],
["The Bean Boss", "Emergency", "+46 001 000"],
["The Bean Boss", "Fax", "+44 001 001"],
["The Bean Boss", "VOIP", "+45 001 001"],
["The Bean Boss", "Emergency", "+46 001 000"],
["The Bean Boss", "Fax", "+44 001 002"],
["The Bean Boss", "VOIP", "+45 001 002"],
["The Bean Boss", "Emergency", "+46 001 000"]
]
6 changes: 6 additions & 0 deletions tests/data/0013-input.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"static": {
"boss_name": {
"value": "The Cheese Boss"
},
"emergency_phone": {
"value": "+46 000 000"
}
},
"repeating": {
Expand Down Expand Up @@ -38,6 +41,9 @@
"static": {
"boss_name": {
"value": "The Bean Boss"
},
"emergency_phone": {
"value": "+46 001 000"
}
},
"repeating": {
Expand Down
28 changes: 28 additions & 0 deletions tests/renderers/test_rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,31 @@ def test_udf_from_repeating_group() -> None:
udfs.add("Number", "value")

assert list(RowsRenderer(cs).render(inp)) == exp


def test_udf_repeat_static() -> None:
(inp, exp) = load_test_case(13, expect_variant="repeat-static")

cs = Columns(ByKey())
cs.add("Boss", ByKey.value("static.boss_name.value"))

fund_group = cs.group(ByKey.value("repeating.contact_details"))

udfs = fund_group.group(
ByUserDefinedFields(
UserDefinedField("Fax", "fax.value"),
UserDefinedField("VOIP", "voip.value"),
UserDefinedField(
"Emergency",
Source(
ByKey.value("static.emergency_phone.value"),
cursor=cs.cursor,
),
),
)
)

udfs.add("Device", "name")
udfs.add("Number", "value")

assert list(RowsRenderer(cs).render(inp)) == exp
Loading

0 comments on commit 6e36718

Please sign in to comment.