Repository: gouline/dbt-metabase

Issue:
Add wildcard syntax to table filters
Certain models - such as staging tables - might not be a part of Metabase's table definitions. As opposed to having to specify all includes or excludes, `dbtmetabase` should allow you to define a wildcard to exclude them, i.e.

```python
        c.export_models(
            metabase_database="my_database",
            schema_filter=Filter(include=["PUBLIC", "SNAPSHOTS"], exclude=["STG_*"]),
            skip_sources=True,
        )
```

Discussion:

Patch:
diff --git a/Makefile b/Makefile
index ee21a27..068fca0 100644
--- a/Makefile
+++ b/Makefile
@@ -77,7 +77,7 @@ sandbox-models:
 		--metabase-username $$MB_USER \
 		--metabase-password $$MB_PASSWORD \
 		--metabase-database $$POSTGRES_DB \
-		--include-schemas "public",other \
+		--include-schemas "pub*",other \
 		--http-header x-dummy-key dummy-value \
 		--order-fields \
 		--verbose )
diff --git a/README.md b/README.md
index 673aa3c..cb6e9a3 100644
--- a/README.md
+++ b/README.md
@@ -211,7 +211,7 @@ dbt-metabase exposures \
     --metabase-username user@example.com \
     --metabase-password Password123 \
     --output-path models/ \
-    --exclude-collections temporary
+    --exclude-collections "temp*"
 ```
 
 Once the execution completes, check your output path for exposures files containing descriptions, creator details and links for Metabase questions and dashboards:
@@ -295,7 +295,7 @@ c.export_models(
 # Extracting exposures
 c.extract_exposures(
     output_path=".",
-    collection_filter=Filter(exclude=["temporary"]),
+    collection_filter=Filter(exclude=["temp*"]),
 )
 ```
 
diff --git a/dbtmetabase/format.py b/dbtmetabase/format.py
index 7ef2b29..862a715 100644
--- a/dbtmetabase/format.py
+++ b/dbtmetabase/format.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import fnmatch
 import logging
 import re
 from logging.handlers import RotatingFileHandler
@@ -29,9 +30,18 @@ class Filter:
 
     def match(self, item: str) -> bool:
         item = self._norm_item(item)
-        included = not self.include or item in self.include
-        excluded = self.exclude and item in self.exclude
-        return included and not excluded
+
+        for exclude in self.exclude:
+            if fnmatch.fnmatch(item, exclude):
+                return False
+
+        if self.include:
+            for include in self.include:
+                if fnmatch.fnmatch(item, include):
+                    return True
+            return False
+
+        return True
 
     @staticmethod
     def _norm_arg(arg: Optional[Sequence[str]]) -> Sequence[str]:


Test patch:
diff --git a/tests/test_format.py b/tests/test_format.py
index b24d172..9b48af6 100644
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -27,6 +27,15 @@ class TestFormat(unittest.TestCase):
         self.assertTrue(Filter(include="alpha").match("Alpha"))
         self.assertFalse(Filter(exclude="alpha").match("Alpha"))
 
+    def test_filter_wildcard(self):
+        self.assertTrue(Filter(include="stg_*").match("stg_orders"))
+        self.assertTrue(Filter(include="STG_*").match("stg_ORDERS"))
+        self.assertFalse(Filter(include="stg_*").match("orders"))
+        self.assertTrue(Filter(include="order?").match("orders"))
+        self.assertFalse(Filter(include="order?").match("ordersz"))
+        self.assertTrue(Filter(include="*orders", exclude="stg_*").match("_orders"))
+        self.assertFalse(Filter(include="*orders", exclude="stg_*").match("stg_orders"))
+
     def test_null_value(self):
         self.assertIsNotNone(NullValue)
         self.assertFalse(NullValue)