protegrity-ai-developer-python 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appython/__init__.py +12 -0
- appython/protector.py +554 -0
- appython/service/auth_provider.py +273 -0
- appython/service/auth_token_provider.py +45 -0
- appython/service/config.py +209 -0
- appython/service/payload_builder.py +141 -0
- appython/service/request_handler.py +115 -0
- appython/service/response_handler.py +78 -0
- appython/stats/__init__.py +3 -0
- appython/stats/collector.py +90 -0
- appython/stats/writer.py +185 -0
- appython/utils/codec_helper.py +86 -0
- appython/utils/constants.py +246 -0
- appython/utils/exceptions.py +141 -0
- appython/utils/input_preprocessor.py +325 -0
- appython/utils/output_postprocessor.py +99 -0
- protegrity_ai_developer_python-1.2.1.dist-info/METADATA +428 -0
- protegrity_ai_developer_python-1.2.1.dist-info/RECORD +53 -0
- protegrity_ai_developer_python-1.2.1.dist-info/WHEEL +5 -0
- protegrity_ai_developer_python-1.2.1.dist-info/entry_points.txt +2 -0
- protegrity_ai_developer_python-1.2.1.dist-info/licenses/LICENSE +21 -0
- protegrity_ai_developer_python-1.2.1.dist-info/top_level.txt +3 -0
- protegrity_developer_python/__init__.py +4 -0
- protegrity_developer_python/scan.py +37 -0
- protegrity_developer_python/securefind.py +83 -0
- protegrity_developer_python/utils/ccn_processing.py +59 -0
- protegrity_developer_python/utils/config.py +60 -0
- protegrity_developer_python/utils/constants.py +123 -0
- protegrity_developer_python/utils/discover.py +49 -0
- protegrity_developer_python/utils/logger.py +23 -0
- protegrity_developer_python/utils/pii_processing.py +291 -0
- protegrity_developer_python/utils/protector.py +23 -0
- protegrity_developer_python/utils/semantic_guardrails.py +240 -0
- protegrity_developer_python/utils/transform.py +66 -0
- pty_migrate/__init__.py +1 -0
- pty_migrate/check_cmd.py +871 -0
- pty_migrate/cli.py +93 -0
- pty_migrate/config.py +127 -0
- pty_migrate/create_policy_cmd.py +795 -0
- pty_migrate/payloads/__init__.py +51 -0
- pty_migrate/payloads/alphabets.json +42 -0
- pty_migrate/payloads/dataelements.json +342 -0
- pty_migrate/payloads/datastores.json +7 -0
- pty_migrate/payloads/deploy_policy_ta.json +1 -0
- pty_migrate/payloads/masks.json +18 -0
- pty_migrate/payloads/members.json +62 -0
- pty_migrate/payloads/policies.json +13 -0
- pty_migrate/payloads/roles.json +32 -0
- pty_migrate/payloads/rules.json +1639 -0
- pty_migrate/payloads/sources.json +10 -0
- pty_migrate/payloads/trusted_apps.json +8 -0
- pty_migrate/ppc_client.py +371 -0
- pty_migrate/stats_cmd.py +87 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""PPC Admin API client for pty-migrate create-policy.
|
|
2
|
+
|
|
3
|
+
Provides full PIM v2 API coverage for creating the DE policy on a
|
|
4
|
+
Team Edition PPC cluster. Supports JWT auth with auto-refresh.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
import urllib3
|
|
12
|
+
|
|
13
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PPCClient:
|
|
19
|
+
"""Client for Protegrity PPC (PIM v2) Admin API with JWT auto-refresh."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, host, user, password, port=443):
|
|
22
|
+
self._base_url = f"https://{host}:{port}/pty"
|
|
23
|
+
self._user = user
|
|
24
|
+
self._password = password
|
|
25
|
+
self._access_token = None
|
|
26
|
+
self._refresh_token = None
|
|
27
|
+
self._token_expiry = 0
|
|
28
|
+
self._refresh_expiry = 0
|
|
29
|
+
self._session = requests.Session()
|
|
30
|
+
self._session.verify = False
|
|
31
|
+
self._session.timeout = 30
|
|
32
|
+
|
|
33
|
+
# ──────────────────────────────────────────────────────────
|
|
34
|
+
# Authentication with auto-refresh
|
|
35
|
+
# ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
def authenticate(self):
|
|
38
|
+
"""Obtain JWT tokens from PPC auth endpoint.
|
|
39
|
+
|
|
40
|
+
PPC has two login endpoints:
|
|
41
|
+
- /api/v1/auth/login/token: Returns token in 'pty_access_jwt_token' header.
|
|
42
|
+
This token is recognized by the gateway's ext auth SecurityPolicy.
|
|
43
|
+
- /pty/v1/auth/login/token: Returns token in JSON body. This token is NOT
|
|
44
|
+
recognized by the gateway ext auth (causes 401 on protected routes).
|
|
45
|
+
|
|
46
|
+
We try the header-based endpoint first, falling back to the body-based one.
|
|
47
|
+
"""
|
|
48
|
+
# Try header-based auth first (gateway-compatible)
|
|
49
|
+
base = self._base_url[:-4] if self._base_url.endswith("/pty") else self._base_url
|
|
50
|
+
url = f"{base}/api/v1/auth/login/token"
|
|
51
|
+
resp = self._session.post(
|
|
52
|
+
url,
|
|
53
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
54
|
+
data={"loginname": self._user, "password": self._password},
|
|
55
|
+
)
|
|
56
|
+
if resp.status_code == 200:
|
|
57
|
+
token = resp.headers.get("pty_access_jwt_token", "").strip()
|
|
58
|
+
if token:
|
|
59
|
+
self._access_token = token
|
|
60
|
+
self._refresh_token = None # header-based auth doesn't return refresh token
|
|
61
|
+
self._token_expiry = time.time() + 300
|
|
62
|
+
self._session.headers["Authorization"] = f"Bearer {self._access_token}"
|
|
63
|
+
logger.debug("PPC authentication successful (header-based)")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Fallback to JSON body-based auth
|
|
67
|
+
url = f"{self._base_url}/v1/auth/login/token"
|
|
68
|
+
resp = self._session.post(
|
|
69
|
+
url,
|
|
70
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
71
|
+
data={"loginname": self._user, "password": self._password},
|
|
72
|
+
)
|
|
73
|
+
if resp.status_code != 200:
|
|
74
|
+
raise RuntimeError(f"Authentication failed: {resp.status_code} {resp.text}")
|
|
75
|
+
data = resp.json().get("data", resp.json())
|
|
76
|
+
self._access_token = data.get("accessToken") or data.get("token") or data.get("access_token")
|
|
77
|
+
self._refresh_token = data.get("refreshToken")
|
|
78
|
+
if not self._access_token:
|
|
79
|
+
raise RuntimeError(f"No token in auth response: {list(data.keys())}")
|
|
80
|
+
self._token_expiry = time.time() + data.get("expiresIn", 300)
|
|
81
|
+
self._refresh_expiry = time.time() + data.get("refreshExpiresIn", 900)
|
|
82
|
+
self._session.headers["Authorization"] = f"Bearer {self._access_token}"
|
|
83
|
+
logger.debug("PPC authentication successful (body-based)")
|
|
84
|
+
|
|
85
|
+
def _ensure_auth(self):
|
|
86
|
+
"""Refresh token if expired."""
|
|
87
|
+
now = time.time()
|
|
88
|
+
if self._access_token is None:
|
|
89
|
+
self.authenticate()
|
|
90
|
+
elif now >= (self._token_expiry - 30):
|
|
91
|
+
if self._refresh_token and now < (self._refresh_expiry - 10):
|
|
92
|
+
self._refresh()
|
|
93
|
+
else:
|
|
94
|
+
self.authenticate()
|
|
95
|
+
|
|
96
|
+
def _refresh(self):
|
|
97
|
+
"""Refresh the access token."""
|
|
98
|
+
url = f"{self._base_url}/v1/auth/login/token/refresh"
|
|
99
|
+
resp = self._session.post(url, json={"refreshToken": self._refresh_token})
|
|
100
|
+
if resp.status_code != 200:
|
|
101
|
+
logger.debug("Token refresh failed, re-authenticating")
|
|
102
|
+
self.authenticate()
|
|
103
|
+
return
|
|
104
|
+
data = resp.json().get("data", resp.json())
|
|
105
|
+
self._access_token = data.get("accessToken") or data.get("token")
|
|
106
|
+
self._refresh_token = data.get("refreshToken", self._refresh_token)
|
|
107
|
+
self._token_expiry = time.time() + data.get("expiresIn", 300)
|
|
108
|
+
self._session.headers["Authorization"] = f"Bearer {self._access_token}"
|
|
109
|
+
|
|
110
|
+
# ──────────────────────────────────────────────────────────
|
|
111
|
+
# Generic HTTP helpers
|
|
112
|
+
# ──────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
def _post(self, path, payload):
|
|
115
|
+
"""POST to PPC API with auto-refresh."""
|
|
116
|
+
self._ensure_auth()
|
|
117
|
+
url = f"{self._base_url}{path}"
|
|
118
|
+
return self._session.post(url, json=payload)
|
|
119
|
+
|
|
120
|
+
def _get(self, path):
|
|
121
|
+
"""GET from PPC API with auto-refresh."""
|
|
122
|
+
self._ensure_auth()
|
|
123
|
+
url = f"{self._base_url}{path}"
|
|
124
|
+
return self._session.get(url)
|
|
125
|
+
|
|
126
|
+
def post_resource(self, endpoint, payload):
|
|
127
|
+
"""POST a resource payload. Returns (created: bool, response).
|
|
128
|
+
|
|
129
|
+
Handles 409 as 'already exists'. Returns (False, resp) on 400/other
|
|
130
|
+
client errors without raising. Non-409 4xx failures are reported via
|
|
131
|
+
`_warn_failed_create` so every caller gets visibility for free.
|
|
132
|
+
|
|
133
|
+
Some PPC endpoints (notably role-member links) return 400 with a body
|
|
134
|
+
like 'must be unique' / 'already exists' instead of 409 for duplicate
|
|
135
|
+
creates. We treat those as the same idempotent skip as 409.
|
|
136
|
+
"""
|
|
137
|
+
resp = self._post(f"/v2/pim/{endpoint}", payload)
|
|
138
|
+
if resp.status_code in (200, 201, 204):
|
|
139
|
+
return True, resp
|
|
140
|
+
if resp.status_code == 409:
|
|
141
|
+
return False, resp
|
|
142
|
+
if resp.status_code >= 500:
|
|
143
|
+
resp.raise_for_status()
|
|
144
|
+
if resp.status_code == 400 and self._is_already_exists(resp):
|
|
145
|
+
return False, resp
|
|
146
|
+
self._warn_failed_create(endpoint, payload, resp)
|
|
147
|
+
return False, resp
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _is_already_exists(resp):
|
|
151
|
+
"""Return True if a 400 response really means 'already exists'."""
|
|
152
|
+
try:
|
|
153
|
+
body = resp.text.lower()
|
|
154
|
+
except Exception:
|
|
155
|
+
return False
|
|
156
|
+
return (
|
|
157
|
+
"already exists" in body
|
|
158
|
+
or "must be unique" in body
|
|
159
|
+
or "duplicate" in body
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _warn_failed_create(endpoint, payload, resp):
|
|
164
|
+
"""Surface non-409 4xx failures from `post_resource` to the CLI."""
|
|
165
|
+
kind = endpoint.rstrip("/").split("/")[-1]
|
|
166
|
+
name = ""
|
|
167
|
+
if isinstance(payload, dict):
|
|
168
|
+
name = (
|
|
169
|
+
payload.get("name")
|
|
170
|
+
or payload.get("label")
|
|
171
|
+
or payload.get("username")
|
|
172
|
+
or ""
|
|
173
|
+
)
|
|
174
|
+
elif isinstance(payload, list) and payload and isinstance(payload[0], dict):
|
|
175
|
+
name = payload[0].get("name") or payload[0].get("username") or ""
|
|
176
|
+
try:
|
|
177
|
+
body = resp.text[:500]
|
|
178
|
+
except Exception:
|
|
179
|
+
body = ""
|
|
180
|
+
print(f" \u2717 Failed to create {kind} '{name}': HTTP {resp.status_code} {body}")
|
|
181
|
+
|
|
182
|
+
# ──────────────────────────────────────────────────────────
|
|
183
|
+
# PIM initialization
|
|
184
|
+
# ──────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def is_pim_initialized(self):
|
|
187
|
+
"""Check if PIM is initialized by querying datastores."""
|
|
188
|
+
try:
|
|
189
|
+
resp = self._get("/v2/pim/datastores")
|
|
190
|
+
return resp.status_code == 200
|
|
191
|
+
except Exception:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
def init_pim(self):
|
|
195
|
+
"""Initialize PIM (first-time setup). Idempotent."""
|
|
196
|
+
if self.is_pim_initialized():
|
|
197
|
+
logger.debug("PIM already initialized")
|
|
198
|
+
return True
|
|
199
|
+
resp = self._post("/v2/pim/init", {})
|
|
200
|
+
if resp.status_code in (200, 201, 204, 409):
|
|
201
|
+
return True
|
|
202
|
+
resp.raise_for_status()
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
# ──────────────────────────────────────────────────────────
|
|
206
|
+
# List resources (for delta computation)
|
|
207
|
+
# ──────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
def _parse_list_response(self, resp):
|
|
210
|
+
"""Parse a list API response into a list of items."""
|
|
211
|
+
if resp.status_code != 200:
|
|
212
|
+
return []
|
|
213
|
+
data = resp.json()
|
|
214
|
+
if isinstance(data, list):
|
|
215
|
+
return data
|
|
216
|
+
if isinstance(data, dict):
|
|
217
|
+
return data.get("data", data.get("items", []))
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
def list_datastores(self):
|
|
221
|
+
"""Get existing datastores."""
|
|
222
|
+
return self._parse_list_response(self._get("/v2/pim/datastores"))
|
|
223
|
+
|
|
224
|
+
def list_sources(self):
|
|
225
|
+
"""Get existing sources."""
|
|
226
|
+
return self._parse_list_response(self._get("/v2/pim/sources"))
|
|
227
|
+
|
|
228
|
+
def list_roles(self):
|
|
229
|
+
"""Get existing roles."""
|
|
230
|
+
return self._parse_list_response(self._get("/v2/pim/roles"))
|
|
231
|
+
|
|
232
|
+
def list_alphabets(self):
|
|
233
|
+
"""Get existing alphabets."""
|
|
234
|
+
return self._parse_list_response(self._get("/v2/pim/alphabets"))
|
|
235
|
+
|
|
236
|
+
def list_masks(self):
|
|
237
|
+
"""Get existing masks."""
|
|
238
|
+
return self._parse_list_response(self._get("/v2/pim/masks"))
|
|
239
|
+
|
|
240
|
+
def list_data_elements(self):
|
|
241
|
+
"""Get existing data elements."""
|
|
242
|
+
return self._parse_list_response(self._get("/v2/pim/dataelements"))
|
|
243
|
+
|
|
244
|
+
def list_applications(self):
|
|
245
|
+
"""Get existing trusted applications."""
|
|
246
|
+
return self._parse_list_response(self._get("/v2/pim/applications"))
|
|
247
|
+
|
|
248
|
+
def list_policies(self):
|
|
249
|
+
"""Get existing policies."""
|
|
250
|
+
return self._parse_list_response(self._get("/v2/pim/policies"))
|
|
251
|
+
|
|
252
|
+
def list_rules(self, policy_id="1"):
|
|
253
|
+
"""Get existing rules for a policy."""
|
|
254
|
+
return self._parse_list_response(self._get(f"/v2/pim/policies/{policy_id}/rules"))
|
|
255
|
+
|
|
256
|
+
def list_role_members(self, role_id):
|
|
257
|
+
"""Get members of a role."""
|
|
258
|
+
return self._parse_list_response(self._get(f"/v2/pim/roles/{role_id}/members"))
|
|
259
|
+
|
|
260
|
+
def list_export_keys(self, datastore_id):
|
|
261
|
+
"""Get export keys registered for a datastore."""
|
|
262
|
+
return self._parse_list_response(
|
|
263
|
+
self._get(f"/v2/pim/datastores/{datastore_id}/export/keys")
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# ──────────────────────────────────────────────────────────
|
|
267
|
+
# Create resources
|
|
268
|
+
# ──────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
def create_datastore(self, payload):
|
|
271
|
+
"""Create a datastore."""
|
|
272
|
+
return self.post_resource("datastores", payload)
|
|
273
|
+
|
|
274
|
+
def create_source(self, payload):
|
|
275
|
+
"""Create a source."""
|
|
276
|
+
return self.post_resource("sources", payload)
|
|
277
|
+
|
|
278
|
+
def create_role(self, payload):
|
|
279
|
+
"""Create a role from full payload."""
|
|
280
|
+
return self.post_resource("roles", payload)
|
|
281
|
+
|
|
282
|
+
def create_alphabet(self, payload):
|
|
283
|
+
"""Create an alphabet."""
|
|
284
|
+
return self.post_resource("alphabets", payload)
|
|
285
|
+
|
|
286
|
+
def create_mask(self, payload):
|
|
287
|
+
"""Create a mask."""
|
|
288
|
+
return self.post_resource("masks", payload)
|
|
289
|
+
|
|
290
|
+
def create_data_element(self, payload):
|
|
291
|
+
"""Create a data element from full payload."""
|
|
292
|
+
return self.post_resource("dataelements", payload)
|
|
293
|
+
|
|
294
|
+
def create_application(self, payload):
|
|
295
|
+
"""Create a trusted application."""
|
|
296
|
+
return self.post_resource("applications", payload)
|
|
297
|
+
|
|
298
|
+
def create_policy(self, payload):
|
|
299
|
+
"""Create a policy."""
|
|
300
|
+
return self.post_resource("policies", payload)
|
|
301
|
+
|
|
302
|
+
def create_rule(self, policy_id, payload):
|
|
303
|
+
"""Create a rule for a policy."""
|
|
304
|
+
return self.post_resource(f"policies/{policy_id}/rules", payload)
|
|
305
|
+
|
|
306
|
+
def add_members(self, role_uid, members_payload):
|
|
307
|
+
"""Add members to a role. members_payload is a list."""
|
|
308
|
+
return self.post_resource(f"roles/{role_uid}/members", members_payload)
|
|
309
|
+
|
|
310
|
+
def deploy(self, datastore_id="1", payload=None):
|
|
311
|
+
"""Deploy policy to a datastore."""
|
|
312
|
+
if payload is None:
|
|
313
|
+
payload = {"policies": ["1"], "applications": ["1"]}
|
|
314
|
+
return self.post_resource(f"datastores/{datastore_id}/deploy", payload)
|
|
315
|
+
|
|
316
|
+
# ──────────────────────────────────────────────────────────
|
|
317
|
+
# User management (auth v1 API)
|
|
318
|
+
# ──────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
def list_users(self):
|
|
321
|
+
"""List all users via auth API."""
|
|
322
|
+
resp = self._get("/v1/auth/users")
|
|
323
|
+
if resp.status_code != 200:
|
|
324
|
+
return []
|
|
325
|
+
data = resp.json()
|
|
326
|
+
if isinstance(data, list):
|
|
327
|
+
return data
|
|
328
|
+
if isinstance(data, dict):
|
|
329
|
+
return data.get("data", data.get("items", []))
|
|
330
|
+
return []
|
|
331
|
+
|
|
332
|
+
def user_exists(self, username):
|
|
333
|
+
"""Check if a user exists."""
|
|
334
|
+
users = self.list_users()
|
|
335
|
+
return any(u.get("username") == username for u in users)
|
|
336
|
+
|
|
337
|
+
def create_user(self, username, password, roles=None):
|
|
338
|
+
"""Create a user via auth API. Returns (created: bool, response)."""
|
|
339
|
+
if roles is None:
|
|
340
|
+
roles = []
|
|
341
|
+
payload = {
|
|
342
|
+
"username": username,
|
|
343
|
+
"password": password,
|
|
344
|
+
"roles": roles,
|
|
345
|
+
}
|
|
346
|
+
self._ensure_auth()
|
|
347
|
+
url = f"{self._base_url}/v1/auth/users"
|
|
348
|
+
resp = self._session.post(url, json=payload)
|
|
349
|
+
if resp.status_code in (200, 201):
|
|
350
|
+
return True, resp
|
|
351
|
+
if resp.status_code == 409:
|
|
352
|
+
return False, resp
|
|
353
|
+
if resp.status_code == 400 and "exist" in resp.text.lower():
|
|
354
|
+
return False, resp
|
|
355
|
+
return False, resp
|
|
356
|
+
|
|
357
|
+
def ensure_role_permissions(self, role_name, permissions):
|
|
358
|
+
"""Update a role with required permissions via PUT /v1/auth/roles."""
|
|
359
|
+
self._ensure_auth()
|
|
360
|
+
payload = {"name": role_name, "permissions": permissions}
|
|
361
|
+
url = f"{self._base_url}/v1/auth/roles"
|
|
362
|
+
resp = self._session.put(url, json=payload)
|
|
363
|
+
return resp.status_code in (200, 201)
|
|
364
|
+
|
|
365
|
+
def re_authenticate(self, user, password):
|
|
366
|
+
"""Switch identity by authenticating as a different user."""
|
|
367
|
+
self._user = user
|
|
368
|
+
self._password = password
|
|
369
|
+
self._access_token = None
|
|
370
|
+
self._refresh_token = None
|
|
371
|
+
self.authenticate()
|
pty_migrate/stats_cmd.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""pty-migrate stats command — display usage statistics summary."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _load_stats(stats_file=None):
|
|
10
|
+
"""Load usage stats from file. CLI flag > env > config file > default."""
|
|
11
|
+
from pty_migrate.config import resolve
|
|
12
|
+
resolved = resolve(stats_file, "PTY_STATS_FILE", "stats_file",
|
|
13
|
+
str(Path.home() / ".protegrity" / "usage_stats.json"))
|
|
14
|
+
path = Path(resolved)
|
|
15
|
+
|
|
16
|
+
if not path.is_file():
|
|
17
|
+
return None, path
|
|
18
|
+
with open(path, "r") as f:
|
|
19
|
+
return json.load(f), path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_stats(args):
|
|
23
|
+
"""Execute the stats command."""
|
|
24
|
+
data, path = _load_stats(args.stats_file)
|
|
25
|
+
|
|
26
|
+
if data is None:
|
|
27
|
+
print(f"No usage statistics found at: {path}")
|
|
28
|
+
print("Stats are collected automatically when using the SDK with Developer Edition.")
|
|
29
|
+
print("Ensure DEV_EDITION_* environment variables are set and perform some operations.")
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
if args.json:
|
|
33
|
+
print(json.dumps(data, indent=2))
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
# Formatted output
|
|
37
|
+
collected_since = data.get("collected_since", "unknown")
|
|
38
|
+
last_updated = data.get("last_updated", "unknown")
|
|
39
|
+
|
|
40
|
+
print(f"Usage Statistics (collected since {collected_since[:10]})")
|
|
41
|
+
print("─" * 55)
|
|
42
|
+
print(f"Last updated: {last_updated[:10]}")
|
|
43
|
+
print()
|
|
44
|
+
|
|
45
|
+
# Data elements
|
|
46
|
+
data_elements = data.get("data_elements", {})
|
|
47
|
+
if data_elements:
|
|
48
|
+
print(f"Data Elements Used ({len(data_elements)}):")
|
|
49
|
+
# Sort by protect_count descending
|
|
50
|
+
sorted_des = sorted(
|
|
51
|
+
data_elements.items(),
|
|
52
|
+
key=lambda x: x[1].get("protect_count", 0),
|
|
53
|
+
reverse=True,
|
|
54
|
+
)
|
|
55
|
+
for de_name, de_stats in sorted_des:
|
|
56
|
+
protect = de_stats.get("protect_count", 0)
|
|
57
|
+
unprotect = de_stats.get("unprotect_count", 0)
|
|
58
|
+
reprotect_src = de_stats.get("reprotect_source_count", 0)
|
|
59
|
+
reprotect_tgt = de_stats.get("reprotect_target_count", 0)
|
|
60
|
+
last_used = de_stats.get("last_used", "—")
|
|
61
|
+
line = f" {de_name:<16} protect: {protect:<6} unprotect: {unprotect:<6}"
|
|
62
|
+
if reprotect_src or reprotect_tgt:
|
|
63
|
+
line += f" reprotect: {reprotect_src + reprotect_tgt:<4}"
|
|
64
|
+
line += f" last: {last_used}"
|
|
65
|
+
print(line)
|
|
66
|
+
else:
|
|
67
|
+
print("No data elements recorded yet.")
|
|
68
|
+
|
|
69
|
+
print()
|
|
70
|
+
|
|
71
|
+
# Policy users
|
|
72
|
+
policy_users = data.get("policy_users", {})
|
|
73
|
+
if policy_users:
|
|
74
|
+
print(f"Policy Users ({len(policy_users)}):")
|
|
75
|
+
sorted_users = sorted(
|
|
76
|
+
policy_users.items(),
|
|
77
|
+
key=lambda x: x[1].get("session_count", 0),
|
|
78
|
+
reverse=True,
|
|
79
|
+
)
|
|
80
|
+
for user, user_stats in sorted_users:
|
|
81
|
+
sessions = user_stats.get("session_count", 0)
|
|
82
|
+
last_used = user_stats.get("last_used", "—")
|
|
83
|
+
print(f" {user:<16} sessions: {sessions:<6} last: {last_used}")
|
|
84
|
+
else:
|
|
85
|
+
print("No policy users recorded yet.")
|
|
86
|
+
|
|
87
|
+
return 0
|