Forum Discussion
how to create a query that paginates?
Hi there
Sorry for the delay in getting back to you. I did get it to work, but it is slow. I'm unsure where the bottleneck is (my code, or the service I'm hitting). Either way, here is the code I pulled together:
let
/*
Purpose: Request all timesheet records from Dovico
*/
/*
Generate a list of XML objects based on the Dovico's pagination of its return set
The initial URL is: https://api.dovico.com/TimeEntries/?version=5
Pagination is controlled by return randomly-generated values in URI attributes PrevPageURI and NextPageURI
The limit in each direction is denoted by the value "N/A"
Date filtering can be introduced by using: https://api.dovico.com/TimeEntries/?daterange=2016-04-01%202016-04-15&version=5
See the Dovico API for more information: http://apideveloper.dovico.com/Time+Entries
*/
DataList = List.Generate(
()=> [SourceURI="https://api.dovico.com/TimeEntries/?version=5",ImportedXML=""],
each Text.PositionOf([SourceURI],"N/A") = -1,
each [
Source = Web.Contents([SourceURI],[Headers=[#"Authorization"="WRAP access_token=""client=CLIENT_TOKEN&user_token=USER_TOKEN"""]]),
ImportedXML = Xml.Tables(Source,null,1252),
ChangeType = Table.TransformColumnTypes(ImportedXML,{{"PrevPageURI", type text}, {"NextPageURI", type text}}),
SourceURI = Record.Field(Table.First(ChangeType),"NextPageURI"),
TimeEntries = ChangeType{0}[TimeEntries],
TimeEntry = TimeEntries{0}[TimeEntry]
],
each [[SourceURI],[ImportedXML]]
),
/*
Now expand and shape the list of XML objects into a single table of data
*/
ConvertToTable = Table.FromList(DataList, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Expanded Column1" = Table.ExpandRecordColumn(ConvertToTable, "Column1", {"ImportedXML"}, {"ImportedXML"}),
#"Filtered Rows" = Table.SelectRows(#"Expanded Column1", each ([ImportedXML] <> "")),
#"Expanded ImportedXML" = Table.ExpandTableColumn(#"Filtered Rows", "ImportedXML", {"TimeEntries"}, {"TimeEntries"}),
#"Expanded TimeEntries" = Table.ExpandTableColumn(#"Expanded ImportedXML", "TimeEntries", {"TimeEntry"}, {"TimeEntry"}),
#"Expanded TimeEntry" = Table.ExpandTableColumn(#"Expanded TimeEntries", "TimeEntry", {"Sheet", "Client", "Project", "Task", "Employee", "Date", "TotalHours", "Description"}, {"Sheet", "Client", "Project", "Task", "Employee", "Date", "TotalHours", "Description"}),
#"Changed Type1" = Table.TransformColumnTypes(#"Expanded TimeEntry",{{"Date", type date}, {"TotalHours", type number}, {"Description", type text}}),
#"Expanded Project" = Table.ExpandTableColumn(#"Changed Type1", "Project", {"Name"}, {"Project.Name"}),
#"Expanded Task" = Table.ExpandTableColumn(#"Expanded Project", "Task", {"Name"}, {"Task.Name"}),
#"Expanded Employee" = Table.ExpandTableColumn(#"Expanded Task", "Employee", {"Name"}, {"Employee.Name"}),
#"Expanded Client" = Table.ExpandTableColumn(#"Expanded Employee", "Client", {"Name"}, {"Client.Name"}),
#"Expanded Sheet" = Table.ExpandTableColumn(#"Expanded Client", "Sheet", {"Status"}, {"Sheet.Status"}),
#"Renamed Columns" = Table.RenameColumns(#"Expanded Sheet",{{"Client.Name", "Customer"}, {"Sheet.Status", "Approval status"}, {"Project.Name", "Project"}, {"Task.Name", "Task"}, {"Employee.Name", "Raw Name"}, {"TotalHours", "Effort (hrs)"}}),
#"Added Week Ending" = Table.AddColumn(#"Renamed Columns", "Week ending", each Date.EndOfWeek([Date],Day.Saturday), type date),
#"Added Name" = Table.AddColumn(#"Added Week Ending", "Name", each Text.Combine({List.Last(Text.Split([Raw Name],",")), List.First(Text.Split([Raw Name],","))}, " "), type text),
#"Removed Columns" = Table.RemoveColumns(#"Added Name",{"Raw Name"}),
#"Trimmed Text" = Table.TransformColumns(#"Removed Columns",{{"Name", Text.Trim}}),
#"Reordered Columns" = Table.ReorderColumns(#"Trimmed Text",{"Name", "Date", "Week ending", "Customer", "Project", "Task", "Effort (hrs)", "Description", "Approval status"}),
#"Removed Columns1" = Table.RemoveColumns(#"Reordered Columns",{"Description", "Approval status"}),
#"Sorted Rows" = Table.Sort(#"Removed Columns1",{{"Date", Order.Descending}}),
#"Changed Type" = Table.TransformColumnTypes(#"Sorted Rows",{{"Customer", type text}, {"Project", type text}, {"Task", type text}})
in
#"Changed Type"Hi tempranello,
thanks for getting back and providing such a nice code.
Nothing really obvious in the List.Generate part, although: What about "TimeEntries" and "TimeEntry" - cannot see where you use them there and you don't return them in the output. So could you omit them? (Although I wouldn't believe that this is a performance killer).
The next step to localize the performance-problem would be to skip all steps after #"Expanded ImportedXML" This seems to be the step where the first "real" value from the procedure is shown and before further transformations take place.
You can try to put a buffer here:
ConvertToTable = Table.FromList(List.Buffer(DataList), Splitter.SplitByNothing(), null, null, ExtraValues.Error),
making sure to avoid multiple web-calls.
Or you use Fiddler to check how long the web-calls actually take.
- ImkeF9 years ago
Community Champion
Delete the {0}, so just:
let Source = {1..200}, #"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), #"Added Custom" = Table.AddColumn(#"Converted to Table", "Custom", each Json.Document(Web.Contents("https://api.mywebsite.com?myapikey=1234567&page="&Text.From([Column1])&""))[items]) in #"Added Custom"Then check what format is returned in column [Custom] before deciding on how to expand that.
- ImkeF9 years ago
Community Champion
That article describes the procedure for a cursor-based pagination, which means that the URL for the next step will be returned from its previous step.
In the screenshots you've provided I cannot spot such a field. Could it be that your source paginates differently, for example just by counting pages?
Then you would need to use a different method.
For cursor-based-pagination you need to explore the first record (or table) returned from the first step and try to find the field that contains the field with the key for the next iteration.
Thats what goes into the step "next".
- ImkeF9 years ago
Community Champion
No need for recursion here.
This is an easy example:
let Source = {1..11}, #"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), #"Added Custom" = Table.AddColumn(#"Converted to Table", "Custom", each Web.Page(Web.Contents("http://www.boerse-online.de/index/liste/S&P_500?p="&Text.From([Column1])&"")){0}[Data]) in #"Added Custom"You need to modify it like this:
1) Step Source: replace 11 by your page_count (YourRecord[page_count])
2) Step Added Custom: Replace by your url and replace the page number by: "&Text.From([Column1])&"
This should return the correct record per page which you can then further expand.
It creates a list of your pages, turns it into a table and then adds a column where each page is called by its individual URL.
- Anonymous9 years agoNot applicable
Hi sterling
I'm not an expert at this and I had a lot of help writing the query (thanks again ImkeF!), but could the API you're using limit you to a certain amount of results? Is there a "HAS MORE" (pages) with a "TRUE" or "FALSE" option for example? I don't see a page option in your URL, but if that can be included, below is the solution that ImkeF sent me, and it resolved my issue. Notice: p="&Text.From([Column1])&"let
Source = {1..11},
#"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Added Custom" = Table.AddColumn(#"Converted to Table", "Custom", each Web.Page(Web.Contents("http://www.boerse-online.de/index/liste/S&P_500?p="&Text.From([Column1])&"")){0}[Data])
in
#"Added Custom"I wish I had more specific technical advise here, but hopefully that helps.
- ImkeF9 years ago
Community Champion
I modified it a bit and included your further transformation steps in there as well - might actually be the best idea because it will prevent multiple API-calls (hopefully...). Just make sure that in the last "each-step", you reference the last step of your transformations (where I've now replaced "Result" with "Value":
let
Pagination = List.Skip(List.Generate( () => [Last_Key = "20170319015902380428278", Counter=0], // Start Value
each [Last_Key] <> null and [Last_Key] <> "", // Condition under which the next execution will happen
each [ WebCall = "https://url.com/?fromday=20170301&today=20171231&offset="&[Last_Key]&"%4041627&authKey=123", // retrieve results per call
Last_Key = if [Counter]<=1 then "20170319015902380428278" else WebCall[lastKey] ],// determine the LastKey for the next execution
Counter = [Counter]+1,// internal counter
#"Converted to Table" = Record.ToTable(WebCall), // steps of your further query
Value = #"Converted to Table"{1}[Value], // last step of your further queries
each [Value]),1) // Select just the Record of the last step from your query
in
Pagination - ImkeF9 years ago
Community Champion
Yes, I can understand you assesment, but this shouldn't be the cause. Pls check the following query that paginates through 3 webpages using this method successfully:
let Pagination = List.Skip(List.Generate( () => [Result = Web.Page(Web.Contents("http://www.finanzen.net/aktien/US-Aktien-Realtimekurse@intpagenr_"&Text.From(Counter)))[Data]{0}, Counter = 0], // Start Value each [Counter] <=3, // Condition under which the next execution will happen each [Result = Web.Page(Web.Contents("http://www.finanzen.net/aktien/US-Aktien-Realtimekurse@intpagenr_"&Text.From(Counter)))[Data]{0}, // retrieve results per call Counter = [Counter]+1 ], // determine the LastKey for the next execution each [Result] ),1), Combine = Table.Combine(Pagination) in CombineIf you find a website where we could harvest the "next page" in the results returned, pls forward and we can test your scenario there.
- Anonymous9 years agoNot applicable
Hi ImkeF,
I have a similar issue to others in the thread. I just cant seem to get my query to paginate. I've done some initial exploring/research which will hopefully limit the amount of effort needed to solve this. I'm very new to power BI and have little coding experience so any help would be appriciated. Below is the code I've been playing around with (I've removed my token):
let iterations = 20, url = "https://az1.qualtrics.com/API/v3/mailinglists/ML_cwQxQJJ5adc1YyN/contacts", FnGetOnePage = let Source = Json.Document(Web.Contents("https://az1.qualtrics.com/API/v3/mailinglists/ML_cwQxQJJ5adc1YyN/contacts", [Headers=[#"X-API-TOKEN"="my token here"]])), data = try Source[result][elements] otherwise null, next = try Source[result][nextPage] otherwise null, res = [Data=data, Next=next] in res, GeneratedList = List.Generate( ()=>[i=0, res = FnGetOnePage(url)], each [i]<iterations and [res][Data]<>null, each [i=[i]+1, res = FnGetOnePage([res][Next])], each [res][Data]) in GeneratedListAs you can see, I'm using an API with headers which may be the cause of some of the issues I'm having. The url for the next page of data is in a field called "nextPage". The recors are in a field called "elemts". There are only 100 records per page and I have a few thousand records that I'd like to automatically bring in.
Using the code above, I get the FnGetOnePage to run fine and it's producing the correct data in the "Data" and "Next" fields. However, the List.Genreate function is where I'm getting an error. Below is the error I'm getting:
Expression.Error: We cannot convert a value of type Record to type Function.
Details:
Value=Record
Type=TypeYou seem to know what you're doing when it comes to this topic, I would love to hear your feedback or any suggestions you might have!
Thanks :)
- ImkeF9 years ago
Community Champion
Yes, you're code is looking very good - especially for a "beginner" - kudos!
I haven't changed much, pls see if the following code works for you:
let iterations = 20, url = "https://az1.qualtrics.com/API/v3/mailinglists/ML_cwQxQJJ5adc1YyN/contacts", // Turn your query into a function where the url is fed in as a parameter FnGetOnePage = (url) => let // Replace the hardcoded url to a reference to the parameter that's going to be fed in Source = Json.Document(Web.Contents(url, [Headers=[#"X-API-TOKEN"="my token here"]])), data = try Source[result][elements] otherwise null, next = try Source[result][nextPage] otherwise null, res = [Data=data, Next=next] in res, GeneratedList = List.Generate( ()=>[i=0, res = FnGetOnePage(url)], each [i]<iterations and [res][Data]<>null, each [i=[i]+1, res = FnGetOnePage([res][Next])], each [res][Data]) in GeneratedList - ImkeF9 years ago
Community Champion
Ooops, I'm really sorry: The closing square bracket was at the wrong place. I've moved the steps around and it shifted to the wrong place:
//Previous code with access credentials let Pagination = List.Skip(List.Generate( () => [Last_Key = "20170404130408053410572", Counter=0], // Start Value each [Last_Key] <> null and [Last_Key] <> "", // Condition under which the next execution will happen each [ WebCall = Json.Document(Web.Contents("https://apiv2.clickmeter.com/datapoints/8697350/hits?timeframe=last30&limit=100&offset="&[Last_Key]&"%408693934&authKey=fde74f69-ea93-411f-96b2-5eb9cb4c0993")), // retrieve results per call Last_Key = if [Counter]<=1 then "20170404130408053410572" else WebCall[lastKey] ,// determine the LastKey for the next execution Counter = [Counter]+1,// internal counter #"Converted to Table" = Record.ToTable(WebCall), // steps of your further query Value = #"Converted to Table"{1}[Value] // last step of your further queries
], each [Value]),1) // Select just the Record of the last step from your query in PaginationWorks for me now, just expand the record (& ignore the error-message for a start): Transfer the list to a table & then you can expand the records you need.
Not sure about your other questions/aspects from your post: Is there anything that is still to be done now?
- ImkeF9 years ago
Community Champion
Pls check this code:
let Pagination = List.Skip(List.Generate( () => [Last_Key = "20170404130408053410572", Counter=0], // Start Value each [Last_Key] <> null and [Last_Key] <> "", // Condition under which the next execution will happen each [ Last_Key = try if [Counter]<=1 then "20170404130408053410572" else [WebCall][lastKey] otherwise null,// determine the LastKey for the next execution WebCall = Json.Document(Web.Contents("https://apiv2.clickmeter.com/datapoints/8697350/hits?timeframe=last30&limit=10&offset="&Last_Key&"%408693934&authKey=fde74f69-ea93-411f-96b2-5eb9cb4c0993")), // retrieve results per call Counter = [Counter]+1// internal counter ], each [WebCall] ),1), #"Converted to Table" = Table.FromList(Pagination, Splitter.SplitByNothing(), null, null, ExtraValues.Error), #"Expanded Column3" = Table.ExpandRecordColumn(#"Converted to Table", "Column1", {"hits"}, {"hits"}), #"Expanded hits2" = Table.ExpandListColumn(#"Expanded Column3", "hits"), #"Expanded hits3" = Table.ExpandRecordColumn(#"Expanded hits2", "hits", {"id", "accessTime", "entity", "browser", "os", "location", "conversions", "type", "ip", "isSpider", "isUnique", "trackedParameters"}, {"id", "accessTime", "entity", "browser", "os", "location", "conversions", "type", "ip", "isSpider", "isUnique", "trackedParameters"}) in #"Expanded hits3" - ImkeF8 years ago
Community Champion
Have you refreshed the query? I've used a filter which I might have forgotten to take out.
For learning purposes I'd suggest that you step through the queries: Pretty sure you find the filters (just delete them) and the answer to your URL question in there ;)
- Anonymous7 years agoNot applicable
in your web call - in second part(else part) you are giving a variable inside web.contents - which is not supported in power bi service for scheduled refresh.
Thanks to excellent blog by Chris Webb - this can be solved - or lets say there is a way to overcome this issue.
In your case, you would need to prvide a fixed value here instead of "&Last_Key&"
"https://api.airtable.com/v0/ID/Audit?api_key=KEY&offset="&Last_Key&""))
with query paramters as folow
Web.Contents("https://api.airtable.com/v0/ID/Audit?api_key=KEY&offset=someFixedValue",Query=[offset=Last_Key])This someFixedValue has to be some valid value which works fine - say your first value of key - which will be used as a dummy value only to 'trick' the PBI service.
BR
emudria.
- Anonymous9 years agoNot applicable
Hi,
I'm trying to understand this code and manipulate it for my own query, but I'm having difficulty. Do you have any thoughts on how I would edit this in order to display all pages on this query?
I've attached a screen shot of what I see when I first bring the query in. There are 1132 pages and 56580 records, but when I expand it out, I only get 50 records.
Screen shot above and here is the final code below (the URL is bogus for security purposes):
let
Source = Json.Document(Web.Contents("https://api.thewebsite.com/stats/leads?&start=2016-01-01&end=2016-11-25api_key=myapikey")),
#"Converted to Table" = Record.ToTable(Source),
Value = #"Converted to Table"{2}[Value],
#"Converted to Table1" = Table.FromList(Value, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Expanded Column1" = Table.ExpandRecordColumn(#"Converted to Table1", "Column1", {"bad", "campaign_id", "campaign_name", "converted", "created_at", "delivered", "delivery_summary_id", "error_delivery_summary_id", "good", "id", "invalid", "margin", "marked_for_wipe_at", "pickled_at", "profile_id", "profit", "purchase_price", "recipient_id", "rejected", "returned", "sale_price", "source_id", "source_lead_id", "values", "wiped_at"}, {"bad", "campaign_id", "campaign_name", "converted", "created_at", "delivered", "delivery_summary_id", "error_delivery_summary_id", "good", "id", "invalid", "margin", "marked_for_wipe_at", "pickled_at", "profile_id", "profit", "purchase_price", "recipient_id", "rejected", "returned", "sale_price", "source_id", "source_lead_id", "values", "wiped_at"})
in
#"Expanded Column1"Thanks for any advise!!
- Anonymous9 years agoNot applicable
FYI: I've tried this below, and I'm still only getting 50 records as opposed to 96209... Any thoughts?
let
iterations = 10,
url = "https://mywebiste.com/leads?api_key=myapikey",FnGetOnePage =
(url) as record =>
let
Source = Json.Document(Web.Contents(url)),
data = try Source[items] otherwise null,
next = try Source[page_count][has_more] otherwise null,
res = [Data=data, Next=next]
in
res,
GeneratedList =
List.Generate(
()=>[i=0, res = FnGetOnePage(url)],
each [i]<iterations and [res][Data]<>null,
each [i=[i]+1, res = FnGetOnePage([res][Next])],
each [res][Data]),
#"Converted to Table" = Table.FromList(GeneratedList, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Expanded Column1" = Table.ExpandListColumn(#"Converted to Table", "Column1")
in
#"Expanded Column1" - ImkeF9 years ago
Community Champion
It looks as if you're missing the {0} when trying to select the values.
Try debugging your function by using placeholders to see what kind of values each of your step actually returns (a bit like here: http://www.thebiccountant.com/2016/05/30/analyze-m-functions-step-step/ )
- Anonymous9 years agoNot applicable
Thanks ImkeF,
I've tried copying/pasting around the function, with no success.
I did notice that when I copy/paste the URL after the "FnGetOnePage", I get a lot of lists and tons of columns, but they're all blanks...
I have to admit, I'm in a little over my head on this one, but I know I'm close.
Because I copied this code from a how-to for getting data from Facebook, I think it might have to do with what's different on this website/API. I've tried to edit the code to fit this situation as much as possible, but maybe it's off somehow.
Here are the results I get from the website I'm getting data from, before breaking anything out into a table:
Here is my code to try and work with all of the 96209 records. Again, I only get 1List and 50 records when I expand out the list:
let
iterations = 200,
url = "https://api.thewebsite.com?api_key=myapikey",FnGetOnePage =
(url) as record =>let
Source = Json.Document(Web.Contents(url)),
data = try Source[items] otherwise null,
next = try Source[has_more][next] otherwise null,
res = [Data=data, Next=next]
in
res,
GeneratedList =
List.Generate(
()=>[i=0, res = FnGetOnePage(url)],
each [i]<iterations and [res][Data]<>null,
each [i=[i]+1, res = FnGetOnePage([res][Next])],
each [res][Data]),
#"Converted to Table" = Table.FromList(GeneratedList, Splitter.SplitByNothing(), null, null, ExtraValues.Error)
in
#"Converted to Table"Any thoughts?
- ImkeF9 years ago
Community Champion
Could You please send the link where you've copied the code from? - Anonymous9 years agoNot applicable
Sure. Here's the blog:
http://datachant.com/2016/06/27/cursor-based-pagination-power-query/
FYI: What I'm actually connecting to through the Power BI "web" source is an API. I'm not sure if that changes anything.
- Anonymous9 years agoNot applicable
Thank you ImkeF,
I really appreciate you taking the time to help me with this.
That must be it.
I also just realized that in the data I get back from this source, there is a "has_more" row that contains "TRUE" if there are more pages and "FALSE" if there are no more pages. It also shows the "page_count". I can even tell it what page I want in the URL by including the paramater "page=x".
If that's the case, I'm thinking the query/code would definitely need to be different, and possibly more straightforward?
Any advise on how that would look? (screen shot of the response back below)
- Anonymous9 years agoNot applicable
I'm thinking I might be able use something like:
if Source[has_more]="TRUE" then go to next page
I just don't know M language to figure this out on my own...
- Anonymous9 years agoNot applicable
We're so close I can taste it...
So, if I use just what you sent for my scenario and change it to my environment (URL, page, etc), the wheels just keep turning and nothing happens.
So I edited it a little bit and almost got something, but I'm getting an error: "Expression.Error: We cannot convert a value of type Record to type List.
Details:
Value=Record
Type=TypeHere's a screen shot of where it almost seems to work...
You'll have to excuse my lack of knowledge on this...
Here is the full code I used... (FYI: I'm contecting to an Json.Document not actually a Web.page)
let
Source = Json.Document(Web.Contents("api.mywebsite.com/leads?myapikey=1234567")),
Source1 = {1..11},
#"Converted to Table" = Table.FromList(Source1, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Added Custom" = Table.AddColumn(#"Converted to Table", "Custom", each Json.Document(Web.Contents("api.mywebsite.com/leads?myapikey=1234567&page="&Text.From([Column1])&"")){0}[Data]),
Custom = #"Added Custom"{0}[Custom]
in
CustomAny thoughts?
Thanks again for all your help.
- ImkeF9 years ago
Community Champion
Pls check this out:
let Source = Json.Document(Web.Contents("api.mywebsite.com/leads?myapikey=1234567")), Source1 = {1..Source[page_count]}, #"Converted to Table" = Table.FromList(Source1, Splitter.SplitByNothing(), null, null, ExtraValues.Error), #"Added Custom" = Table.AddColumn(#"Converted to Table", "Custom", each Json.Document(Web.Contents("api.mywebsite.com/leads?myapikey=1234567&page="&Text.From([Column1])&""))) in #"Added Custom" - Anonymous9 years agoNot applicable
FYI: The other finally went through and I'm getting the same ("We cannot convert a vlue of type Record to type List") error.
Here's the code:
let
Source = {1..200},
#"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Added Custom" = Table.AddColumn(#"Converted to Table", "Custom", each Json.Document(Web.Contents("https://api.mywebsite.com?myapikey=1234567&page="&Text.From([Column1])&"")){0}[items]),
Custom = #"Added Custom"{0}[Custom]
in
Custom - Anonymous9 years agoNot applicable
That worked!!
YOU ARE AWESOME!!
Thank you so much ImkeF!!
- remix9 years agoRegular Visitor
Hello Anonymous
I`m also working to get the results from Hubspot Deals. Do you mind sharing your query?