trustarc-cli 1.0.0__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.
- ta/__init__.py +1 -0
- ta/commands/__init__.py +0 -0
- ta/commands/auth/__init__.py +1 -0
- ta/commands/auth/command.py +91 -0
- ta/commands/config/__init__.py +1 -0
- ta/commands/config/command.py +57 -0
- ta/commands/self_update/__init__.py +2 -0
- ta/commands/self_update/command.py +36 -0
- ta/commands/services/__init__.py +1 -0
- ta/commands/services/command.py +37 -0
- ta/lib/__init__.py +0 -0
- ta/lib/analytics.py +63 -0
- ta/lib/auth.py +212 -0
- ta/lib/auth_servers.py +56 -0
- ta/lib/catalog_cache.py +72 -0
- ta/lib/config.py +65 -0
- ta/lib/dynamic.py +259 -0
- ta/lib/services.py +13 -0
- ta/lib/spec_cache.py +25 -0
- ta/lib/update_check.py +66 -0
- ta/lib/urls.py +18 -0
- ta/main.py +69 -0
- trustarc_cli-1.0.0.dist-info/METADATA +114 -0
- trustarc_cli-1.0.0.dist-info/RECORD +31 -0
- trustarc_cli-1.0.0.dist-info/WHEEL +4 -0
- trustarc_cli-1.0.0.dist-info/entry_points.txt +2 -0
- trustarc_sdk/openapi/__init__.py +39 -0
- trustarc_sdk/openapi/call.py +204 -0
- trustarc_sdk/openapi/feature_manager.py +106 -0
- trustarc_sdk/openapi/parse.py +207 -0
- trustarc_sdk/openapi/schema.py +92 -0
ta/lib/config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ta.lib.catalog_cache import load_catalog
|
|
5
|
+
from ta.lib.services import ENVS, services_for
|
|
6
|
+
|
|
7
|
+
CONFIG_DIR = Path.home() / ".ta"
|
|
8
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
9
|
+
|
|
10
|
+
DEFAULT_CONFIG = {
|
|
11
|
+
"env": "prod-us",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _deep_merge(base: dict, override: dict) -> dict:
|
|
16
|
+
result = base.copy()
|
|
17
|
+
for key, value in override.items():
|
|
18
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
19
|
+
result[key] = _deep_merge(result[key], value)
|
|
20
|
+
else:
|
|
21
|
+
result[key] = value
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UnknownEnvError(ValueError):
|
|
26
|
+
def __init__(self, env: str):
|
|
27
|
+
self.env = env
|
|
28
|
+
super().__init__(
|
|
29
|
+
f"Saved env '{env}' is not supported. Run `ta config setup` to pick one of: {', '.join(ENVS)}."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_config() -> dict:
|
|
34
|
+
if CONFIG_FILE.exists():
|
|
35
|
+
with open(CONFIG_FILE) as f:
|
|
36
|
+
return _deep_merge(DEFAULT_CONFIG, json.load(f))
|
|
37
|
+
return dict(DEFAULT_CONFIG)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def saved_env() -> str:
|
|
41
|
+
env = _read_config()["env"]
|
|
42
|
+
if env not in ENVS:
|
|
43
|
+
raise UnknownEnvError(env)
|
|
44
|
+
return env
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_config(refresh_catalog: bool = False) -> dict:
|
|
48
|
+
cfg = _read_config()
|
|
49
|
+
if cfg["env"] not in ENVS:
|
|
50
|
+
raise UnknownEnvError(cfg["env"])
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
catalog = load_catalog(cfg["env"], force=refresh_catalog)
|
|
54
|
+
except RuntimeError:
|
|
55
|
+
catalog = {}
|
|
56
|
+
cfg["catalog"] = catalog
|
|
57
|
+
cfg["services"] = services_for(catalog)
|
|
58
|
+
return cfg
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def save_config(config: dict) -> Path:
|
|
62
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
63
|
+
with open(CONFIG_FILE, "w") as f:
|
|
64
|
+
json.dump(config, f, indent=2)
|
|
65
|
+
return CONFIG_FILE
|
ta/lib/dynamic.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import keyword
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from trustarc_sdk.openapi import (
|
|
11
|
+
Entrypoint,
|
|
12
|
+
build_schema_snapshot,
|
|
13
|
+
call,
|
|
14
|
+
fetch_spec,
|
|
15
|
+
group_operations_by_resource,
|
|
16
|
+
resolve_base_url,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from ta import __version__
|
|
20
|
+
from ta.lib.analytics import report_tool_call
|
|
21
|
+
from ta.lib.auth import get_access_token
|
|
22
|
+
from ta.lib.auth_servers import FEATURE_MANAGER_API_URLS
|
|
23
|
+
from ta.lib.config import UnknownEnvError, load_config
|
|
24
|
+
from ta.lib.spec_cache import SPEC_CACHE_TTL_SECONDS, load_spec, save_spec, spec_cache_path
|
|
25
|
+
from ta.lib.urls import to_public_url
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
_USER_AGENT = " ".join(
|
|
30
|
+
(
|
|
31
|
+
f"python/{sys.version.split()[0]}",
|
|
32
|
+
f"{platform.system()}/{platform.release()}",
|
|
33
|
+
f"trustarc-cli/{__version__}",
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
_SOURCE = "cli"
|
|
37
|
+
_WRITE_EPILOG = "Changes data in TrustArc. Confirm the change before running it."
|
|
38
|
+
_WRITE_EPILOG_WITH_BODY = (
|
|
39
|
+
"Changes data in TrustArc. Check the request body with --schema and confirm the change before running it."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def register_dynamic_commands(app: typer.Typer) -> None:
|
|
44
|
+
try:
|
|
45
|
+
cfg = load_config()
|
|
46
|
+
except UnknownEnvError:
|
|
47
|
+
return
|
|
48
|
+
env = cfg["env"]
|
|
49
|
+
token = get_access_token(env) or ""
|
|
50
|
+
active_service = sys.argv[1] if len(sys.argv) > 1 else None
|
|
51
|
+
|
|
52
|
+
catalog = cfg["catalog"]
|
|
53
|
+
for service, spec_url in sorted(cfg["services"].items()):
|
|
54
|
+
entry = catalog[service]
|
|
55
|
+
prefix = entry["prefix"]
|
|
56
|
+
include_tags = entry["include_tags"]
|
|
57
|
+
description = entry["description"]
|
|
58
|
+
full_name = entry["name"] or service
|
|
59
|
+
service_app = typer.Typer(help=f"{full_name} — {description}", no_args_is_help=True)
|
|
60
|
+
_register_refresh(service_app, service, env, spec_url)
|
|
61
|
+
|
|
62
|
+
if service == active_service and _is_stale(service, env):
|
|
63
|
+
try:
|
|
64
|
+
save_spec(service, env, fetch_spec(spec_url))
|
|
65
|
+
except httpx.HTTPError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
spec = load_spec(service, env)
|
|
69
|
+
if spec:
|
|
70
|
+
base_url = to_public_url(resolve_base_url(spec, spec_url))
|
|
71
|
+
grouped = group_operations_by_resource(
|
|
72
|
+
spec,
|
|
73
|
+
prefix,
|
|
74
|
+
include_tags=include_tags,
|
|
75
|
+
feature_manager_url=FEATURE_MANAGER_API_URLS.get(env),
|
|
76
|
+
access_token=token,
|
|
77
|
+
)
|
|
78
|
+
for resource in sorted(grouped):
|
|
79
|
+
resource_app = typer.Typer(help=f"{resource} operations", no_args_is_help=True)
|
|
80
|
+
for action, op in grouped[resource]:
|
|
81
|
+
_register_operation(resource_app, service, resource, action, op, base_url, spec)
|
|
82
|
+
service_app.add_typer(resource_app, name=resource)
|
|
83
|
+
|
|
84
|
+
app.add_typer(service_app, name=service)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _is_stale(service: str, env: str) -> bool:
|
|
88
|
+
path = spec_cache_path(service, env)
|
|
89
|
+
if not path.exists():
|
|
90
|
+
return True
|
|
91
|
+
return (time.time() - path.stat().st_mtime) > SPEC_CACHE_TTL_SECONDS
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _register_refresh(service_app: typer.Typer, service: str, env: str, spec_url: str) -> None:
|
|
95
|
+
@service_app.command("refresh", help=f"Fetch and cache the {service} OpenAPI spec.")
|
|
96
|
+
def refresh():
|
|
97
|
+
console.print(f"[bold]{service}[/bold] {spec_url}")
|
|
98
|
+
try:
|
|
99
|
+
spec = fetch_spec(spec_url)
|
|
100
|
+
except httpx.HTTPError as e:
|
|
101
|
+
console.print(f" [red]failed:[/red] {e}")
|
|
102
|
+
raise typer.Exit(1) from e
|
|
103
|
+
path = save_spec(service, env, spec)
|
|
104
|
+
grouped = group_operations_by_resource(spec)
|
|
105
|
+
total = sum(len(ops) for ops in grouped.values())
|
|
106
|
+
console.print(f" [green]cached[/green] {path} [dim]({len(grouped)} resources, {total} ops)[/dim]")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _register_operation(
|
|
110
|
+
sub: typer.Typer, service: str, resource: str, action: str, op: dict, base_url: str, spec: dict
|
|
111
|
+
) -> None:
|
|
112
|
+
help_text = op.get("summary") or op.get("description") or f"{op['method']} {op['path']}"
|
|
113
|
+
|
|
114
|
+
path_params = [p for p in op["parameters"] if p.get("in") == "path"]
|
|
115
|
+
query_params = [p for p in op["parameters"] if p.get("in") == "query"]
|
|
116
|
+
has_body = op["request_body"] is not None
|
|
117
|
+
schema_snapshot = build_schema_snapshot(op, spec)
|
|
118
|
+
|
|
119
|
+
seen_py_names: set[str] = set()
|
|
120
|
+
path_py = [(_py_name(p["name"], seen_py_names), p["name"]) for p in path_params]
|
|
121
|
+
query_py = [(_py_name(p["name"], seen_py_names), p["name"]) for p in query_params]
|
|
122
|
+
|
|
123
|
+
def make_runner(
|
|
124
|
+
op=op,
|
|
125
|
+
base_url=base_url,
|
|
126
|
+
path_py=path_py,
|
|
127
|
+
query_py=query_py,
|
|
128
|
+
has_body=has_body,
|
|
129
|
+
):
|
|
130
|
+
def runner(**kwargs):
|
|
131
|
+
kwargs.pop("schema", None)
|
|
132
|
+
cfg = load_config()
|
|
133
|
+
path_values, query_values, body = split_request(path_py, query_py, has_body, kwargs)
|
|
134
|
+
|
|
135
|
+
token = get_access_token(cfg["env"]) or ""
|
|
136
|
+
try:
|
|
137
|
+
resp = call(
|
|
138
|
+
op,
|
|
139
|
+
base_url,
|
|
140
|
+
token,
|
|
141
|
+
None,
|
|
142
|
+
path_params=path_values,
|
|
143
|
+
query_params=query_values,
|
|
144
|
+
body=body,
|
|
145
|
+
user_agent=_USER_AGENT,
|
|
146
|
+
entrypoint=Entrypoint.CLI,
|
|
147
|
+
source=_SOURCE,
|
|
148
|
+
feature_manager_url=FEATURE_MANAGER_API_URLS.get(cfg["env"]),
|
|
149
|
+
)
|
|
150
|
+
except Exception:
|
|
151
|
+
report_tool_call(cfg["env"], token, service, resource, action, op["method"], None, "request_failed")
|
|
152
|
+
raise
|
|
153
|
+
report_tool_call(cfg["env"], token, service, resource, action, op["method"], resp.status_code)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
console.print_json(data=resp.json())
|
|
157
|
+
except ValueError:
|
|
158
|
+
console.print(resp.text)
|
|
159
|
+
if resp.status_code >= 400:
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
return runner
|
|
163
|
+
|
|
164
|
+
runner = make_runner()
|
|
165
|
+
params: list[tuple[str, object, object]] = []
|
|
166
|
+
for (py_name, _), p in zip(path_py, path_params):
|
|
167
|
+
params.append((py_name, typer.Argument(..., help=p.get("description", "")), str))
|
|
168
|
+
for (py_name, api_name), p in zip(query_py, query_params):
|
|
169
|
+
default = ... if p.get("required") else None
|
|
170
|
+
flag = f"--{api_name.replace('_', '-')}"
|
|
171
|
+
params.append((py_name, typer.Option(default, flag, help=p.get("description", "")), str))
|
|
172
|
+
if has_body:
|
|
173
|
+
params.append(
|
|
174
|
+
("body", typer.Option(None, "--body", help="JSON request body. Run with --schema to see its fields."), str)
|
|
175
|
+
)
|
|
176
|
+
params.append(
|
|
177
|
+
(
|
|
178
|
+
"schema",
|
|
179
|
+
typer.Option(
|
|
180
|
+
False,
|
|
181
|
+
"--schema",
|
|
182
|
+
is_eager=True,
|
|
183
|
+
callback=_schema_printer(schema_snapshot),
|
|
184
|
+
help="Print request/response schema and exit",
|
|
185
|
+
),
|
|
186
|
+
bool,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
runner.__name__ = action.replace("-", "_")
|
|
191
|
+
runner.__doc__ = help_text
|
|
192
|
+
runner.__signature__ = _build_signature(params) # type: ignore[attr-defined]
|
|
193
|
+
epilog = None
|
|
194
|
+
if op["method"].upper() != "GET":
|
|
195
|
+
epilog = _WRITE_EPILOG_WITH_BODY if has_body else _WRITE_EPILOG
|
|
196
|
+
sub.command(name=action, help=help_text, epilog=epilog)(runner)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _schema_printer(schema_snapshot: dict):
|
|
200
|
+
def print_schema(value: bool) -> None:
|
|
201
|
+
if value:
|
|
202
|
+
console.print_json(data=schema_snapshot)
|
|
203
|
+
raise typer.Exit()
|
|
204
|
+
|
|
205
|
+
return print_schema
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def split_request(
|
|
209
|
+
path_py: list[tuple[str, str]],
|
|
210
|
+
query_py: list[tuple[str, str]],
|
|
211
|
+
has_body: bool,
|
|
212
|
+
kwargs: dict,
|
|
213
|
+
) -> tuple[dict, dict, dict | list | None]:
|
|
214
|
+
"""Keep path, query and body apart so a body field can never change the request target."""
|
|
215
|
+
path_values = {api_name: kwargs[py_name] for py_name, api_name in path_py}
|
|
216
|
+
query_values = {api_name: kwargs[py_name] for py_name, api_name in query_py if kwargs.get(py_name) is not None}
|
|
217
|
+
body: dict | list | None = None
|
|
218
|
+
if has_body and kwargs.get("body"):
|
|
219
|
+
try:
|
|
220
|
+
body = json.loads(kwargs["body"])
|
|
221
|
+
except ValueError as e:
|
|
222
|
+
raise typer.BadParameter(f"--body must be valid JSON: {e}", param_hint="--body") from e
|
|
223
|
+
if isinstance(body, dict):
|
|
224
|
+
given = {**path_values, **query_values}
|
|
225
|
+
conflicting = sorted(key for key in body if key in given and str(body[key]) != str(given[key]))
|
|
226
|
+
if conflicting:
|
|
227
|
+
raise typer.BadParameter(
|
|
228
|
+
f"--body fields {', '.join(conflicting)} disagree with the argument or option of the same "
|
|
229
|
+
"name; the arguments and options decide the target, so make the body match them or leave "
|
|
230
|
+
"those fields out",
|
|
231
|
+
param_hint="--body",
|
|
232
|
+
)
|
|
233
|
+
return path_values, query_values, body
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _py_name(api_name: str, seen: set[str]) -> str:
|
|
237
|
+
name = api_name.replace("-", "_")
|
|
238
|
+
if not name.isidentifier() or keyword.iskeyword(name):
|
|
239
|
+
name = f"_{name}"
|
|
240
|
+
if not name.isidentifier():
|
|
241
|
+
name = "arg_" + "".join(c if c.isalnum() or c == "_" else "_" for c in name)
|
|
242
|
+
base = name
|
|
243
|
+
i = 2
|
|
244
|
+
while name in seen:
|
|
245
|
+
name = f"{base}_{i}"
|
|
246
|
+
i += 1
|
|
247
|
+
seen.add(name)
|
|
248
|
+
return name
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _build_signature(params: list[tuple[str, object, object]]):
|
|
252
|
+
import inspect
|
|
253
|
+
|
|
254
|
+
return inspect.Signature(
|
|
255
|
+
parameters=[
|
|
256
|
+
inspect.Parameter(name, kind=inspect.Parameter.KEYWORD_ONLY, default=default, annotation=annotation)
|
|
257
|
+
for name, default, annotation in params
|
|
258
|
+
]
|
|
259
|
+
)
|
ta/lib/services.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ENVS = ("dev", "qa", "prod-us", "prod-eu", "prod-india")
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def spec_url(service: str, catalog: dict) -> str:
|
|
5
|
+
entry = catalog[service]
|
|
6
|
+
return f"{entry['base_url'].rstrip('/')}{entry['spec_path']}"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def services_for(catalog: dict) -> dict[str, str]:
|
|
10
|
+
return {service: spec_url(service, catalog) for service in catalog if catalog[service].get("base_url")}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ["ENVS", "services_for", "spec_url"]
|
ta/lib/spec_cache.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
SPEC_DIR = Path.home() / ".ta" / "openapi"
|
|
5
|
+
SPEC_CACHE_TTL_SECONDS = 600
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def spec_cache_path(service: str, env: str) -> Path:
|
|
9
|
+
return SPEC_DIR / env / f"{service}.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def save_spec(service: str, env: str, spec: dict) -> Path:
|
|
13
|
+
path = spec_cache_path(service, env)
|
|
14
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
with open(path, "w") as f:
|
|
16
|
+
json.dump(spec, f)
|
|
17
|
+
return path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_spec(service: str, env: str) -> dict | None:
|
|
21
|
+
path = spec_cache_path(service, env)
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return None
|
|
24
|
+
with open(path) as f:
|
|
25
|
+
return json.load(f)
|
ta/lib/update_check.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from packaging.version import InvalidVersion, Version
|
|
10
|
+
|
|
11
|
+
from ta import __version__
|
|
12
|
+
|
|
13
|
+
PACKAGE_NAME = "trustarc-cli"
|
|
14
|
+
PYPI_URL = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
|
|
15
|
+
CACHE_FILE = Path.home() / ".ta" / "update_check.json"
|
|
16
|
+
CHECK_INTERVAL_SECONDS = 24 * 60 * 60
|
|
17
|
+
_TIMEOUT_S = 2.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fetch_latest_version() -> str | None:
|
|
21
|
+
with contextlib.suppress(httpx.HTTPError, ValueError, KeyError, TypeError):
|
|
22
|
+
resp = httpx.get(PYPI_URL, timeout=_TIMEOUT_S)
|
|
23
|
+
resp.raise_for_status()
|
|
24
|
+
return resp.json()["info"]["version"]
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_cache() -> dict:
|
|
29
|
+
with contextlib.suppress(OSError, ValueError):
|
|
30
|
+
return json.loads(CACHE_FILE.read_text())
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _write_cache(latest: str | None) -> None:
|
|
35
|
+
with contextlib.suppress(OSError):
|
|
36
|
+
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
CACHE_FILE.write_text(json.dumps({"checked_at": time.time(), "latest": latest}))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def latest_version(*, force: bool = False) -> str | None:
|
|
41
|
+
cache = _read_cache()
|
|
42
|
+
if not force and time.time() - cache.get("checked_at", 0) < CHECK_INTERVAL_SECONDS:
|
|
43
|
+
return cache.get("latest")
|
|
44
|
+
latest = fetch_latest_version() or cache.get("latest")
|
|
45
|
+
_write_cache(latest)
|
|
46
|
+
return latest
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_newer(latest: str | None, current: str = __version__) -> bool:
|
|
50
|
+
if not latest:
|
|
51
|
+
return False
|
|
52
|
+
try:
|
|
53
|
+
return Version(latest) > Version(current)
|
|
54
|
+
except InvalidVersion:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def notify_if_outdated() -> None:
|
|
59
|
+
if os.getenv("TA_NO_UPDATE_CHECK"):
|
|
60
|
+
return
|
|
61
|
+
latest = latest_version()
|
|
62
|
+
if is_newer(latest):
|
|
63
|
+
print(
|
|
64
|
+
f"A new version of trustarc-cli is available: {__version__} → {latest}. Run: ta self-update",
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
ta/lib/urls.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
2
|
+
|
|
3
|
+
INTERNAL_SUFFIX = ".prod.internal"
|
|
4
|
+
_API_SUFFIX = "-api"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_public_url(url: str | None) -> str | None:
|
|
8
|
+
if not url or INTERNAL_SUFFIX not in url:
|
|
9
|
+
return url
|
|
10
|
+
|
|
11
|
+
parts = urlsplit(url)
|
|
12
|
+
host = parts.netloc.replace(INTERNAL_SUFFIX, "", 1)
|
|
13
|
+
|
|
14
|
+
first, dot, rest = host.partition(".")
|
|
15
|
+
if first.endswith(_API_SUFFIX):
|
|
16
|
+
host = f"{first[: -len(_API_SUFFIX)]}{dot}{rest}"
|
|
17
|
+
|
|
18
|
+
return urlunsplit(parts._replace(netloc=host))
|
ta/main.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ta import __version__
|
|
4
|
+
from ta.commands import auth as auth_mod
|
|
5
|
+
from ta.commands import config as config_mod
|
|
6
|
+
from ta.commands.self_update import self_update
|
|
7
|
+
from ta.commands.services import services as services_cmd
|
|
8
|
+
from ta.lib.config import UnknownEnvError, saved_env
|
|
9
|
+
from ta.lib.dynamic import register_dynamic_commands
|
|
10
|
+
from ta.lib.update_check import is_newer, latest_version, notify_if_outdated
|
|
11
|
+
|
|
12
|
+
_RECOVERY_COMMANDS = ("config", "self-update")
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="ta",
|
|
16
|
+
help="Command-line interface for the TrustArc platform APIs.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
invoke_without_command=True,
|
|
19
|
+
epilog=(
|
|
20
|
+
"Start with `ta auth login` (and `ta config show` to check the env), then `ta services` and "
|
|
21
|
+
"`ta <service> <resource> --help` to find the command. Responses are JSON on stdout. "
|
|
22
|
+
"Before a create, update, patch or delete, run it with --schema to see the request body "
|
|
23
|
+
"and confirm the change first."
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def version_callback(value: bool):
|
|
29
|
+
if value:
|
|
30
|
+
msg = f"trustarc-cli {__version__}"
|
|
31
|
+
latest = latest_version(force=True)
|
|
32
|
+
if is_newer(latest):
|
|
33
|
+
msg += f" (latest: {latest} — run: ta self-update)"
|
|
34
|
+
elif latest:
|
|
35
|
+
msg += " (up to date)"
|
|
36
|
+
typer.echo(msg)
|
|
37
|
+
raise typer.Exit()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.callback()
|
|
41
|
+
def main(
|
|
42
|
+
ctx: typer.Context,
|
|
43
|
+
version: bool = typer.Option(
|
|
44
|
+
False,
|
|
45
|
+
"--version",
|
|
46
|
+
"-v",
|
|
47
|
+
callback=version_callback,
|
|
48
|
+
is_eager=True,
|
|
49
|
+
help="Show the version and check for updates.",
|
|
50
|
+
),
|
|
51
|
+
):
|
|
52
|
+
if ctx.invoked_subcommand not in _RECOVERY_COMMANDS:
|
|
53
|
+
try:
|
|
54
|
+
saved_env()
|
|
55
|
+
except UnknownEnvError as e:
|
|
56
|
+
typer.echo(str(e), err=True)
|
|
57
|
+
raise typer.Exit(1) from e
|
|
58
|
+
if ctx.invoked_subcommand != "self-update":
|
|
59
|
+
notify_if_outdated()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
app.command("self-update")(self_update)
|
|
63
|
+
app.command("services")(services_cmd)
|
|
64
|
+
app.add_typer(auth_mod.app, name="auth")
|
|
65
|
+
app.add_typer(config_mod.app, name="config")
|
|
66
|
+
register_dynamic_commands(app)
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
app()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: trustarc-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Command-line interface for the TrustArc platform APIs
|
|
5
|
+
Project-URL: Homepage, https://trustarc.com
|
|
6
|
+
Author: TrustArc
|
|
7
|
+
Keywords: cli,compliance,privacy,trustarc
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Requires-Dist: httpx>=0.28.1
|
|
15
|
+
Requires-Dist: inflection>=0.5.1
|
|
16
|
+
Requires-Dist: packaging>=24
|
|
17
|
+
Requires-Dist: rich>=13.0
|
|
18
|
+
Requires-Dist: typer>=0.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# trustarc-cli
|
|
22
|
+
|
|
23
|
+
`ta` is the command-line interface for the TrustArc platform APIs. Commands are built from each product's live API spec, so new endpoints show up without upgrading the CLI.
|
|
24
|
+
|
|
25
|
+
Use it directly from your terminal, in scripts, or as a tool for AI agents such as Claude Code, Codex and Cursor.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
With [uv](https://docs.astral.sh/uv/getting-started/installation/) (installs Python 3.13 for you if needed):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install trustarc-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or with [pipx](https://pipx.pypa.io) (requires Python 3.13+):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pipx install trustarc-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Update
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ta self-update
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`ta` checks for a new release once a day and prints a notice to stderr when one is available. Set `TA_NO_UPDATE_CHECK=1` to turn the check off (e.g. in CI).
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
ta config setup # pick your region (prod-us/prod-eu/prod-india)
|
|
53
|
+
ta auth login # OAuth login via browser (PKCE)
|
|
54
|
+
ta services # list configured services for this env
|
|
55
|
+
ta dmrm business-process list # example call
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Use with AI agents
|
|
59
|
+
|
|
60
|
+
`ta` is built to be driven by an AI agent as well as by a person:
|
|
61
|
+
|
|
62
|
+
- API responses are printed as JSON on stdout, and the update notice goes to stderr, so output can be parsed as-is.
|
|
63
|
+
- `--help` and `--schema` let the agent discover operations and their request/response shapes without calling the API.
|
|
64
|
+
- Log in once with `ta auth login`; the agent reuses the saved token.
|
|
65
|
+
|
|
66
|
+
Then ask your agent something like:
|
|
67
|
+
|
|
68
|
+
> Use the `ta` CLI to list my business processes in TrustArc. Run `ta services` and `--help` first to find the right command.
|
|
69
|
+
|
|
70
|
+
## Command tree
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
ta
|
|
74
|
+
├── auth
|
|
75
|
+
│ ├── login # OAuth login (browser + PKCE)
|
|
76
|
+
│ ├── logout # revoke + clear local tokens
|
|
77
|
+
│ └── status # current token expiry / user
|
|
78
|
+
├── config
|
|
79
|
+
│ ├── show # current env + login state
|
|
80
|
+
│ ├── setup # pick region
|
|
81
|
+
│ └── reset # delete ~/.ta/config.json
|
|
82
|
+
├── services # list all services for the current env
|
|
83
|
+
├── self-update # upgrade to the latest release
|
|
84
|
+
└── <service> # e.g. dmrm, tc, am, cpm, ccm, irm, pc, cst, evidence, org
|
|
85
|
+
├── refresh # re-fetch this service's OpenAPI spec
|
|
86
|
+
└── <resource> # e.g. business-process, trust-center
|
|
87
|
+
└── <action> [args] # e.g. list, get-by-internal-id, upsert
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Inspecting operations
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
ta <service> --help # list resources
|
|
94
|
+
ta <service> <resource> --help # list actions
|
|
95
|
+
ta <service> <resource> <action> --help # show args/options
|
|
96
|
+
ta <service> <resource> <action> --schema # request/response schema (no API call)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Passing arguments
|
|
100
|
+
|
|
101
|
+
- **Path params** → positional: `ta dmrm business-process get-by-internal-id <id>`
|
|
102
|
+
- **Query params** → `--flag`: `ta tc subscription list --since 2026-01-01`
|
|
103
|
+
- **Body** → `--body '<json>'`: `ta dmrm business-process upsert --body '{"internalId":"...","name":"..."}'`
|
|
104
|
+
|
|
105
|
+
### Regions and auth
|
|
106
|
+
|
|
107
|
+
Tokens are stored per env at `~/.ta/auth/<env>.json`. Spec caches at `~/.ta/openapi/<env>/<service>.json` (10-minute TTL; auto-refreshed when stale).
|
|
108
|
+
|
|
109
|
+
Switch region:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ta config setup # change default region
|
|
113
|
+
ta auth login --env prod-eu # log into a specific region without changing default
|
|
114
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
ta/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
2
|
+
ta/main.py,sha256=gv-vu4yH87QNCGal31gr44IC3gUPjizJBk8qPYceTj8,2106
|
|
3
|
+
ta/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
ta/commands/auth/__init__.py,sha256=5MIEqVZ5cGgG8mCOV71lthtO6olpVAZBOaKCcN9U-3U,48
|
|
5
|
+
ta/commands/auth/command.py,sha256=agebaFnR7HoYVBlM1HCtIZlu2ORjGtFIT65uBLcpe-g,3019
|
|
6
|
+
ta/commands/config/__init__.py,sha256=0aWe-DWx7aoYZDLEsWbuL05pRxxfDg1OgrjU3-PCMkQ,50
|
|
7
|
+
ta/commands/config/command.py,sha256=wzzfJkY2TnAfdIfnPujZh6xSdUuT7G-65OKcXoX7UXc,1906
|
|
8
|
+
ta/commands/self_update/__init__.py,sha256=A9mBjfV37eOEmhIjPHELvzFnZvBQD1vwTQj2qgTrPpk,150
|
|
9
|
+
ta/commands/self_update/command.py,sha256=uyhGH-dQ7ZA17hw8op6bmFG1FGmDQlJE6Z3Nckk1Zhw,1105
|
|
10
|
+
ta/commands/services/__init__.py,sha256=EEIrAK25APADsml7ye959peakp2-GibuazzzA580Gr8,62
|
|
11
|
+
ta/commands/services/command.py,sha256=FTWwQXkd6jpfkV-fa7ne6rC1uWUDdTv5ekWbGZ9H2s0,1360
|
|
12
|
+
ta/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
ta/lib/analytics.py,sha256=lQaJZdq_whg1A-DSTSBtS3S53VLmX6Adk42yxtLQrvM,1524
|
|
14
|
+
ta/lib/auth.py,sha256=PIp-uAuyx2Zx6qnhukY4PQtS9AiIflOBD5fNndeiVLU,6794
|
|
15
|
+
ta/lib/auth_servers.py,sha256=DQk-1Cqiot6l4izy3fKKEpNx0oMTxM8wUQcsLhsndyE,1931
|
|
16
|
+
ta/lib/catalog_cache.py,sha256=rhx2CZdOHMcyJluPOZvA8UdKQFnqmhOqKrXszsaWHtU,2151
|
|
17
|
+
ta/lib/config.py,sha256=QQTlRPEeWRpSIsNMWJO3ybFq6R-nZ5Xr6VhpSFfKrZI,1682
|
|
18
|
+
ta/lib/dynamic.py,sha256=wDkClvMGUo05bKProBaE9RMtRv2vyEN1Egjak9lLaIE,9497
|
|
19
|
+
ta/lib/services.py,sha256=uIZOwkJfwGEASfvEpW1szQpsTeXk92knWtOxbjHvCqI,414
|
|
20
|
+
ta/lib/spec_cache.py,sha256=l3xlHb6DIisnpELAwBYru3sof-2-NEtIKOb5QCmxU-w,635
|
|
21
|
+
ta/lib/update_check.py,sha256=W9wQJxMMtqyjyTNgSuQ-gkE8SPwl6SZomFwNI5QVEKU,1873
|
|
22
|
+
ta/lib/urls.py,sha256=8qrQEh06e8460c2BLfzx1aC_DvIQ4Fto85DO2yv-C6Q,493
|
|
23
|
+
trustarc_sdk/openapi/__init__.py,sha256=qiMYPXPtP5pB0nGEyzMI1GmL6sh1XQ6YK6HegmOwoTY,981
|
|
24
|
+
trustarc_sdk/openapi/call.py,sha256=RdUWtLqfvgcDZLhhT-Fqgx5MhsxtLzwAfYak6Pfr_NM,6460
|
|
25
|
+
trustarc_sdk/openapi/feature_manager.py,sha256=2-uurrK0ZBNzwA1sjyGJjHVaPstGZbU0wyrWSiHbknY,3327
|
|
26
|
+
trustarc_sdk/openapi/parse.py,sha256=JqmTKL-86SIZLWElQODOaENQvhEekCxZh59FCa8esFw,5933
|
|
27
|
+
trustarc_sdk/openapi/schema.py,sha256=2s95Y_dX9-z7xPNcxEx2a7pd9qgqMjIlh-lhesjuIHU,3440
|
|
28
|
+
trustarc_cli-1.0.0.dist-info/METADATA,sha256=NSeZl5zNsbJUUMkBoj8N6qE15S3ZcopuRpJuq_o5pyA,4167
|
|
29
|
+
trustarc_cli-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
30
|
+
trustarc_cli-1.0.0.dist-info/entry_points.txt,sha256=Cs_ki__IkcU8u-22kgTRG5ujLtDIOXLwNDJeC0Idwk4,35
|
|
31
|
+
trustarc_cli-1.0.0.dist-info/RECORD,,
|