From 18e6edc7628c24ee477752b3d19bb859978e94c2 Mon Sep 17 00:00:00 2001 From: wakinniranye31 Date: Sat, 18 Jul 2026 15:52:38 +0100 Subject: [PATCH] Wrap Web.Contents so functions using it aren't excluded from M.pq Web.Contents was excluded entirely from the generated Expression.Evaluate environment because passing it in directly (Web.Contents = Web.Contents) triggers the 'can't use a Dynamic Data Source' Formula.Firewall error. That meant any custom function using Web.Contents internally (e.g. GetSPList, GitHubRepoFiles, Countries) would throw during evaluation and silently fall back to displaying as raw text. Instead of excluding Web.Contents, map it to a same-signature wrapper function defined in M.pq's own let block: #"Web.Contents wrapper" = (url as text, optional options as nullable record) as any => Web.Contents(url, options) Passing the wrapper (rather than Web.Contents itself) through Expression.Evaluate's environment is the standard community workaround for this exact Formula.Firewall limitation. Closes #1 --- M.pq | 20 ++++++++++++++++---- M_Creator.py | 33 +++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/M.pq b/M.pq index dfe76b1..c6d2088 100644 --- a/M.pq +++ b/M.pq @@ -8,10 +8,17 @@ let PAT = "", // Personal Access Token (PAT) for GitHub API https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens QueryHeaders = if PAT <> "" then - [Authorization = "Bearer " & PAT] + [Authorization = "Bearer " & PAT] else [], + // Wrapping Web.Contents in a same-signature function lets it be passed into + // Expression.Evaluate's environment without tripping the "can't use a Dynamic + // Data Source" Formula.Firewall error that occurs when Web.Contents itself is + // passed around as a first-class value. + #"Web.Contents wrapper" = (url as text, optional options as nullable record) as any => + Web.Contents(url, options), + #"Get Trees all trees" = Json.Document( Web.Contents( BaseURL,[ @@ -70,7 +77,7 @@ let Text.FromBinary( Binary.FromText(#"Get functions fx") ), - [ + [ Any.Type = Any.Type, Binary.FromText = Binary.FromText, Binary.ToText = Binary.ToText, @@ -110,6 +117,7 @@ let DateTimeZone.ZoneMinutes = DateTimeZone.ZoneMinutes, Day.Friday = Day.Friday, Day.Monday = Day.Monday, + Documentation.InspiredBy = Documentation.InspiredBy, Duration.Days = Duration.Days, Duration.From = Duration.From, Duration.Hours = Duration.Hours, @@ -233,8 +241,12 @@ let Type.Is = Type.Is, Value.ReplaceMetadata = Value.ReplaceMetadata, Value.ReplaceType = Value.ReplaceType, - Value.Type = Value.Type - //,Web.Contents = Web.Contents //Unfortunately adding this function to the M code will create a dynamic error :( + Value.Type = Value.Type, + api.github = api.github, + docs.github = docs.github, + e.g = e.g, + vnd.github = vnd.github, + Web.Contents = #"Web.Contents wrapper" ] ) in diff --git a/M_Creator.py b/M_Creator.py index 1d79817..b575a9b 100644 --- a/M_Creator.py +++ b/M_Creator.py @@ -34,7 +34,10 @@ 'Text.ContainsAny', 'Text.ListFromString', 'Text.ReplaceMany', - 'Web.Contents', #Unfortunately adding this function to the M code will create a dynamic error :( + 'Web.Contents', # Passing Web.Contents itself into Expression.Evaluate's environment triggers + # the "can't use a Dynamic Data Source" Formula.Firewall error. A wrapper function + # (see #"Web.Contents wrapper" below) is mapped in via manual_mappings instead - + # see https://community.powerbi.com/t5/Power-Query/Dynamic-data-sources-aren-t-refreshed-in-the-Power-BI-service/td-p/1563630 'api.openai', 'api.worldbank', 'bibb.pro', @@ -60,6 +63,13 @@ 'Number.Abs' ] +# Identifiers that can't just be mapped as `Name = Name` in the Expression.Evaluate +# environment record and need a custom expression instead (see #"Web.Contents wrapper" +# in the M_Code template below). +manual_mappings = { + 'Web.Contents': '#"Web.Contents wrapper"', +} + M_Code = """ let @@ -70,10 +80,17 @@ PAT = "", // Personal Access Token (PAT) for GitHub API https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens QueryHeaders = if PAT <> "" then - [Authorization = "Bearer " & PAT] + [Authorization = "Bearer " & PAT] else [], + // Wrapping Web.Contents in a same-signature function lets it be passed into + // Expression.Evaluate's environment without tripping the "can't use a Dynamic + // Data Source" Formula.Firewall error that occurs when Web.Contents itself is + // passed around as a first-class value. + #"Web.Contents wrapper" = (url as text, optional options as nullable record) as any => + Web.Contents(url, options), + #"Get Trees all trees" = Json.Document( Web.Contents( BaseURL,[ @@ -132,9 +149,8 @@ Text.FromBinary( Binary.FromText(#"Get functions fx") ), - [ + [ #TextToReplace - //,Web.Contents = Web.Contents //Unfortunately adding this function to the M code will create a dynamic error :( ] ) in @@ -213,13 +229,10 @@ def process_directory(directory): unique_functions.sort() print("Extracted Power Query functions (excluding numbers and specified strings):") -result = "" -for i, func in enumerate(unique_functions): - if i == len(unique_functions) - 1: - result += f" {func} = {func}" - else: - result += f" {func} = {func},\n" +entries = [f"{func} = {func}" for func in unique_functions] +entries += [f"{name} = {expression}" for name, expression in manual_mappings.items()] +result = ",\n".join(f" {entry}" for entry in entries) M_Code = M_Code.replace("#TextToReplace", result)