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)
```