Invoke-WebRequest Canvas LMS API pagination - powershell

I cannot to seem to find any examples on this topic and I would like to know how to do it. Can anyone show me an example or point me to a link on how to do pagination in powershell with Invoke web-request? The challenge I am facing is that I am making API calls to a server that only returns 100 rows at a time. In order to get any more rows, I would have to make a second call to the server. I have no clue how to do it.
If it helps, here is the link provided by the Canvas LMS and my code that I have so far.
Pagination:
Pagination
Requests that return multiple items will be paginated to 10 items by
default. You can set a custom per-page amount with the ?per_page
parameter. There is an unspecified limit to how big you can set
per_page to, so be sure to always check for the Link header.
To retrieve additional pages, the returned Link headers should be
used. These links should be treated as opaque. They will be absolute
urls that include all parameters necessary to retrieve the desired
current, next, previous, first, or last page. The one exception is
that if an access_token parameter is sent for authentication, it will
not be included in the returned links, and must be re-appended.
Pagination information is provided in the Link header:
Link:
<https://<canvas>/api/v1/courses/:id/discussion_topics.json?opaqueA>; rel="current",
<https://<canvas>/api/v1/courses/:id/discussion_topics.json?opaqueB>;> rel="next",
<https://<canvas>/api/v1/courses/:id/discussion_topics.json?opaqueC>;> rel="first",
<https://<canvas>/api/v1/courses/:id/discussion_topics.json?opaqueD>;> rel="last"
The possible rel values are:
current - link to the current page of results. next - link to the next
page of results. prev - link to the previous page of results. first -
link to the first page of results. last - link to the last page of
results. These will only be included if they are relevant. For
example, the first page of results will not contain a rel="prev" link.
rel="last" may also be excluded if the total count is too expensive to
compute on each request.
The beginning product
$curlly=""
$url_main="https://[instance].instructure.com/api/v1/accounts/1/courses?per_page=1"
$security_token="imhungry"
$header = #{"Authorization"="Bearer "+ $security_token; "rel"="last"}
$curlly=Invoke-WebRequest -Headers $header -Method Get -Uri $url_main
$curlly = ConvertFrom-Json $curlly.Content
foreach($course in $curlly)
{
$course.name
}
$curlly.Count
The final product
##This is an example on how to use pagination in powershell
$url_main="https://[instance].instructure.com/api/v1/accounts/1/courses?per_page=100"
$security_token="boyimhungry"
$header = #{"Authorization"="Bearer "+ $security_token}
$purlly=Invoke-WebRequest -Headers $header -Method Get -Uri $url_main
$curlly = ConvertFrom-Json $purlly.Content
$url_main = $purlly.Headers.Link.Split(",")[1].Replace("<","").Replace(">","") ## you can get away with just doing one replace("<","") but it looks neater this way
while( !$url_main.Contains("prev"))
{
$purlly=Invoke-WebRequest -Headers $header -Method Get -Uri $url_main
$curlly += ConvertFrom-Json $purlly.Content
$url_main = $purlly.Headers.Link.Split(",")[1].Replace("<","").Replace(">","")
cls
$curlly.Count
$url_main
}
foreach($course in $curlly)
{
$course.name
}
$curlly.Count

I know you've accepted an answer, but I thought I'd give my code example just in case anybody needs one. This example is getting a list of all of our Canvas users. Not a terrible process -- most of the work is done with just a 4-line do..while loop.
$token = "YOUR_ACCESS_TOKEN"
$headers = #{"Authorization"="Bearer "+$token}
$allCanvasUsers = New-Object System.Collections.ArrayList #()
$pageNumber = 1
Function RequestPageOfUsers($page) {
$script:resultsPage = Invoke-WebRequest -Method GET -Headers $headers -Uri "https://$domain/api/v1/accounts/self/users?per_page=100&search_term=P00&page=$page"
$usersPage = ConvertFrom-Json $resultsPage.Content
foreach ($user in $usersPage) {
$script:allCanvasUsers.Add($user)
}
}
do {
RequestPageOfUsers $pageNumber
$pageNumber++
} while ($resultsPage.Headers.Link.Contains('rel="next"'))

David Baker's answer is fantastic, and nearly worked for me, but was only returning a single page. Not sure why, but the CONTAINS('...') test was always returning FALSE.
In order to get this to work, I had to make a minor change to the WHILE condition. For me, the ResultsPage object was returned with a property called RelationLink, which is a dictionary containing key-value pairs. The last page in a paginated set of results has no 'Next' key. So, my DO-WHILE loop runs until the 'Next' value is found to be null.
$Token = '<YOUR-TOKEN>'
$headers = #{"Authorization"="Bearer "+$token}
$allCanvasUsers = New-Object System.Collections.ArrayList #()
$pageNumber = 1
Function RequestPageOfUsers($page) {
$script:resultsPage = Invoke-WebRequest `
-Method GET `
-Headers $headers `
-Uri "https://<DOMAIN>:443/api/v1/accounts/self/users?per_page=100&page=$page"
$usersPage = ConvertFrom-Json $resultsPage.Content
foreach ($user in $usersPage) {
$script:allCanvasUsers.Add($user)
}
}
do {
RequestPageOfUsers $pageNumber
$pageNumber++
} while ($resultsPage.RelationLink.Next -NE $NULL)

This looks like a real pain.
On each paginated request, you're going to get back a Link header that contains one or more of the links they describe.
For your purposes (a sequential read through of every result), you only really need to be concerned with the link rel=next link. You would keep calling that one until there is no rel=next anymore, which is how you'll know that you're on the last page.
You don't provide the rel part; it's not a header. It's something that lets you identify which link to use, and then use that link as is.
So the basic overview of what you need to do:
Make the first request.
Read through the returned Link header, and find the one that corresponds to rel=next (if it's not there, you're done).
Make the next request directly to the link you found.
Parse the Link header again, repeat.

Related

How to define a global variable in Powershell

I am trying to define a variable globally in powershell and get the value in one function and pass that value in different function but I am unable to do so. I googled and tried $global:myvariable but its not working. what wrong am i doing?
here is my code:
$global:namerel = $null
Function GET-value{
$uriAccount = $orz + "_apis/release/releases/1914?api-version=6.0"
$responseRels = Invoke-RestMethod -Uri $uriAccount -Method get -Headers $AzureDevOpsAuthenicationHeader
$namerel = $responseRels.Name
write-host $namerel # it prints the required value
}
Function GET-rel{
$test = GET-value
write-host $namerel # nothing gets printed. its blank
}
To use a variable scoped global, you need to use syntax $global:namerel inside the functions. Without that, variable $namerel will be just a new variable, local to that function.
Also, you may not need to use global. Scope script usually is broad enough. See About_scopes

How to bind an array to an HTML tag in powershell?

Below is a snip of my powershell code where my response or my variable($witrevisions) is of type array. I am looking to bind this in a html tag which i have defined in the power shell. As I am very new to coding stuff , I am looking the ways how can I bind array to html tag in best possible way
...continuing my line of code
$response4s = (Invoke-RestMethod -Uri $uriAccount -Method get -Headers $AzureDevOpsAuthenicationHeader).values
$wits = $response4s | where({$_.fields.'System.WorkItemType' -eq 'Task'}) # Only retrieve Tasks
$witrevisions = #()
foreach($wit in $response4s){
$customObject = new-object PSObject -property #{
"Title" = $wit.fields.'System.Title'
"AssignedTo" = $wit.fields.'System.AssignedTo'
}
$witrevisions += $customObject
}
$witrevisions | Select-Object `
Title,
AssignedTo
}
and this the sample response i am getting in $witrevisions which i have exported in text file. its a table with two column one having emails and other having a title name.i have tried to show by giving it a table view for better understanding
Assigned To Title
xyz#outlook.com testingpro
drdr#outlook.com resttesting
and here is the html tag where I trying to bind the $witrevisions.
$DOWNLOAD_PAGE_BODY_CONTENT = "<!DOCTYPE html>
`n<html>
`n<head>
`n <title>Validation</title>
`n</head>
`n<body>
`n
`n<p>Please click the link to download the release.</p>
`n<p></p>
`n<p></p>
`n<p>$witrevisions</p>
`n</body>
`n</html>
`n"
Can someone please tell me how should I do this??
Here is an example of some code that would take your array and emit a table, with an explanation to help you tweak to your specific needs:
"<table><body>$($witrevisions|% {"<tr><td>$($_.Title)</td><td>$($_.AssignedTo)</td></tr>"} )</body></table>"
The double quotes are important because they allow string interpolation (it will replace variables with this value, versus being read a plain text. E.g. '[' + $test + ']' => "[$test]"
If you need to do more complex logic in string interpolation, you can use $(...), the ellipses being regular code.
You can iterate through an array by piping to the ForEach-Object, or it's alias %. All the code in the braces will be executed for each item in the array. The current items is represented by $_.
We're then back to string interpolation and using $(...), which is needed to access the members of the current item.
Note: There are several other ways to accomplish (functionally) the same thing. E.g. foreach(...){} vs |%{...}, so feel free to use a different technique if you are more comfortable with doing something a different way.

Powershell variable not displaying nested object

I am sending a GET request that returns an object. Using postman, I get:
{
"changedInstance": {
"change": "Created",
"instanceAfterChange": {
"instanceId": "123",
"schemaName": "RBAC",
"className": "PermissionCategory",
"properties": {
"Name": "IModel Schema Management Service",
"TypeId": 2,
"TypeName": "Service Access and Permissions"
}
}
}}
But when I send a request through Powershell, I get the response:
#{change=Created; instanceAfterChange=}
The request I have made:
$Response = Invoke-RestMethod -Uri $base_url/v2.4/Repositories/BentleyCONNECT--Main/RBAC/PermissionCategory -Method Post -Headers #{'Authorization' = 'Bearer ' + $global:token} -Body $postParams -ContentType "application/json"
I have checked all the header values and the body, no values are missing.
The property 'instanceAfterChange' is empty, however when I monitor this same request in fiddler, it shows the correct response. My question is, why is Powershell displaying it as empty even though when it is not.
ConvertFrom-Json constructs a [pscustomobject] instance from your JSON input, and what you're seeing (#{change=Created; instanceAfterChange=}) is the default display formatting for the instance's .changedInstance property value.
Due to a bug as of PowerShell 7.0, the default display formatting makes it appear as if property instanceAfterChange is empty, but it actually isn't.
Specifically, the bug is that calling .ToString() on [pscustomobject] instances unexpectedly yields the empty string - see this GitHub issue.
The hashtable-like display representation of [pscustomobject]s as property values - as shown in your question and explained in this answer - involves .ToString() calls on nested [pscustomobject] instances, which is why the bug surfaces.
Therefore, you shouldn't have a problem accessing the nested [pscustomobject]s' properties, e.g., $convertedFromJson.changedInstance.instanceAfterChange.schemaName should yield RBAC.
An easy way to verify that the data is there is to convert back to JSON (ConvertTo-Json) or to visualize the structure via Format-Custom.
A simplified example:
$convertedFromJson = '{
"changedInstance": {
"instanceAfterChange": {
"instanceId": "123",
"schemaName": "RBAC"
}
}
}
' | ConvertFrom-Json
# Output as-is: the bug makes .changedInstance appear empty.
$convertedFromJson
'---'
# Access a nested property - this works.
$convertedFromJson.changedInstance.instanceAfterChange.schemaName
'---'
# Visualize the structure of the nested object.
$convertedFromJson | Format-Custom
The above yields:
changedInstance
---------------
#{instanceAfterChange=} # DISPLAY BUG: .instanceAfterChange appears empty.
---
RBAC # OK: accessing a property of .instanceAfterChange works.
---
# Visualization of the nested object's structure via Format-Custom
class PSCustomObject
{
changedInstance =
class PSCustomObject
{
instanceAfterChange =
class PSCustomObject
{
instanceId = 123
schemaName = RBAC
}
}
}

Variable expansion in JSON object for use with Invoke-RestMethod

Over the past few days I've been looking for documentation on variable expansion in JSON objects. I've tried a lot of recommendations and fixes, but none of them seem to work quite as expected. I'm left wondering if there's a better way than I have implemented. Anyway, on to my use case.
Use Case
I have an array of integers, which represent objects in a web application. These objects are able to have a relationship established between them by making a POST request to an API endpoint.
This array of integers is referred to as $combined.
PS C:\> $combined
i_id m_id
------------ ----------
75419 6403
75420 6403
75421 6403
75410 6403
To set up the parameters used for my POST request, I've created another array called $associationParams.
$associationParams = #{
ContentType = "application/json"
Body = '{"id": '+$($thing).m_id+'}'
Method = "POST"
URI = "https://application/api/in/$($thing.i_id)/endpoint"
Headers = $headers
}
The $headers variable above is the authorization token.
So, my initial inclination is to loop through the array of integers ($combined) and call Invoke-RestMethod as so:
Foreach ($thing in $combined) {
Invoke-RestMethod #iocAssociationParams
}
Problem
So, I don't really have a problem per se... There seems to be some inconsistency in my testing where the final ForEach doesn't seem to loop through all the elements in the $combined array... But I am wondering if there's a better way. I've tried string formatting the JSON object in the Body parameter in $associationParams, but it definitely didn't work. I've also tried various forms of quoting, but they didn't work either. Is trying to splat the parameters for Invoke-RestMethod the best way? Should I just list out all the parameters for Invoke-RestMethod in the ForEach loop?
Use the -f format operator to insert $thing.m_id inside the foreach into $AssociationParams.
You have to escape the literal curly braces by doubling them.
Foreach ($thing in $combined) {
$AssociationParams = #{
ContentType = "application/json"
Body = '{{"id": {0}}}' -f $thing.m_id
Method = "POST"
URI = "https://application/api/in/$($thing.i_id)/endpoint"
Headers = $headers
}
Invoke-RestMethod #AssociationParams
}
Here's my solution as implemented.
Foreach ($thing in $combined) {
# Now we create the body of the request as a Powershell object with the intent to convert it to JSON later
$AssociationBodyObj = [PSCustomObject]#{
id = $($thing.id)
}
# ...and then we convert it to JSON
$AssociationBodyObj = $AssociationBodyObj | ConvertTo-Json
$iocAssociationParams = #{
ContentType = "application/json"
Body = $AssociationBodyObj
Method = "POST"
URI = "https://application/api/in/$($thing.i_id)/endpoint"
Headers = $headers
}
Invoke-RestMethod #AssociationParams
}

Use GetElementsByClassName in a script

I'm trying to write a PowerShell script to get the text within all the classes named "newstitle" from a website.
This is what I have:
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news/
$news=$geturl.parsedhtml.body.GetElementsByClassName("newstitle")[0]
Write-Host "$news"
}
check-krpano
It obviously needs much more tweaking, but so far, it doesn't work.
I managed to write an script using GetElementById, but I don't know the syntax for GetElementsByClassName, and to be honest, I haven't been able to find much information about it.
NOTE:
I've ticked the right answer to my question, but that's not the solution that I had chose to use in my script.
Although I was able to find the content within a tag containing a certain class, using 2 methods, they were much slower that searching for links.
Here is the output using Measure-Command:
Search for divs containing class 'newstitle' using parsedhtml.body -> 29.6 seconds
Search for devs containing class 'newstitle' using Allelements -> 10.4 seconds
Search for links which its element 'href' contains #news -> 2.4 seconds
So I have marked as useful the Links method answer.
This is my final script:
function check-krpano {
Clear-Host
$geturl=Invoke-WebRequest http://krpano.com/news
$news = ($geturl.Links |Where href -match '\#news\d+' | where class -NotMatch 'moreinfo+' )
$news.outertext | Select-Object -First 5
}
check-krpano
If you figure out how to get GetElementsByClassName to work, I'd like to know. I just ran into this yesterday and ran out of time so I came up with a workaround:
$geturl.ParsedHtml.body.getElementsByTagName('div') |
Where {$_.getAttributeNode('class').Value -eq 'newstitle'}
getElementsByClassName does not return an array directly but instead a proxy to the results via COM. As you have discovered, conversion to an array is not automatic with the [] operator. You can use the list evaluation syntax, #(), to force it to an array first so that you can access individual elements:
#($body.getElementsByClassName("foo"))[0].innerText
As an aside, conversion is performed automatically if you use the object pipeline, e.g.:
$body.getElementsByClassName("foo") | Select-Object -First 1
It is also performed automatically with the foreach construct:
foreach ($element in $body.getElementsByClassName("foo"))
{
$element.innerText
}
Cannot, for the life of me, get that method to work either!
Depending upon what you need back in the result though, this might help;
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news
$news=($geturl.Links|where href -match '\#news\d+')[0]
$news
}
check-krpano
Gives me back:
innerHTML : krpano 1.16.5 released
innerText : krpano 1.16.5 released
outerHTML : krpano 1.16.5 released
outerText : krpano 1.16.5 released
tagName : A
href : #news1165
You can use those properties directly of course, so if you only wanted to know the most recently released version of krpano, this would do it:
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news
$news=($geturl.Links|where href -match '\#news\d+')[0]
$krpano_version = $news.outerText.Split(" ")[1]
Write-Host $krpano_version
}
check-krpano
would return 1.16.5 at time of writing.
Hope that achieves what you wanted, albeit in a different manner.
EDIT:
This is a possibly a little faster than piping through select-object:
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news
($geturl.Links|where href -match '\#news\d+'|where class -notmatch 'moreinfo+')[0..4].outerText
}
I realize this is an old question, but I wanted to add an answer for anyone else who might be trying to achieve the same thing by controlling Internet Explorer using the COM object like such:
$ie = New-Object -com internetexplorer.application
$ie.navigate($url)
while ($ie.Busy -eq $true) { Start-Sleep -Milliseconds 100; }
I normally prefer to use Invoke-WebRequest as the original poster did, but I've found cases where it seemed like I needed a full-fledged IE instance in order to see all of the JavaScript-generated DOM elements even though I would expect parsedhtml.body to include them.
I found that I could do something like this to get a collection of elements by a class name:
$titles = $ie.Document.body.getElementsByClassName('newstitle')
foreach ($storyTitle in $titles) {
Write-Output $storyTitle.innerText
}
I observed the same really slow performance the original poster noted when using PowerShell to search the DOM, but using PowerShell 3.0 and IE11, Measure-Command shows that my collection of classes is found in a 125 KB HTML document in 280 ms.
It seems to work with PowerShell 5.1:
function check-krpano {
$geturl = Invoke-WebRequest -Uri "http://krpano.com/news/"
$news = $geturl.ParsedHtml.body.getElementsByClassName("newstitle")
Write-Host "$($news[0].innerHTML)"
}
check-krpano
Output:
krpano 1.20.6<SPAN class=smallcomment style="FLOAT: right"><A href="https://krpano.co
m/forum/wbb/index.php?page=Thread&postID=81651#post81651"><IMG class=icon16m src="../design/ico-forumlink
.png"> krpano Forum Link</A></SPAN>