applied-cli 0.1.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.
@@ -0,0 +1,2 @@
1
+ """applied-cli package."""
2
+
@@ -0,0 +1,263 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from applied_cli.config import Credentials
6
+
7
+ SERVICE_NAME = "applied-cli"
8
+ ACCOUNT_NAME = "default"
9
+ STORE_VERSION = 2
10
+
11
+
12
+ def _fallback_path() -> Path:
13
+ return Path.home() / ".config" / "applied-cli" / "credentials.json"
14
+
15
+
16
+ def _credentials_to_dict(credentials: Credentials) -> dict[str, str]:
17
+ return {
18
+ "base_url": credentials.base_url,
19
+ "shop_id": credentials.shop_id,
20
+ "api_token": credentials.api_token,
21
+ }
22
+
23
+
24
+ def _credentials_from_dict(data: dict[str, object]) -> Optional[Credentials]:
25
+ base_url = str(data.get("base_url", "")).strip()
26
+ shop_id = str(data.get("shop_id", "")).strip()
27
+ api_token = str(data.get("api_token", "")).strip()
28
+ if not base_url or not shop_id or not api_token:
29
+ return None
30
+ return Credentials(base_url=base_url, shop_id=shop_id, api_token=api_token)
31
+
32
+
33
+ def _normalize_store(raw: object) -> dict[str, object]:
34
+ if not isinstance(raw, dict):
35
+ return {"version": STORE_VERSION, "active_profile": None, "profiles": {}}
36
+
37
+ # Legacy single-profile shape:
38
+ # {"base_url": "...", "shop_id": "...", "api_token": "..."}
39
+ legacy = _credentials_from_dict(raw)
40
+ if legacy:
41
+ return {
42
+ "version": STORE_VERSION,
43
+ "active_profile": "default",
44
+ "profiles": {"default": _credentials_to_dict(legacy)},
45
+ }
46
+
47
+ profiles_raw = raw.get("profiles")
48
+ profiles: dict[str, dict[str, str]] = {}
49
+ if isinstance(profiles_raw, dict):
50
+ for profile_name, payload in profiles_raw.items():
51
+ if not isinstance(profile_name, str) or not profile_name.strip():
52
+ continue
53
+ if not isinstance(payload, dict):
54
+ continue
55
+ creds = _credentials_from_dict(payload)
56
+ if creds is None:
57
+ continue
58
+ profiles[profile_name.strip()] = _credentials_to_dict(creds)
59
+
60
+ active_profile_raw = raw.get("active_profile")
61
+ active_profile = (
62
+ active_profile_raw.strip()
63
+ if isinstance(active_profile_raw, str) and active_profile_raw.strip()
64
+ else None
65
+ )
66
+ if active_profile and active_profile not in profiles:
67
+ active_profile = None
68
+ if not active_profile and profiles:
69
+ active_profile = sorted(profiles.keys())[0]
70
+
71
+ return {
72
+ "version": STORE_VERSION,
73
+ "active_profile": active_profile,
74
+ "profiles": profiles,
75
+ }
76
+
77
+
78
+ def _serialize_store(store: dict[str, object]) -> str:
79
+ return json.dumps(
80
+ {
81
+ "version": STORE_VERSION,
82
+ "active_profile": store.get("active_profile"),
83
+ "profiles": store.get("profiles", {}),
84
+ }
85
+ )
86
+
87
+
88
+ def _load_store_from_keyring() -> Optional[dict[str, object]]:
89
+ try:
90
+ import keyring
91
+ except Exception:
92
+ return None
93
+
94
+ try:
95
+ raw = keyring.get_password(SERVICE_NAME, ACCOUNT_NAME)
96
+ except Exception:
97
+ return None
98
+
99
+ if not raw:
100
+ return None
101
+
102
+ try:
103
+ return _normalize_store(json.loads(raw))
104
+ except Exception:
105
+ return None
106
+
107
+
108
+ def _save_store_to_keyring(store: dict[str, object]) -> bool:
109
+ try:
110
+ import keyring
111
+ except Exception:
112
+ return False
113
+
114
+ try:
115
+ keyring.set_password(
116
+ SERVICE_NAME,
117
+ ACCOUNT_NAME,
118
+ _serialize_store(store),
119
+ )
120
+ return True
121
+ except Exception:
122
+ return False
123
+
124
+
125
+ def _clear_keyring() -> None:
126
+ try:
127
+ import keyring
128
+
129
+ keyring.delete_password(SERVICE_NAME, ACCOUNT_NAME)
130
+ except Exception:
131
+ return
132
+
133
+
134
+ def _load_store_from_file() -> Optional[dict[str, object]]:
135
+ path = _fallback_path()
136
+ if not path.exists():
137
+ return None
138
+
139
+ try:
140
+ return _normalize_store(json.loads(path.read_text()))
141
+ except Exception:
142
+ return None
143
+
144
+
145
+ def _save_store_to_file(store: dict[str, object]) -> None:
146
+ path = _fallback_path()
147
+ path.parent.mkdir(parents=True, exist_ok=True)
148
+ path.write_text(_serialize_store(store))
149
+ path.chmod(0o600)
150
+
151
+
152
+ def _clear_file() -> None:
153
+ path = _fallback_path()
154
+ if path.exists():
155
+ path.unlink()
156
+
157
+
158
+ def _load_store() -> dict[str, object]:
159
+ store = _load_store_from_keyring()
160
+ if store:
161
+ return store
162
+ store = _load_store_from_file()
163
+ if store:
164
+ return store
165
+ return {"version": STORE_VERSION, "active_profile": None, "profiles": {}}
166
+
167
+
168
+ def _save_store(store: dict[str, object]) -> str:
169
+ if _save_store_to_keyring(store):
170
+ return "keyring"
171
+
172
+ _save_store_to_file(store)
173
+ return "file"
174
+
175
+
176
+ def save_credentials(
177
+ credentials: Credentials,
178
+ *,
179
+ profile: str = "default",
180
+ set_active: bool = True,
181
+ ) -> str:
182
+ profile_name = profile.strip() or "default"
183
+ store = _load_store()
184
+ profiles = store.get("profiles")
185
+ if not isinstance(profiles, dict):
186
+ profiles = {}
187
+ profiles[profile_name] = _credentials_to_dict(credentials)
188
+ store["profiles"] = profiles
189
+ if set_active:
190
+ store["active_profile"] = profile_name
191
+ return _save_store(store)
192
+
193
+
194
+ def load_profiles() -> dict[str, Credentials]:
195
+ store = _load_store()
196
+ profiles = store.get("profiles")
197
+ if not isinstance(profiles, dict):
198
+ return {}
199
+ out: dict[str, Credentials] = {}
200
+ for profile_name, payload in profiles.items():
201
+ if not isinstance(profile_name, str) or not isinstance(payload, dict):
202
+ continue
203
+ creds = _credentials_from_dict(payload)
204
+ if creds is not None:
205
+ out[profile_name] = creds
206
+ return out
207
+
208
+
209
+ def list_profiles() -> list[str]:
210
+ return sorted(load_profiles().keys())
211
+
212
+
213
+ def get_active_profile_name() -> Optional[str]:
214
+ store = _load_store()
215
+ active = store.get("active_profile")
216
+ return active if isinstance(active, str) and active.strip() else None
217
+
218
+
219
+ def set_active_profile(profile: str) -> bool:
220
+ profile_name = profile.strip()
221
+ if not profile_name:
222
+ return False
223
+ store = _load_store()
224
+ profiles = store.get("profiles")
225
+ if not isinstance(profiles, dict) or profile_name not in profiles:
226
+ return False
227
+ store["active_profile"] = profile_name
228
+ _save_store(store)
229
+ return True
230
+
231
+
232
+ def delete_profile(profile: str) -> bool:
233
+ profile_name = profile.strip()
234
+ if not profile_name:
235
+ return False
236
+ store = _load_store()
237
+ profiles = store.get("profiles")
238
+ if not isinstance(profiles, dict) or profile_name not in profiles:
239
+ return False
240
+ del profiles[profile_name]
241
+ store["profiles"] = profiles
242
+ active = store.get("active_profile")
243
+ if active == profile_name:
244
+ store["active_profile"] = sorted(profiles.keys())[0] if profiles else None
245
+ _save_store(store)
246
+ return True
247
+
248
+
249
+ def load_credentials(*, profile: Optional[str] = None) -> Optional[Credentials]:
250
+ profiles = load_profiles()
251
+ if not profiles:
252
+ return None
253
+ if profile and profile in profiles:
254
+ return profiles[profile]
255
+ active = get_active_profile_name()
256
+ if active and active in profiles:
257
+ return profiles[active]
258
+ return profiles.get("default")
259
+
260
+
261
+ def clear_credentials() -> None:
262
+ _clear_keyring()
263
+ _clear_file()
@@ -0,0 +1,2 @@
1
+ """CLI command groups."""
2
+
@@ -0,0 +1,11 @@
1
+ from difflib import get_close_matches
2
+ from typing import Iterable
3
+
4
+
5
+ def suggest_value(raw: str, options: Iterable[str]) -> str | None:
6
+ value = raw.strip().lower()
7
+ normalized = [item.strip().lower() for item in options if item.strip()]
8
+ if not value or not normalized:
9
+ return None
10
+ matches = get_close_matches(value, normalized, n=1, cutoff=0.6)
11
+ return matches[0] if matches else None
@@ -0,0 +1,79 @@
1
+ import re
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from applied_cli.commands._hints import suggest_value
7
+
8
+ MODALITY_ALIASES = {
9
+ "all": "All",
10
+ "call": "Call",
11
+ "sms": "SMS",
12
+ "email": "Email",
13
+ "chat": "Chat",
14
+ "internal": "Internal",
15
+ }
16
+
17
+ AGENT_TYPE_ALIASES = {
18
+ "generic": "Generic",
19
+ "customer_support": "Customer Support",
20
+ "customer support": "Customer Support",
21
+ "conversion": "Conversion",
22
+ "sales": "Sales",
23
+ "marketing": "Marketing",
24
+ "engineering": "Engineering",
25
+ "product": "Product",
26
+ }
27
+
28
+ RESPONSE_TYPE_ALIASES = {
29
+ "escalation": "escalate",
30
+ "escalate": "escalate",
31
+ "exact": "exact",
32
+ "qa": "qa",
33
+ "context": "context",
34
+ "greeting": "greeting",
35
+ "signature": "signature",
36
+ }
37
+
38
+
39
+ def normalize_question(text: str) -> str:
40
+ return re.sub(r"\s+", " ", text.strip().lower())
41
+
42
+
43
+ def normalize_response_type(
44
+ raw: str,
45
+ *,
46
+ field_label: str = "type",
47
+ include_suggestion: bool = False,
48
+ ) -> str:
49
+ key = raw.strip().lower()
50
+ if key not in RESPONSE_TYPE_ALIASES:
51
+ suffix = ""
52
+ if include_suggestion:
53
+ suggestion = suggest_value(key, RESPONSE_TYPE_ALIASES.keys())
54
+ if suggestion:
55
+ suffix = f" Did you mean '{suggestion}'?"
56
+ raise typer.BadParameter(
57
+ f"{field_label} must be one of: escalation, exact, qa, context, greeting, signature.{suffix}"
58
+ )
59
+ return RESPONSE_TYPE_ALIASES[key]
60
+
61
+
62
+ def normalize_modality(raw: Optional[str]) -> Optional[str]:
63
+ if raw is None:
64
+ return None
65
+ key = raw.strip().lower()
66
+ if key not in MODALITY_ALIASES:
67
+ raise typer.BadParameter("modality must be one of: all, call, sms, email, chat, internal")
68
+ return MODALITY_ALIASES[key]
69
+
70
+
71
+ def normalize_agent_type(raw: Optional[str]) -> Optional[str]:
72
+ if raw is None:
73
+ return None
74
+ key = raw.strip().lower()
75
+ if key not in AGENT_TYPE_ALIASES:
76
+ raise typer.BadParameter(
77
+ "type must be one of: generic, customer_support, conversion, sales, marketing, engineering, product"
78
+ )
79
+ return AGENT_TYPE_ALIASES[key]
@@ -0,0 +1,58 @@
1
+ import json
2
+ import uuid
3
+ from typing import Any, Optional
4
+
5
+ import typer
6
+
7
+
8
+ def validate_uuid(value: str, *, field_name: str) -> None:
9
+ try:
10
+ uuid.UUID(value)
11
+ except ValueError as exc:
12
+ raise typer.BadParameter(f"{field_name} must be a valid UUID.") from exc
13
+
14
+
15
+ def parse_optional_bool(raw: Optional[str], *, field_name: str) -> Optional[bool]:
16
+ if raw is None:
17
+ return None
18
+ normalized = raw.strip().lower()
19
+ if normalized in {"1", "true", "t", "yes", "y"}:
20
+ return True
21
+ if normalized in {"0", "false", "f", "no", "n"}:
22
+ return False
23
+ raise typer.BadParameter(f"{field_name} expected one of: true/false/1/0/yes/no")
24
+
25
+
26
+ def parse_json_dict(raw: Optional[str], *, field_name: str) -> Optional[dict[str, Any]]:
27
+ if raw is None:
28
+ return None
29
+ try:
30
+ parsed = json.loads(raw)
31
+ except json.JSONDecodeError as exc:
32
+ raise typer.BadParameter(f"{field_name} must be valid JSON.") from exc
33
+ if not isinstance(parsed, dict):
34
+ raise typer.BadParameter(f"{field_name} must decode to a JSON object.")
35
+ return parsed
36
+
37
+
38
+ def parse_json_array(raw: Optional[str], *, field_name: str) -> Optional[list[Any]]:
39
+ if raw is None:
40
+ return None
41
+ try:
42
+ parsed = json.loads(raw)
43
+ except json.JSONDecodeError as exc:
44
+ raise typer.BadParameter(f"{field_name} must be valid JSON.") from exc
45
+ if not isinstance(parsed, list):
46
+ raise typer.BadParameter(f"{field_name} must decode to a JSON array.")
47
+ return parsed
48
+
49
+
50
+ def parse_csv_list(raw: Optional[str]) -> list[str]:
51
+ if not raw:
52
+ return []
53
+ return [token.strip() for token in raw.split(",") if token.strip()]
54
+
55
+
56
+ def validate_uuid_list(values: list[str], *, field_name: str) -> None:
57
+ for value in values:
58
+ validate_uuid(value, field_name=field_name)
@@ -0,0 +1,33 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ import typer
5
+
6
+
7
+ def show_target(fields: dict[str, Any]) -> None:
8
+ typer.echo("Target:")
9
+ for key, value in fields.items():
10
+ typer.echo(f"- {key}: {value}")
11
+
12
+
13
+ def confirm_or_exit(*, yes: bool, prompt: str = "Continue?") -> None:
14
+ if not yes and not typer.confirm(prompt):
15
+ raise typer.Exit(code=1)
16
+
17
+
18
+ def emit_dry_run(*, payload: dict[str, Any], output_json: bool) -> None:
19
+ typer.echo(json.dumps(payload, indent=2) if output_json else str(payload))
20
+ raise typer.Exit(code=0)
21
+
22
+
23
+ def emit_success(
24
+ *,
25
+ output_json: bool,
26
+ payload: Any,
27
+ fields: dict[str, Any],
28
+ ) -> None:
29
+ if output_json:
30
+ typer.echo(json.dumps(payload, indent=2, default=str))
31
+ return
32
+ items = [f"{key}={value}" for key, value in fields.items()]
33
+ typer.echo("result=success | " + " | ".join(items))