Browse packs / Data / emptymalei

powerbi-cli

A command line tool for Power BI admins that saves workspaces, apps, reports, report users and workspace scans as JSON and Excel files: auth profiles for user and admin tokens, a versioned offline cache, file converters and batch scans from a YAML config, with every API call it makes written down.

emptymalei/powerbi-cliversion 1MIT 71 decisions200 checks

Build this pack

Paste this into Claude Code, Codex or Cursor, with your language. Your agent runs every step below on its own, builds the product the pack describes, and stops when it works and the pack's checks pass.

Build https://symbola.sh/packs/emptymalei/powerbi-cli/ in Python
The steps, to run yourself or to see what your agent does
  1. Install the Symbola CLI (Python 3.11+): uv tool install symbola (or pipx install symbola). Not pip install symbolon: that PyPI name is another project.

  2. Download the pack: symbola pull emptymalei/powerbi-cli --dest spec.

  3. Read spec/AGENTS.md and follow it. Build only from spec/: don't look for the original product's source, the registry repository (it holds holdout tests) or other builds. Packs released before the rename say symbolon <command>: run symbola <command> instead. This is a product pack: spec/PRODUCT.md describes the whole product (its shape, surfaces and flows). Build that product, in the user's stack, with the components below as its core. Don't build a demo page around the components instead.

  4. Build each component in its own folder and run its checks as you go, fixing what fails:

    symbola impl-init impl/auth-profiles --contract spec/components/auth-profiles/versions/1
    symbola test spec/components/auth-profiles/versions/1 impl/auth-profiles
    

    In Python, define run(input) in impl.py: it takes and returns a dict, and raises ValueError for input the contract rejects. Pin third-party packages with == in requirements.lock. In another language, set language = "command" and command = [...] in symbolon-impl.toml; the program reads one JSON input per line on stdin and writes {"output": ...} per line. A component whose symbolon.toml says driver = "cli" is a command line: build the real program, set command to it, and symbola runs it once per check.

  5. When every component passes, run symbola keygen once, then for each component symbola attest spec/components/auth-profiles/versions/1 impl/auth-profiles -o attestations/auth-profiles.json. On macOS, attest needs Docker running and accepts Python, Node or a static Linux binary (for Go, GOOS=linux); symbola test runs the same checks without it.

You are done when every flow in spec/PRODUCT.md works in the product you built and attest reports PASSED for every component. Write open questions and the readings you chose in NOTES.md. Every command is described at https://kausalflow.github.io/symbola/cli/.

Components:

  • auth-profiles: spec/components/auth-profiles/versions/1
  • cache-keys: spec/components/cache-keys/versions/1
  • cached-reads: spec/components/cached-reads/versions/1
  • config-cache: spec/components/config-cache/versions/1
  • file-converters: spec/components/file-converters/versions/1
  • flatten: spec/components/flatten/versions/1
  • scan-batch: spec/components/scan-batch/versions/1
  • workspace-entries: spec/components/workspace-entries/versions/1

Every command: Symbola CLI docs. Agents can read these steps as plain text in llms.txt.

Evidence

Who has built it

Test results that ship with the pack, one column per build. 8 of 8 components pass a build · 1 build · 1 runner.

Componentemptymalei
auth-profiles ✓ 45 checks
cache-keys ✓ 17 checks
cached-reads ✓ 22 checks
config-cache ✓ 39 checks
file-converters ✓ 25 checks
flatten ✓ 11 checks
scan-batch ✓ 26 checks
workspace-entries ✓ 15 checks

Runners: runner-i · newest 2026-10-28. Hover a result for details.

Decisions

What a build must do

Each one is a decision a builder could get wrong. Decided is required, default may change in a fork with a note, and open is deliberately left to the builder.

auth

  1. A token given as `Bearer <token>` is stored without the prefix. auth.token-prefix · 2 checks
  2. Without `-g` a saved profile becomes the ungrouped active profile when none is active, or when it is named `default`; with `-g` it becomes the group's active profile when the group has none. An existing active profile otherwise stays, and a grouped save leaves the ungrouped profiles alone. auth.activation · 6 checks
  3. Tokens are kept once per profile name in `~/.pbi_cli/credentials.json` (a JSON object, indent 2), shared by the ungrouped and grouped profiles of that name: saving replaces the token, deleting a profile deletes it, even if another list still has a profile of that name. auth.token-per-name · 5 checks
  4. The credentials file is readable and writable only by its owner (mode 600). auth.credentials-private · 0 checks
  5. When an OS keyring works, tokens are stored there (service `pbi-cli`, user name = profile name) and read from there first; the credentials file is only the fallback, also when the keyring raises any error. auth.keyring · 0 checks
  6. Old installations keep working: a legacy `profiles.json` is imported into a new `config.yaml`, and a legacy `auth.json` token becomes the ungrouped profile `default` when there are no ungrouped profiles. auth.legacy-import · 2 checks · default
  7. A command uses the active profile of its group (`user`, or `admin` for the admin-only commands) if that is one of the group's profiles; otherwise the ungrouped active profile, which must be one of the ungrouped profiles; it then needs a token for it. A group profile without a token does not fall back. Failures exit 1 with an `Error:` line naming the group or profile. auth.resolution · 13 checks

profile

  1. `profile list` shows the ungrouped profiles (✓ when a token is stored under the name, ✗ when not, `(active)` on the active one) and then each group, with a hint for each empty part. profile.list · 4 checks
  2. `profile switch NAME [-g G]` makes NAME the active profile of that list; an unknown NAME, or an empty list, is reported and changes nothing, with exit 0. profile.switch · 7 checks
  3. Without a name, `profile switch` lists the profiles numbered from 1, reads the number from standard input (an empty answer is 1) and switches; a number out of range prints `Invalid selection` and changes nothing. profile.switch-interactive · 3 checks · default
  4. `profile delete NAME [-g G]` asks for confirmation (`--yes` skips it; declining aborts with exit 1), removes the profile and its token, and when it was the active one activates the first remaining profile of that list (none if none remain), announcing it. An unknown name is reported with exit 0. profile.delete · 8 checks

config

  1. `~/.pbi_cli/config.yaml` is a block-style YAML mapping with `active_profile`, `profiles`, `groups` (`user`, `admin`, each with `active_profile` and `profiles`), `default_output_folder`, `cache_folder` and `cache_enabled` (default true). A missing or unreadable file reads as the defaults; a change rewrites the whole file and keeps all other keys as they were. config.file · 7 checks
  2. `config set-output-folder P` and `set-cache-folder P` drop surrounding quotes from P, expand `~` to the home directory, make a relative path absolute from the current directory and store it without a trailing slash; they print `✓ ... set to: <path>` and a hint line. config.set-folder · 5 checks
  3. A cache folder starting with `s3://`, `gs://` or `az://` is stored and shown as given. config.cloud-cache-folder · 3 checks
  4. `get-output-folder` and `get-cache-folder` print the stored folder (the cache one also `Cache enabled: True|False`) or say it is not configured; `enable-cache` and `disable-cache` set `cache_enabled` and print `✓ Cache enabled` / `✓ Cache disabled`. config.get-and-switch · 7 checks
  5. `config show` prints the active ungrouped profile, default output folder, cache folder, whether the cache is enabled, the profile count, the ungrouped profiles, and per group its profile count and active profile. config.show · 3 checks · default
  6. `verify_certificates` in `config.yaml` defaults to true. `pbi config set-verify-certificates true|false|PATH` sets it (`true`/`false` in any case; a path must name an existing file and is stored absolute, else exit 1 and nothing changes); setting false prints a warning. `pbi config get-verify-certificates` prints `Verify certificates: <value>`. config.verify-certificates · 7 checks

cache

  1. The cache folder holds `<key>/<version>/<key>.json`. A version is a directory under the key that contains `<key>.json`; versions are listed newest first (descending string order); a key is listed when it has at least one version, keys sorted; the latest data of a key is its first version. cache.layout · 8 checks
  2. A save creates one file `<key>/<version>/<key>.json` whose version is the local time as `YYYYMMDD_HHMMSS`, holding a JSON object with `cache_key`, `cached_at` (ISO 8601 time), `version`, `metadata` and `data`. cache.entry-format · 3 checks
  3. The cache key of `workspaces list` is `workspaces`, of `apps list` `apps_<role>`, of `users user-access` `user_access_<slug of the id>`; the request metadata saved is `{top, expand, filter}`, `{role}` and `{user_id}`. cache.keys · 16 checks
  4. `cache list [-k KEY]` and `cache clear [-k KEY] [-v VERSION]` print what is in the cache or report what was cleared, as in the examples; `clear` asks for confirmation (`--yes` skips; declining aborts with exit 1); `-v` without `-k` is refused with a message (exit 0); with no cache folder both say so. cache.list-and-clear · 15 checks
  5. `cache clear` removes everything in the folder (the folder stays), the key's whole folder, or the one version's file, and reports success even when nothing was there. cache.clear-effect · 0 checks
  6. A list command reads the cache only with `--use-cache` or `--cache-only`, and only when a cache folder is set and caching is enabled. cache.read-conditions · 7 checks
  7. A read takes the latest version of the command's key. cache.read-latest · 2 checks
  8. On a hit the first line is `Using cached data from <cached_at> (version: <version>)` with both fields taken from inside the cache file, not from the folder name. cache.notice-from-file · 3 checks · default
  9. If the latest version's file cannot be read as JSON the cache counts as unavailable; an older version is not used instead. cache.unreadable-latest · 2 checks · default
  10. With `--cache-only` and no usable cache, standard output says `Error: Cache not available and --cache-only was specified` (or, when no cache is set up or it is disabled, `Error: Cache not configured and --cache-only was specified` and a hint line), and the command exits 1 with `Aborted!`. cache.miss-cache-only · 6 checks
  11. With only `--use-cache` and an empty cache the command says `Cache not available, fetching from API...` (not when no cache is set up) and goes on to the API. cache.miss-use-cache · 3 checks
  12. Without a target folder the result is printed as a blank line, a rule of 80 `=`, the title (`Workspaces: n record(s)`, `Apps (<role>): n record(s)`, `User Access Information for: <id>`), a rule, the table (header line, one line per record) and a rule; with no records it prints `No workspaces found.` / `No apps (<role>) found.`. cache.table · 8 checks
  13. The workspaces table shows `id, name, type, state, isReadOnly, isOnDedicatedCapacity` and the apps table `id, name, description, publishedBy, lastUpdate`, in that order and only those that exist; when none exists all columns are shown, nested objects flattened into dotted columns. cache.table-columns · 5 checks · default
  14. With `--target-folder`, the list commands save `<name>.json` (the data on one line) and, with `-ft excel`, `<name>.xlsx`; `-n` sets the name (default `workspaces`, `apps`); the folder is created when missing and announced with `creating folder <path>`. cache.save-files · 3 checks
  15. A relative target folder is placed under `default_output_folder` when one is set, else under the current directory. cache.target-under-default-output · 2 checks
  16. `users user-access` with `--target-folder` is meant to save `<name>.json` / `.xlsx` like the other list commands; the original only prints and saves nothing. cache.user-access-save · 0 checks · open
  17. `workspaces list --use-cache|--cache-only -ft excel -tf ...`: what is saved as the workbook when the data came from the cache (the original crashes after writing the JSON). cache.workspaces-excel-from-cache · 0 checks · open

flatten

  1. Workspaces become seven sheets, in the order users, reports, dashboards, datasets, dataflows, workbooks, workspace: one row per child with the workspace's own (non-list) fields, in order, followed by the child's fields as `<child>_<field>`; sheet `workspace` has one row of own fields per workspace. Other list fields are dropped; sheets with no rows are kept. flatten.workspaces · 3 checks
  2. An app becomes the sheets users, reports, dashboards and app by the same rules. flatten.app · 3 checks
  3. Report users are flattened per workspace name: one row per user of each report, with the report's fields (without the users field) and the user's fields as `<key>_<field>` (key `reports_users_value` unless given); a report without users produces no row. flatten.report-users · 4 checks
  4. When a child's prefixed field has the same name as an own field, the child's value replaces it in place. flatten.child-wins · 1 check · default
  5. Each sheet is written as one worksheet of the same name, with a header row of the column names, no index column, and nested objects in a cell spread over dotted columns (`a.b`). (`apps list -ft excel` is an exception in the original: it has an index column; see REVIEW.) flatten.workbook · 0 checks
  6. Converting or saving as Excel a listing with no workspaces (an empty `value`, or no `value`): the original fails with an internal error. flatten.empty-listing · 0 checks · open

convert

  1. `workspaces format-convert -s S -t T` writes the workbook of the `flatten` sheets to T, whatever T is called, without needing a token; a missing or non-JSON S, or a T in a missing folder, fails. convert.workbook · 7 checks
  2. `apps augment` and `reports users` resolve the user group's token before anything else and write nothing without one. convert.apps-token · 3 checks
  3. A source with no apps (no `value`, or an empty list) needs no request: a json target holds `[]`. convert.apps-no-apps · 5 checks
  4. A missing source, or a source that is not a JSON object, fails. convert.apps-source · 5 checks
  5. With `-ft excel` the target is a folder without a file extension, created when missing; a target with an extension is refused and nothing is written (the original fails with an internal error where it means a usage error). convert.excel-target · 5 checks · default
  6. With apps in the source each app is fetched in full (`app.endpoint-app`); one the API answers with an error is reported in red (`Cannot download <app info>`) and skipped; a json target holds the list of fetched apps, an excel target gets `<app name>_<app id>.xlsx` per app (sheets of `flatten` kind app). convert.apps-fetch · 0 checks
  7. `reports users` also fetches the users of each report of each fetched app (`app.endpoint-report-users`) and replaces the report by the users answer's fields followed by the report's fields; a report whose fetch fails with an error answer is dropped from the app and logged; an excel target gets `<app name>_<app id>_report_users.xlsx`. convert.report-users · 0 checks

scan

  1. Each `workspace_ids` entry is a string (an id) or a mapping with a non-empty `id` and an optional `name`; entries keep their order, extra keys are ignored. scan.entry-shape · 5 checks
  2. An entry's label is `<name> (<id>)` or the id; its result file is `<slug of name>-<id>.json`, or `<id>.json` when there is no name or its slug is empty. The slug is ASCII (accents folded), lowercase letters and digits with every other run of characters as one `-`, no leading or trailing `-`. scan.label-and-file-name · 5 checks
  3. `workspace_ids` that is not a list, or an entry that is not a string or a mapping with an `id`, makes the config invalid. scan.entry-validation · 11 checks
  4. Config problems are reported one at a time, in this order: not a mapping; bad `workspace_ids`; no entries; no `target_folder`; then (after the folder is created) a non-boolean flag in the order lineage, datasource_details, dataset_schema, dataset_expressions, get_artifact_users; then non-numeric, then non-positive `interval`/`timeout`. Each is an `Error:` line and exit 1. scan.config-order · 14 checks
  5. A flag must be a YAML boolean; `1`, a string and null are refused with `'<flag>' must be a boolean value.` scan.flags-boolean · 4 checks
  6. `interval` (default 5) and `timeout` (default 300) are numbers or numeric strings; null or absent means the default; they must be above 0. scan.interval-timeout · 6 checks
  7. The config is read as YAML 1.1: `yes`, `no`, `on` and `off` are booleans. scan.yaml-1-1 · 1 check · default
  8. `target_folder` is resolved like other output folders (relative: under `default_output_folder` when set, else the current directory), created when missing and announced as `creating folder <path>` before the flags are checked. scan.target-folder · 4 checks
  9. Workspaces run one after another, each announced by a blank line and `=== Workspace <label> ===`; a failure prints `✗ <label>: <message>` and the loop goes on. scan.loop · 4 checks
  10. If any workspace failed, after the loop a blank line and `Failed workspaces: <labels, comma separated>` are printed and the exit code is 1; otherwise `✓ Scanned <n> workspace(s) into <path>` and exit 0. scan.exit-code · 4 checks
  11. A scanned workspace's result is saved as `<target folder>/<file stem>.json` (indent 2) and announced `✓ Saved <path>`. scan.result-file · 0 checks
  12. Each workspace gets its own scan: initiate with just its id, poll its status, fetch its result; a failure of one does not affect the others. scan.own-cycle · 0 checks

app

  1. Every request carries `Authorization: Bearer <token>`; GETs are plain, the one POST sends JSON. Failed connections and HTTP 500, 502 and 504 are retried up to 5 times with backoff factor 0.3. app.http · 0 checks · default
  2. Every API request verifies TLS certificates as the `verify_certificates` setting says (`config.verify-certificates`): true against the system CA bundle, a path against that CA bundle file, false not at all. Nothing else turns verification off. app.verify-certificates · 0 checks
  3. A JSON answer with an `error` key is a failure that shows the answer; HTTP errors of report, scan and export calls fail with their status. app.api-errors · 0 checks
  4. `workspaces list` calls `GET /admin/groups` with the query `$top=<top>` (default 1000), `$expand=<kinds joined by commas>` (default all of users, reports, dashboards, datasets, dataflows, workbooks) and, when given, `$filter=<odata filter>`; it prints `Retrieving workspaces for: ...` first. app.endpoint-workspaces-list · 0 checks
  5. `apps list` calls `GET /apps` for role user and `GET /admin/apps?$top=200` for role admin (admin token); it prints `Listing Apps as <role>` first. app.endpoint-apps-list · 0 checks
  6. `apps app` and `apps augment` get one app from `GET /apps/{appId}`, its reports from `GET /apps/{appId}/reports` and its dashboards from `GET /apps/{appId}/dashboards`, and return the app object with `reports` and `dashboards` lists (the `value` of those answers). app.endpoint-app · 0 checks
  7. `users user-access` calls `GET /admin/users/{userId}/artifactAccess` (admin token), takes `ArtifactAccessEntities` from the first page only (a continuation URI is not followed) and returns `{"artifacts": [...]}`. app.endpoint-user-access · 0 checks
  8. `reports list` calls `GET /groups/{groupId}/reports`; `reports pages` calls `GET /groups/{groupId}/reports/{reportId}/pages`, or for every report of the group (a report whose call fails is logged and skipped) a list of `{report_id, report_name, pages}`; `reports export` and `export` call `GET /groups/{groupId}/reports/{reportId}/Export` and save the bytes; the list and pages results are printed or saved as JSON with indent 2. app.endpoint-reports · 0 checks
  9. The users of a report come from `GET /admin/reports/{reportId}/users` (admin API). `workspaces report-users` reads the `reports` sheet of the Excel file `workspaces list` saved, keeps rows whose `type` is `Workspace` and (with `-wn`) whose `name` is listed, requests each row's `reports_id`, waits `-wi` seconds (default 3) after each success, merges the answer into the row as `reports_users_<field>` plus the row's own fields, logs failures and goes on, and saves `workspaces_reports_users.json` / `.xlsx` (json and excel by default) per workspace name, or prints a summary table without a target folder. app.endpoint-report-users · 0 checks
  10. A scan is `POST /admin/workspaces/getInfo?lineage=<bool>&datasourceDetails=<bool>&datasetSchema=<bool>&datasetExpressions=<bool>&getArtifactUsers=<bool>` with the JSON body `{"workspaces": [ids]}` (booleans as `true`/`false`, all five always sent), its state is `GET /admin/workspaces/scanStatus/{scanId}` and its result `GET /admin/workspaces/scanResult/{scanId}`. app.endpoint-scan · 0 checks
  11. `scan get` and each `scan batch` workspace poll the status every `interval` seconds (default 5, a positive number) until `Succeeded`, then fetch the result; `Failed` and running past `timeout` seconds (default 300) end the workspace with an error naming the scan id; a result that is HTTP 202 is not ready. app.scan-polling · 0 checks
  12. `scan get` takes one or more ids; `--target` and `--target-folder` exclude each other, and `--target-folder` needs exactly one id and saves `<id>.json` (indent 2); otherwise the result is printed as JSON. `scan initiate` prints the API answer, `scan status` the status answer, `scan result [-t FILE]` the result. app.scan-get-options · 0 checks
Assumptions

What it takes for granted

Facts about the world the pack relies on, and how to notice when one stops holding.

K-api-limits

The admin endpoints are throttled by Power BI and the tool does not back off beyond a fixed retry on HTTP 500, 502 and 504; users keep to the limits with `--wait-interval`, scan `interval`/`timeout` and by scanning workspaces one by one.

Watch for: HTTP 429 answers.

K-api-shape

The Power BI REST API answers with the shapes the tool reads: lists under `value`, failures under an `error` key, user access under `ArtifactAccessEntities`, scan ids under `id`, scan states `NotStarted`, `Running`, `Succeeded` or `Failed`, and HTTP 202 while a scan result is not ready.

Watch for: none

K-bearer-token

The user supplies a valid Power BI bearer token (for the admin commands, one with admin rights on the tenant) and replaces it when it expires; the tool never obtains, refreshes or validates one.

Watch for: HTTP 401 and 403 answers from the API; the tool itself reports the failure.

K-keyring-optional

A machine may have no OS keyring. Then tokens are kept in `~/.pbi_cli/credentials.json`; with a keyring they are kept there (service `pbi-cli`, user name = profile name) and the file is not used. The checks run without a keyring.

Watch for: `pbi profile list` shows ✓ for a profile only if its token is found in either place.

K-single-user

One `pbi` process at a time uses a home directory and a cache folder, and the machine's clock does not go backwards between runs.

Watch for: none

Brief

powerbi-cli (pbi)

pbi is a command line tool for people who administer or document a Power BI tenant. They give it a bearer token, and it asks the Power BI REST API (https://api.powerbi.com/v1.0/myorg) for workspaces, apps, reports, report users, user access and workspace scans, and saves the answers as JSON files and Excel workbooks, one folder per day if they like. Results can be cached in a folder (local or S3) so later runs can work offline, and saved files can be fed back into later commands (convert to Excel, fetch the users of each report).

Its users script it (PowerShell or bash loops, scheduled backups), so what they rely on is the command line, the exit codes, the names and shapes of the files it writes, the profile/config/cache files in ~/.pbi_cli, and the messages that say why a run failed. Almost every command needs the live API and cannot be checked here; the pack pins what can be checked offline (profiles and settings, the cache, the saved-file converters, the scan config) and records the API calls as decisions.

Non-goals

  • Getting a token. The user brings a bearer token; there is no sign-in flow, no refresh, no service principal login.
  • Changing anything in Power BI. Every call is a read (the one POST asks for a scan).
  • A graphical interface, a server, or a library API for others to import.
  • Other clouds (national or sovereign Power BI endpoints) and API versions other than v1.0/myorg.
  • Concurrency control: two pbi processes sharing one home directory are not supported.

Priorities

When goals conflict, earlier ones win:

  1. Scripts keep working: command names, options, exit codes, file names and the shapes of saved JSON and Excel files do not change.
  2. Tokens stay out of sight: kept in the OS keyring when there is one, otherwise in a file only the user can read; never printed or logged.
  3. A run fails loudly and says why; one failing workspace or app does not stop a batch that can go on.
  4. Work offline from the cache and from saved files whenever the data is already there.
  5. Few dependencies, plain files, no hidden state beyond ~/.pbi_cli and the cache folder.

Who resolves gaps

emptymalei, as the namespace holder. If the pack doesn't settle something, record the question and the reading you chose in NOTES.md next to your build; obligations marked open in obligations.toml are yours to choose.

Product

powerbi-cli: the product

pbi is one command line program that reads Power BI's REST API and saves the answers as files. The components in this pack are its core (the parts that can be checked without the API); this page says what the whole program is.

Shape

  • A command line program, pbi, written in Python (3.10 or later) and installed as a package named pbi_cli (uv sync, then uv run pbi, or pip install). It runs on Windows, macOS and Linux; its documentation shows PowerShell and bash loops around it. No server, no daemon, no GUI.
  • Command line parsing with click, nested command groups with --help on each, short options like -t, -tf, -ft, -wn, -wi as listed below. Run with no command it greets and points to --help; pbi version prints the installed package version.
  • Tables and Excel files with pandas and openpyxl; configuration and the scan config with YAML; HTTP with requests; file names with python-slugify; cache locations through cloudpathlib (local folders and s3://); the OS keyring through keyring when one exists. Logging goes to standard error at INFO level; results and messages go to standard output.
  • It needs a Power BI bearer token supplied by the user (see Interfaces). Commands marked admin need a token with admin rights.
  • State lives in ~/.pbi_cli/ (config.yaml, credentials.json, the legacy auth.json and profiles.json) and in the optional cache folder. Nothing else is kept between runs.

Surfaces

Setup and local state (offline, checked):

  • pbi auth -t TOKEN [-p PROFILE] [-g user|admin]: store a token as a profile. (auth-profiles)
  • pbi profile list | switch [NAME] [-g G] | delete NAME [-g G] [--yes]: manage profiles. (auth-profiles)
  • pbi config set-output-folder P | get-output-folder | set-cache-folder P | get-cache-folder | enable-cache | disable-cache | set-verify-certificates true|false|PATH | show: settings. (config-cache)
  • pbi cache list [-k KEY] | clear [-k KEY] [-v VERSION] [--yes]: look at and empty the cache folder. (config-cache, cache-keys)
  • pbi version, and pbi alone (a greeting).

Reading the API (needs a token and the network; each can also read the cache, see Flows):

  • pbi workspaces list [--top N] [-e KIND]... [-f ODATA] [-ft json|excel]... [-tf FOLDER] [-n NAME] [--use-cache|--cache-only]: admin; all workspaces, with users, reports, dashboards, datasets, dataflows, workbooks expanded by default; prints a table, or saves NAME.json and NAME.xlsx (default name workspaces).
  • pbi apps list [-r user|admin] [-tf FOLDER] [-ft ...] [-n NAME] [--use-cache|--cache-only]: the apps the user can see, or all apps of the tenant (admin); default file name apps.
  • pbi apps app -a APP_ID [-t FILE] [-ft json|excel]: one app with its reports and dashboards; prints JSON, or saves it (an Excel file has the sheets of flatten kind app).
  • pbi users user-access -u USER_ID [-tf FOLDER] [-ft ...] [-n NAME] [--use-cache|--cache-only]: admin; the artifacts a user can access, as {"artifacts": [...]}.
  • pbi reports list -g GROUP_ID [-t FILE], pbi reports pages -g GROUP_ID [-r REPORT_ID] [-t FILE], pbi reports export -g GROUP_ID -r REPORT_ID [-t FILE] and the older pbi export -g GROUP_ID -r REPORT_ID [-t FILE]: reports of a workspace, their pages (all reports of the group when -r is left out; a report whose pages cannot be fetched is skipped and logged), and the exported report file (without -t it prints the size instead of the content).
  • pbi workspaces scan initiate IDS... | status SCAN_ID | result SCAN_ID [-t FILE] | get IDS... [-t FILE | -tf FOLDER]: admin; the workspace-info scan, step by step or all at once, with --lineage --datasource-details --dataset-schema --dataset-expressions --get-artifact-users, --interval (default 5) and --timeout (default 300) for get. -t and -tf exclude each other and -tf takes exactly one workspace id (it saves <id>.json).
  • pbi workspaces scan batch -c CONFIG.yaml: admin; scans many workspaces one by one from a config file. (scan-batch, workspace-entries)

Working on saved files:

  • pbi workspaces format-convert -s WORKSPACES.json -t WORKSPACES.xlsx: JSON to Excel. (file-converters, flatten)
  • pbi apps augment -s APPS.json -t OUT [-ft json|excel]: fetch every listed app in full. (file-converters, flatten)
  • pbi reports users -s APPS.json -t OUT [-ft json|excel]: the same, plus the users of each report. (file-converters, flatten)
  • pbi workspaces report-users -s WORKSPACES.xlsx [-wn NAME]... [-wi SECONDS] [-ft json|excel]... [-t FOLDER] [-n NAME]: admin; reads the reports sheet of the Excel file workspaces list saved, keeps rows whose type is Workspace (and, with -wn, whose workspace name is listed), fetches the users of each report (reports_id column), waiting -wi seconds (default 3) between requests, and saves workspaces_reports_users.json/.xlsx (name with -n) with one list of enriched report records per workspace name, flattened by flatten kind report_users; prints a summary table without -t.

Files users touch: ~/.pbi_cli/config.yaml, ~/.pbi_cli/credentials.json, the cache folder (<key>/<version>/<key>.json), saved *.json and *.xlsx results, scan config YAML files (an example is examples/scan_config.example.yaml).

Flows

  1. First setup. pbi auth -t TOKEN -p work -g admin (and -g user for a user token), pbi profile list shows ✓ marks and the active profile of each group, pbi profile switch work -g admin changes it. Commands then use the right group's active profile. (auth-profiles)
  2. Back up all workspaces. pbi config set-output-folder ~/PowerBI/backups, then pbi workspaces list -ft json -ft excel -tf 2024-01-01 fetches (admin API), caches the result if a cache folder is set (cache-keys), and saves workspaces.json and a workbook with the sheets users, reports, dashboards, datasets, dataflows, workbooks and workspace (flatten) in ~/PowerBI/backups/2024-01-01/. A JSON saved earlier is converted later with pbi workspaces format-convert. (config-cache, file-converters)
  3. Work offline from the cache. pbi config set-cache-folder ~/PowerBI/cache once; every list command saves what it fetched as a new version. pbi workspaces list --cache-only prints the latest saved result without a token or network, --use-cache falls back to the API when nothing is cached; pbi cache list [-k workspaces] shows keys and versions, pbi cache clear -k workspaces -v VERSION removes some. (cached-reads, config-cache, cache-keys)
  4. Who can open which report. pbi workspaces list -ft excel -tf ..., then pbi workspaces report-users -s workspaces.xlsx -wn "Finance" -wi 5 -tf ... saves the report users of one workspace; or pbi apps list -tf ..., pbi reports users -s apps.json -t out.json for apps. (file-converters, flatten)
  5. Scan many workspaces. Copy scan_config.example.yaml, list workspace ids (with optional names for readable file names), set target_folder, run pbi workspaces scan batch -c scan.yaml: each workspace is scanned on its own (start the scan, poll its status every interval seconds up to timeout, fetch the result) and saved as <name-slug>-<id>.json; failed workspaces are listed at the end and the exit code is 1. (scan-batch, workspace-entries)
  6. Look at one thing. pbi apps app -a ID, pbi reports list -g GROUP, pbi reports pages -g GROUP -r REPORT, pbi users user-access -u USER, pbi reports export -g GROUP -r REPORT -t report.pbix, pbi workspaces scan get IDS... --lineage -t result.json.

Interfaces

  • Power BI REST API, https://api.powerbi.com/v1.0/myorg (hard-coded), with the header Authorization: Bearer <token> on every call. TLS certificates are verified unless the verify_certificates setting says otherwise (app.verify-certificates). Failed connections and HTTP 500, 502 and 504 are retried up to 5 times with a 0.3 backoff factor. Which endpoint and query parameters each command uses is in the app.* obligations.
  • Credentials: bearer tokens per profile name, in the OS keyring (service pbi-cli) when it works, otherwise in ~/.pbi_cli/credentials.json (mode 600). Profiles are ungrouped or in the groups user and admin.
  • Cache storage: any folder cloudpathlib understands; local folders and s3:// are documented (credentials for S3 come from the standard AWS configuration), gs:// and az:// are accepted by the setting.

Order

Build the state layer first (auth-profiles, config-cache, the cache layout of cache-keys), then flatten and workspace-entries, then the saved-file commands and cached-reads, then the API commands one by one (workspaces list, apps, users, reports) and last the scans (scan-batch, scan get). A build that has the offline commands and the cache already serves scripted users who work from saved files.

Files

Everything in the pack

/ 5
assumptions 5
components/auth-profiles/versions/1 5
components/auth-profiles/versions/1/mutants 7
components/auth-profiles/versions/1/schema 2
components/cache-keys/versions/1 4
components/cache-keys/versions/1/mutants 6
components/cache-keys/versions/1/schema 2
components/cached-reads/versions/1 5
components/cached-reads/versions/1/mutants 6
components/cached-reads/versions/1/schema 2
components/config-cache/versions/1 5
components/config-cache/versions/1/mutants 7
components/config-cache/versions/1/schema 2
components/file-converters/versions/1 5
components/file-converters/versions/1/mutants 5
components/file-converters/versions/1/schema 2
components/flatten/versions/1 4
components/flatten/versions/1/mutants 6
components/flatten/versions/1/schema 2
components/scan-batch/versions/1 5
components/scan-batch/versions/1/mutants 5
components/scan-batch/versions/1/schema 2
components/workspace-entries/versions/1 4
components/workspace-entries/versions/1/mutants 6
components/workspace-entries/versions/1/schema 2