Licensed Users
Identifying licensed users
This recipe contains Quarto documents in Python and R that identify the Posit Connect user accounts that count against the limit specified by the product license.
The document uses the GET /v1/users
endpoint to obtain information about user accounts.
---
title: "Licensed Users Report"
---
This document analyzes the recently active Posit Connect user accounts and
summarizes each account type and enumerates accounts.
```{python libraries}
#| echo: false
#| message: false
import os
import requests
import pandas as pd
from IPython.display import Markdown
from tabulate import tabulate
```
```{python configuration}
#| echo: false
# Confirm that environment variables are available.
connect_server = os.environ["CONNECT_SERVER"]
connect_api_key = os.environ["CONNECT_API_KEY"]
session = requests.Session()
session.headers.update({ "Authorization": f"Key {connect_api_key}" })
```
```{python fetch}
#| echo: false
#| warning: false
# Fetch all users from Posit Connect.
page = 0
users = None
while True:
page = page + 1
resp = session.get(
f"{connect_server}__api__/v1/users",
params = {
"page_number": page,
"page_size": 100,
"account_status": "licensed",
}
)
resp.raise_for_status()
payload = resp.json()
if len(payload["results"]) == 0:
break
results = pd.DataFrame.from_dict(payload["results"])
if users is None:
users = results
else:
users = pd.concat([users,results])
```
```{python tidying}
#| echo: false
def parse_iso8601(str):
return pd.to_datetime(str, format="%Y-%m-%dT%H:%M:%SZ", utc=True)
users["created_time"] = users["created_time"].transform(parse_iso8601)
users["updated_time"] = users["updated_time"].transform(parse_iso8601)
users["active_time"] = users["active_time"].transform(parse_iso8601)
```
## User role frequency
```{python frequency}
#| echo: false
# Summarize by user role.
# restriction.
roles = {
"administrator": "Administrator",
"publisher": "Publisher",
"viewer": "Viewer",
}
users["user_role"] = pd.Categorical(
users["user_role"],
["administrator", "publisher", "viewer"],
)
counts = users.groupby(
'user_role', observed = True,
)['user_role'].count().reset_index(name="N")
counts["Role"] = counts["user_role"].transform(
lambda t: roles.get(t,t),
)
counts = counts[["Role","N"]]
Markdown(tabulate(
counts,
headers=["Role", "N"],
showindex=False,
))
```
## Licensed users
```{python report}
#| echo: false
activity = users.copy()
activity = activity.sort_values(by = ["active_time"], ascending = False)
activity["email"] = activity["email"].transform(
lambda e: f"<{e}>",
)
activity["dashboard_url"] = activity["guid"].transform(
lambda guid: f"{connect_server}connect/#/people/users/{guid}",
)
activity["dashboard_url"] = activity["dashboard_url"].transform(
lambda u: f"[link](u){{target='_blank'}}",
)
def name(each):
first_name = each["first_name"]
last_name = each["last_name"]
if not first_name:
return(last_name)
elif not last_name:
return(first_name)
return f"{first_name} {last_name}"
activity["name"] = activity.apply(name, axis = 1)
activity["active"] = activity["active_time"].dt.strftime("%Y-%m-%d")
activity = activity[[
"username",
"name",
"email",
"user_role",
"active",
"dashboard_url",
]]
Markdown(tabulate(
activity,
headers=[
"Username",
"Name",
"Email",
"Role",
"Active",
"URL",
],
showindex = False,
))
```
---
title: "Licensed Users Report"
---
This document analyzes the recently active Posit Connect user accounts and
summarizes each account type and enumerates accounts.
```{r libraries}
#| echo: false
#| message: false
library(httr)
library(jsonlite)
library(magrittr)
library(dplyr)
library(knitr)
```
```{r configuration}
#| echo: false
# Confirm that environment variables are available.
connect_server <- Sys.getenv("CONNECT_SERVER")
if (nchar(connect_server) == 0) {
stop("Set the CONNECT_SERVER environment variable.")
}
connect_api_key <- Sys.getenv("CONNECT_API_KEY")
if (nchar(connect_api_key) == 0) {
stop("Set the CONNECT_API_KEY environment variable.")
}
```
```{r fetch}
#| echo: false
# Fetch all users from Posit Connect.
page <- 0
users <- c()
while (TRUE) {
page <- page + 1
cat(file = stderr(), "fetching page: ", page, "; acquired: ", nrow(users), "\n", sep = "")
res <- httr::GET(
paste0(connect_server, "__api__/v1/users"),
httr::add_headers(Authorization = paste0("Key ", connect_api_key)),
query = list(
page_number = page,
page_size = 100,
account_status = "licensed"
)
)
if (httr::http_error(res)) {
err <- sprintf(
"%s request failed with %s",
res$request$url,
httr::http_status(res)$message
)
message(capture.output(str(httr::content(res))))
stop(err)
}
payload <- httr::content(
res,
"parsed",
simplifyVector = FALSE,
simplifyDataFrame = TRUE
)
if (length(payload$results) == 0) {
break
}
users <- rbind(users, payload$results)
}
```
```{r tidying}
#| echo: false
# Reshape date fields.
parse_iso8601 <- function(str) {
as.POSIXct(strptime(str, "%Y-%m-%dT%H:%M:%SZ", tz = "UTC"))
}
users <- users %>%
mutate(
created_time = parse_iso8601(created_time),
updated_time = parse_iso8601(updated_time),
active_time = parse_iso8601(active_time)
)
```
## User role frequency
```{r frequency}
#| echo: false
# Summarize by user role.
user_roles <- users %>%
mutate(Role = user_role) %>%
select(Role) %>%
group_by(Role) %>%
summarise(N = n()) %>%
arrange(match(Role, c('administrator', 'publisher', 'viewer'))) %>%
mutate(Role = case_when(
Role == "administrator" ~ "Administrator",
Role == "publisher" ~ "Publisher",
Role == "viewer" ~ "Viewer"))
kable(user_roles)
```
## Licensed users
```{r report}
#| echo: false
# Enumerate all active users with email/url as links.
activity <- users %>%
arrange(dplyr::desc(active_time)) %>%
mutate(
name = case_when(
first_name == "" ~ last_name,
last_name == "" ~ first_name,
.default = paste(first_name, last_name, sep = " ")),
dashboard_url = paste0(connect_server, "connect/#/people/users/", guid)
) %>%
mutate(
email = paste0("<", email, ">"),
url = paste0("[link](", dashboard_url, "){target='_blank'}"),
active = strftime(active_time, "%Y-%m-%d")
) %>%
rename(
Username = username,
Name = name,
Email = email,
Role = user_role,
Active = active,
URL = url,
) %>%
select(Username, Name, Email, Role, Active, URL)
kable(activity)
```