Repository: joshtemple/lkml

Issue:
Error when serializing query filters
The following demonstrates the error:
`>>> import lkml
>>> raw_lookml = """explore: foo { query: goo { filters: [zoo: "error"]}}"""
>>> content = lkml.load(raw_lookml)
>>> lkml.dump(content)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/__init__.py", line 68, in dump
    tree: DocumentNode = parser.parse(obj)
                         ^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 266, in parse
    nodes = [self.parse_any(key, value) for key, value in obj.items()]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 266, in <listcomp>
    nodes = [self.parse_any(key, value) for key, value in obj.items()]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 315, in parse_any
    return self.expand_list(key, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 290, in expand_list
    return flatten([self.parse_any(singular_key, value) for value in values])
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 290, in <listcomp>
    return flatten([self.parse_any(singular_key, value) for value in values])
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 324, in parse_any
    return self.parse_block(key, to_parse, name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 346, in parse_block
    nodes = [self.parse_any(key, value) for key, value in items.items()]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 346, in <listcomp>
    nodes = [self.parse_any(key, value) for key, value in items.items()]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 315, in parse_any
    return self.expand_list(key, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 290, in expand_list
    return flatten([self.parse_any(singular_key, value) for value in values])
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 290, in <listcomp>
    return flatten([self.parse_any(singular_key, value) for value in values])
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 324, in parse_any
    return self.parse_block(key, to_parse, name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 346, in parse_block
    nodes = [self.parse_any(key, value) for key, value in items.items()]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 346, in <listcomp>
    nodes = [self.parse_any(key, value) for key, value in items.items()]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 317, in parse_any
    return self.parse_list(key, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dotan/dev/delphi_backend/venv/lib/python3.11/site-packages/lkml/simple.py", line 409, in parse_list
    [(key, val)] = value.items()
                   ^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'items'`

I've looked at the code and realized filters are a topic this library already confronted in the past. It doesn't look like the matter is resolved. Regretfully my lookml knowledge is not on par to be able to propose a working fix contribution, for that I apologize.

Hints from issue comments:
@joshtemple: Hey @dotans-delphi, thanks for reporting this. It seems to be unique to it being a `query`. If I change this to a `view` with a dimension with filters, for example, it works as expected. I'll continue to dig in here and try to get a fix for you.@joshtemple: Ah, it's because of the behavior added in the fix for #53.@dotans-delphi: Yeah. Btw we use it at my workplace and we'd be happy to contribute. I would have gone for a fix contribution, but wasn't sure of the right approach once I saw the special handling filters get. There's the plural form, and the query construct getting exempt during parsing, and then of course the unique flow for filters. I'm still ramping up my lookml, so opted not to make a mess..
Anyhow, thanks so much for looking into this!! (and for this lib, while at it)@joshtemple: I've released the fix in v1.3.6 which should now be live on PyPI. Could you give it a try for me?@dotans-delphi: Tested, works!
Amazing turn-around time, dude.. thanks so much!!@joshtemple: You’re welcome, glad it was a quick fix!

On Thu Nov 21, 2024, 09:48 PM GMT, dotans-delphi ***@***.***> wrote:
> Tested, works!
> Amazing turn-around time, dude.. thanks so much!!
> —
> Reply to this email directly, view it on GitHub <https://github.com/joshtemple/lkml/issues/96#issuecomment-2492406357>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/ACCFHKY2EIJYE5PISENKGKL2BZILLAVCNFSM6AAAAABSHKWVVSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDIOJSGQYDMMZVG4>.
> You are receiving this because you modified the open/close state.Message ID: ***@***.***>

Solution patch:
diff --git a/lkml/simple.py b/lkml/simple.py
index 0d3b651..275b85c 100644
--- a/lkml/simple.py
+++ b/lkml/simple.py
@@ -226,7 +226,7 @@ def is_plural_key(self, key: str) -> bool:
                 singular_key == "allowed_value"
                 and self.parent_key.rstrip("s") == "access_grant"
             )
-            and not (self.parent_key == "query")
+            and not (self.parent_key == "query" and singular_key != "filters")
         )
 
     def resolve_filters(self, values: List[dict]) -> Union[List[BlockNode], ListNode]:


Test patch:
diff --git a/tests/test_simple.py b/tests/test_simple.py
index 2c5a748..be7e84d 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -268,7 +268,7 @@ def test_parse_query(parser):
             {
                 "name": "query_one",
                 "dimensions": ["dimension_one", "dimension_two"],
-                "measures": ["measure_one"]
+                "measures": ["measure_one"],
             }
         ]
     }
@@ -284,6 +284,32 @@ def test_parse_query(parser):
     )
 
 
+def test_parse_query_with_filters(parser):
+    obj = {
+        "explores": [
+            {
+                "queries": [{"filters__all": [[{"baz": "expression"}, {"qux": "expression"}]], "name": "bar"}],
+                "name": "foo",
+            }
+        ]
+    }
+    node = parser.parse(obj)
+    result = str(node)
+    print(result)
+    assert result == "\n".join(
+        (
+            "explore: foo {",
+            "  query: bar {",
+            "    filters: [",
+            '      baz: "expression",',
+            '      qux: "expression",',
+            "    ]",
+            "  }",
+            "}",
+        )
+    )
+
+
 def test_resolve_filters_filter_only_field(parser):
     nodes = parser.resolve_filters(
         [{"name": "filter_a", "type": "string"}, {"name": "filter_b", "type": "number"}]