Repository navigation
Expand file tree
/
Copy pathcli.py
More file actions
281 lines (226 loc) · 9.38 KB
/
Copy pathcli.py
File metadata and controls
281 lines (226 loc) · 9.38 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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
"""
sdta/cli.py
Unified CLI for the US State Department SDTA Affairs Data API.
Commands
--------
advisory Travel advisory level + summary for a country
advisories All current travel advisories (optionally filtered by level)
info Full travel information record for a country
field Single field from a country's travel info record
debug Raw API response for a country (for inspection / debugging)
Examples
--------
sdta advisory AR
sdta advisory France
sdta advisories --level 4
sdta info MX
sdta info MX --field safety_and_security
sdta info MX --json
sdta field AR health
sdta field AR health --html
sdta debug FR
"""
from __future__ import annotations
import argparse
import json
import sys
import textwrap
from typing import Optional
import httpx
# ---------------------------------------------------------------------------
# Local imports (relative — works when run as part of the package)
# ---------------------------------------------------------------------------
from travel_advisory_by_country import fetch as fetch_advisory
from travel_advisories_all import fetch as fetch_all_advisories
from country_travel_info_by_country import fetch as fetch_info
from country_travel_info_field import fetch as fetch_field
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
VALID_INFO_FIELDS = [
"destination_description",
"entry_exit_requirements",
"health",
"local_laws_and_special_circumstances",
"safety_and_security",
"travel_embassyAndConsulate",
"travel_transportation",
]
ADVISORY_LEVELS = {1: "Normal", 2: "Exercise Increased Caution", 3: "Reconsider Travel", 4: "Do Not Travel"}
LEVEL_COLORS = {1: "\033[32m", 2: "\033[33m", 3: "\033[91m", 4: "\033[31m"}
RESET = "\033[0m"
BOLD = "\033[1m"
def _level_badge(level: Optional[int]) -> str:
if level is None:
return "Level ?"
color = LEVEL_COLORS.get(level, "")
label = ADVISORY_LEVELS.get(level, f"Level {level}")
return f"{color}{BOLD}Level {level} — {label}{RESET}"
def _handle_http_error(e: httpx.HTTPStatusError) -> None:
print(f"HTTP {e.response.status_code}: {e.response.text[:200]}", file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Command handlers
# ---------------------------------------------------------------------------
def cmd_advisory(args: argparse.Namespace) -> None:
"""Fetch travel advisory for a single country."""
try:
result = fetch_advisory(args.country)
except httpx.HTTPStatusError as e:
_handle_http_error(e)
if result is None:
print(f"No advisory found for '{args.country}'.")
sys.exit(0)
if args.json:
import dataclasses
print(json.dumps(dataclasses.asdict(result), indent=2, default=str))
return
print(f"\n{BOLD}{result.country_name}{RESET} ({result.country_code})")
print(_level_badge(result.level))
if result.summary_text:
print()
print(textwrap.fill(result.summary_text, width=80))
if result.updated:
from datetime import datetime
try:
dt = datetime.fromisoformat(result.updated)
print(f"\nUpdated: {dt.strftime('%B %d, %Y')}")
except ValueError:
print(f"\n{result.updated}")
print()
def cmd_advisories(args: argparse.Namespace) -> None:
"""Fetch all current travel advisories, optionally filtered by level."""
try:
results = fetch_all_advisories()
except httpx.HTTPStatusError as e:
_handle_http_error(e)
if args.level:
results = [r for r in results if r.level == args.level]
if not results:
print("No advisories found matching that filter.")
sys.exit(0)
if args.json:
import dataclasses
print(json.dumps([dataclasses.asdict(r) for r in results], indent=2, default=str))
return
# Sort by level descending (highest risk first)
results.sort(key=lambda r: (r.level or 0), reverse=True)
for r in results:
badge = _level_badge(r.level)
print(f" {r.country_name:<35} {badge}")
print(f"\n{len(results)} advisories listed.")
def cmd_info(args: argparse.Namespace) -> None:
"""Fetch full travel information for a country."""
try:
info = fetch_info(args.country, timeout=60)
except httpx.HTTPStatusError as e:
_handle_http_error(e)
if info is None:
print(f"No travel information found for '{args.country}'.")
sys.exit(0)
if args.json:
import dataclasses
print(json.dumps(dataclasses.asdict(info), indent=2, default=str))
return
if args.field:
value = info.get_field(args.field)
if value:
print(textwrap.fill(value, width=80))
else:
print(f"Field '{args.field}' not available for {info.tag}.")
return
# Default: formatted multi-section output
print(f"\n{BOLD}=== {info.geopoliticalarea} ({info.tag}) ==={RESET}")
print(f"{info.last_update_date or 'Update date unknown'}")
print()
sections = [
("Safety & Security", info.safety_and_security_text),
("Entry/Exit Requirements", info.entry_exit_requirements_text),
("Health", info.health_text),
("Local Laws", info.local_laws_and_special_circumstances_text),
("Transportation", info.travel_transportation_text),
("Embassy & Consulate", info.travel_embassy_and_consulate_text),
("Destination Overview", info.destination_description_text),
]
for title, content in sections:
if content:
preview = content[:500] + ("…" if len(content) > 500 else "")
print(f"{BOLD}--- {title} ---{RESET}")
print(textwrap.fill(preview, width=80))
print()
def cmd_field(args: argparse.Namespace) -> None:
"""Fetch a single specific field from a country's travel info record."""
try:
result = fetch_field(args.country, args.field)
except httpx.HTTPStatusError as e:
_handle_http_error(e)
if result is None:
print(f"No data for '{args.country.upper()}' / '{args.field}'.")
sys.exit(0)
if args.json:
import dataclasses
print(json.dumps(dataclasses.asdict(result), indent=2))
return
if args.html:
print(result.html or "")
else:
print(textwrap.fill(result.text or "", width=80))
# ---------------------------------------------------------------------------
# Argument parser
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="sdta",
description="Query the US State Department SDTA Affairs Data API.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
examples:
sdta advisory AR
sdta advisory France
sdta advisories --level 4
sdta advisories --level 3 --json
sdta info MX
sdta info MX --field safety_and_security
sdta info MX --json
sdta field AR health
sdta field AR health --html
sdta debug FR
""",
)
subparsers = parser.add_subparsers(dest="command", metavar="command")
subparsers.required = True
# --- advisory ---
p_adv = subparsers.add_parser("advisory", help="Travel advisory level and summary for a country")
p_adv.add_argument("country", help="Country code or name (e.g. AR, France, MX)")
p_adv.add_argument("--json", action="store_true", help="Output as JSON")
p_adv.set_defaults(func=cmd_advisory)
# --- advisories ---
p_advs = subparsers.add_parser("advisories", help="All current travel advisories")
p_advs.add_argument("--level", type=int, choices=[1, 2, 3, 4],
help="Filter by advisory level (1=Normal … 4=Do Not Travel)")
p_advs.add_argument("--json", action="store_true", help="Output as JSON")
p_advs.set_defaults(func=cmd_advisories)
# --- info ---
p_info = subparsers.add_parser("info", help="Full travel information for a country")
p_info.add_argument("country", help="State Dept 2-letter country code (e.g. AR, FR, MX)")
p_info.add_argument("--field", choices=VALID_INFO_FIELDS, default=None,
help="Print only a specific field (plain text)")
p_info.add_argument("--json", action="store_true", help="Output full record as JSON")
p_info.set_defaults(func=cmd_info)
# --- field ---
p_field = subparsers.add_parser("field", help="Single field from a country's travel info")
p_field.add_argument("country", help="State Dept 2-letter country code (e.g. AR, FR, MX)")
p_field.add_argument("field", choices=VALID_INFO_FIELDS, help="Field to retrieve")
p_field.add_argument("--html", action="store_true", help="Output raw HTML instead of plain text")
p_field.add_argument("--json", action="store_true", help="Output as JSON with html + text variants")
p_field.set_defaults(func=cmd_field)
return parser
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = build_parser()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()