Repository navigation
Expand file tree
/
Copy pathplex_diagnostics.py
More file actions
265 lines (227 loc) · 10.5 KB
/
Copy pathplex_diagnostics.py
File metadata and controls
265 lines (227 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
"""
plex_diagnostics.py
Plex Connect — diagnostic checks
================================
Small suite of read-only checks against the Plex API to verify connectivity,
authentication, and tenant routing. Used as a sanity layer before any sync
work and as the visible "is the right tenant connected?" indicator in the UI.
All functions are read-only and safe to run against any tenant — including
G5, where we have read access only.
"""
from typing import Any
import bootstrap
# ─────────────────────────────────────────────
# Known tenants
# Tenant IDs are not secrets — committing them is fine. These labels are
# used to make the whoami report human-readable.
#
# History: an earlier version of BRIEFING.md listed a different Grace UUID
# (a6af9c99-bce5-4938-a007-364dc5603d08). That value is dead — verified
# empirically against the live API. The real Grace tenant ID is the one
# below, which the Plex API itself returns when you GET mdm/v1/tenants
# with the Fusion2Plex Consumer Key. The old UUID is kept here labeled
# "Grace (stale)" so anyone hitting it gets a clear signal.
# ─────────────────────────────────────────────
GRACE_TENANT_ID = "58f781ba-1691-4f32-b1db-381cdb21300c" # verified Apr 2026
GRACE_OLD_TENANT_ID = "a6af9c99-bce5-4938-a007-364dc5603d08" # dead, kept for diagnostics
G5_TENANT_ID = "b406c8c4-cef0-4d62-862c-1758b702cd02"
KNOWN_TENANTS = {
GRACE_TENANT_ID: "Grace Engineering",
GRACE_OLD_TENANT_ID: "Grace (stale UUID — replace with verified one)",
G5_TENANT_ID: "G5",
}
# ─────────────────────────────────────────────
# Raw endpoint wrappers
# ─────────────────────────────────────────────
def list_tenants(client) -> Any:
"""
GET /mdm/v1/tenants
Returns the list of tenants visible to the active credential.
For a correctly-scoped credential this is typically a single tenant
(the one your API key is bound to). Useful for confirming which
tenant the credential actually lands on.
"""
return client.get("mdm", "v1", "tenants")
def get_tenant(client, tenant_id: str) -> Any:
"""
GET /mdm/v1/tenants/{id}
Returns the full record for a specific tenant. 404 if the tenant
does not exist or is not visible to the credential.
"""
return client.get("mdm", "v1", f"tenants/{tenant_id}")
# ─────────────────────────────────────────────
# Composite check — the main diagnostic
# ─────────────────────────────────────────────
def tenant_whoami(client, configured_tenant_id: str = "") -> dict:
"""
Composite tenant diagnostic.
Calls list_tenants() and (if a configured ID is provided) get_tenant(),
then compares the visible tenant(s) against the known Grace and G5 UUIDs
so the UI can show a clear "is this the right tenant?" status.
Uses ``client.get_envelope()`` so HTTP errors (401, 403, 404, 5xx) and
network failures surface as distinct ``match`` values instead of being
swallowed into ``no_data``.
Returns a structured report:
{
"configured_tenant_id": "<uuid or ''>",
"configured_tenant_label": "Grace Engineering" | "G5" | "unknown",
"visible_tenants": [{id, code, name, label}, ...],
"list_tenants_raw": <raw API body>,
"list_tenants_envelope": {ok, status, reason, elapsed_ms, error},
"get_tenant_raw": <raw API body or None>,
"match": "grace" | "g5" | "configured" |
"other" | "no_data" |
"auth_failed" | "request_failed",
"summary": "<one-line human-readable status>",
"credential_sources": [{name, source, prefix, length}, ...],
}
``credential_sources`` reports where each credential came from (shell,
infisical, .env.local, unset) via ``bootstrap.describe_sources()``. It
never carries more than an 8-character prefix of any value.
"""
report: dict = {
"configured_tenant_id": configured_tenant_id or "",
"configured_tenant_label": KNOWN_TENANTS.get(configured_tenant_id, "unknown"),
"visible_tenants": [],
"list_tenants_raw": None,
"list_tenants_envelope": None,
"get_tenant_raw": None,
"match": "no_data",
"summary": "",
"credential_sources": [
{"name": n, "source": src, "prefix": pre, "length": ln}
for n, src, pre, ln in bootstrap.describe_sources()
],
}
# ── Step 1: list_tenants via get_envelope so HTTP errors surface ────
list_env = client.get_envelope("mdm", "v1", "tenants")
report["list_tenants_envelope"] = {
"ok": list_env["ok"],
"status": list_env["status"],
"reason": list_env["reason"],
"elapsed_ms": list_env["elapsed_ms"],
"error": list_env["error"],
}
report["list_tenants_raw"] = list_env["body"]
if not list_env["ok"]:
status = list_env["status"]
if status in (401, 403):
report["match"] = "auth_failed"
report["summary"] = (
f"[ERROR] list_tenants returned HTTP {status} {list_env['reason']}. "
f"Check that PLEX_API_KEY and PLEX_API_SECRET are valid in .env.local "
f"or your shell environment. Underlying error: {list_env['error']}"
)
elif status == 0:
report["match"] = "request_failed"
report["summary"] = (
f"[ERROR] list_tenants could not reach Plex: {list_env['error']}. "
f"Check network connectivity and that {client.base} is reachable."
)
else:
report["match"] = "request_failed"
report["summary"] = (
f"[ERROR] list_tenants returned HTTP {status} {list_env['reason']}: "
f"{list_env['error']}"
)
return report
listed = list_env["body"]
# Normalize the response. Plex sometimes wraps lists in {data|items|rows}.
if isinstance(listed, list):
items = listed
elif isinstance(listed, dict):
items = (
listed.get("items")
or listed.get("data")
or listed.get("rows")
or [listed] # single tenant returned as a bare object
)
else:
items = []
visible: list[dict] = []
for t in items:
if not isinstance(t, dict):
continue
tid = t.get("id") or t.get("tenantId") or t.get("Id")
visible.append({
"id": tid,
"code": t.get("code") or t.get("Code"),
"name": t.get("name") or t.get("Name"),
"label": KNOWN_TENANTS.get(tid, "unknown"),
})
report["visible_tenants"] = visible
# ── Step 2: get_tenant for the configured ID ────────────────
if configured_tenant_id:
report["get_tenant_raw"] = get_tenant(client, configured_tenant_id)
# ── Step 3: match logic ─────────────────────
visible_ids = {t["id"] for t in visible if t.get("id")}
if not visible_ids:
report["match"] = "no_data"
report["summary"] = (
"list_tenants returned no data — the response was empty or "
"contained no parseable tenant IDs. Check the raw response "
"in this report."
)
return report
if GRACE_TENANT_ID in visible_ids:
report["match"] = "grace"
report["summary"] = (
"[OK] Connected to Grace Engineering. Tenant routing is resolved — "
"you may flip TENANT_ID in plex_api.py to the Grace UUID and "
"begin write-path testing."
)
return report
if G5_TENANT_ID in visible_ids:
report["match"] = "g5"
report["summary"] = (
"[WARN] Connected to G5 (read-only, another company's data). "
"Awaiting IT (Courtney) to complete tenant routing for Grace. "
"All writes are prohibited until this resolves — see issue #1."
)
return report
if configured_tenant_id and configured_tenant_id in visible_ids:
report["match"] = "configured"
report["summary"] = (
f"Connected to the configured tenant "
f"({report['configured_tenant_label']}), which is neither "
f"Grace nor G5. Verify this is intentional."
)
return report
report["match"] = "other"
report["summary"] = (
"Connected to an unrecognized tenant. Inspect visible_tenants in "
"this report and confirm the credential routing is what you expect."
)
return report
# ─────────────────────────────────────────────
# Standalone test
# ─────────────────────────────────────────────
if __name__ == "__main__":
import json
import sys
# Force UTF-8 stdout so em-dashes / brackets in summary strings don't
# blow up on a Windows cp1252 console.
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
from plex_api import PlexClient, API_KEY, API_SECRET, TENANT_ID, USE_TEST
if not API_KEY or not API_SECRET:
raise SystemExit(
"Missing credentials. Set PLEX_API_KEY and PLEX_API_SECRET "
"environment variables before running this diagnostic."
)
client = PlexClient(
api_key=API_KEY,
api_secret=API_SECRET,
tenant_id=TENANT_ID,
use_test=USE_TEST,
)
print(f"Plex Diagnostics — {'TEST' if USE_TEST else 'PRODUCTION'}")
print(f"Base URL: {client.base}")
print(f"Configured TENANT_ID: {TENANT_ID}\n")
report = tenant_whoami(client, TENANT_ID)
print("─" * 60)
print(report["summary"])
print("─" * 60)
print(json.dumps(report, indent=2, default=str))