opensky-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.
- opensky/__init__.py +3 -0
- opensky/__main__.py +3 -0
- opensky/_vendor/__init__.py +0 -0
- opensky/_vendor/google_flights.py +489 -0
- opensky/airports.py +183 -0
- opensky/cache.py +49 -0
- opensky/cli.py +690 -0
- opensky/config.py +151 -0
- opensky/data/conflict_zones.json +203 -0
- opensky/data/demo_flights.json +3355 -0
- opensky/display.py +358 -0
- opensky/models.py +90 -0
- opensky/providers/__init__.py +81 -0
- opensky/providers/amadeus.py +150 -0
- opensky/providers/duffel.py +122 -0
- opensky/providers/google.py +111 -0
- opensky/safety.py +138 -0
- opensky/search.py +280 -0
- opensky_cli-0.1.0.dist-info/METADATA +231 -0
- opensky_cli-0.1.0.dist-info/RECORD +23 -0
- opensky_cli-0.1.0.dist-info/WHEEL +4 -0
- opensky_cli-0.1.0.dist-info/entry_points.txt +2 -0
- opensky_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
opensky/config.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import tomllib
|
|
5
|
+
from datetime import date, timedelta
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DateRange(BaseModel):
|
|
12
|
+
start: str
|
|
13
|
+
end: str
|
|
14
|
+
|
|
15
|
+
@field_validator("start", "end")
|
|
16
|
+
@classmethod
|
|
17
|
+
def validate_date(cls, v: str) -> str:
|
|
18
|
+
date.fromisoformat(v)
|
|
19
|
+
return v
|
|
20
|
+
|
|
21
|
+
def dates(self) -> list[str]:
|
|
22
|
+
s = date.fromisoformat(self.start)
|
|
23
|
+
e = date.fromisoformat(self.end)
|
|
24
|
+
result = []
|
|
25
|
+
while s <= e:
|
|
26
|
+
result.append(s.isoformat())
|
|
27
|
+
s += timedelta(days=1)
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SearchConfig(BaseModel):
|
|
32
|
+
origins: list[str]
|
|
33
|
+
destinations: list[str]
|
|
34
|
+
date_range: DateRange
|
|
35
|
+
cabin: str = "economy"
|
|
36
|
+
currency: str = "EUR"
|
|
37
|
+
stops: str = "any"
|
|
38
|
+
max_price: float = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_RISK_ALIASES: dict[str, str] = {
|
|
42
|
+
"avoid": "do_not_fly",
|
|
43
|
+
"risky": "high_risk",
|
|
44
|
+
"caution": "caution",
|
|
45
|
+
"do_not_fly": "do_not_fly",
|
|
46
|
+
"high_risk": "high_risk",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SafetyConfig(BaseModel):
|
|
51
|
+
risk_threshold: str = "high_risk"
|
|
52
|
+
|
|
53
|
+
@field_validator("risk_threshold")
|
|
54
|
+
@classmethod
|
|
55
|
+
def normalize_risk(cls, v: str) -> str:
|
|
56
|
+
normalized = _RISK_ALIASES.get(v.lower().strip())
|
|
57
|
+
if normalized is None:
|
|
58
|
+
valid = ", ".join(_RISK_ALIASES.keys())
|
|
59
|
+
raise ValueError(f"Unknown risk_threshold '{v}'. Use: {valid}")
|
|
60
|
+
return normalized
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ScoringConfig(BaseModel):
|
|
64
|
+
price_weight: float = 1.0
|
|
65
|
+
duration_weight: float = 0.5
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConnectionsConfig(BaseModel):
|
|
69
|
+
final_destination: str = ""
|
|
70
|
+
transit_hours: dict[str, float] = {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ScanConfig(BaseModel):
|
|
74
|
+
search: SearchConfig
|
|
75
|
+
safety: SafetyConfig = SafetyConfig()
|
|
76
|
+
scoring: ScoringConfig = ScoringConfig()
|
|
77
|
+
connections: ConnectionsConfig = ConnectionsConfig()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _validate_date_range(cfg: ScanConfig) -> None:
|
|
81
|
+
"""Validate date range: start <= end, warn if start is in the past."""
|
|
82
|
+
s = date.fromisoformat(cfg.search.date_range.start)
|
|
83
|
+
e = date.fromisoformat(cfg.search.date_range.end)
|
|
84
|
+
if s > e:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Start date {cfg.search.date_range.start} is after end date "
|
|
87
|
+
f"{cfg.search.date_range.end}. Start must be before end."
|
|
88
|
+
)
|
|
89
|
+
if s < date.today():
|
|
90
|
+
print(
|
|
91
|
+
f"Warning: start date {cfg.search.date_range.start} is in the past.",
|
|
92
|
+
file=sys.stderr,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_config(path: str | Path) -> ScanConfig:
|
|
97
|
+
p = Path(path)
|
|
98
|
+
with open(p, "rb") as f:
|
|
99
|
+
data = tomllib.load(f)
|
|
100
|
+
cfg = ScanConfig(**data)
|
|
101
|
+
|
|
102
|
+
_validate_date_range(cfg)
|
|
103
|
+
|
|
104
|
+
# Resolve city names to IATA codes in origins/destinations
|
|
105
|
+
from opensky.airports import resolve_airport
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
cfg.search.origins = [resolve_airport(o, interactive=False) for o in cfg.search.origins]
|
|
109
|
+
cfg.search.destinations = [resolve_airport(d, interactive=False) for d in cfg.search.destinations]
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
raise ValueError(str(e)) from None
|
|
112
|
+
|
|
113
|
+
return cfg
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
EXAMPLE_CONFIG = """\
|
|
117
|
+
[search]
|
|
118
|
+
# Airport codes (city names work in CLI: opensky search bangalore hamburg ...)
|
|
119
|
+
origins = ["BLR", "DEL", "BOM", "KUL", "BKK", "SIN"] # Bangalore, Delhi, Mumbai, Kuala Lumpur, Bangkok, Singapore
|
|
120
|
+
destinations = ["HAM", "FRA", "MUC", "BER", "AMS", "CPH"] # Hamburg, Frankfurt, Munich, Berlin, Amsterdam, Copenhagen
|
|
121
|
+
cabin = "economy" # economy, premium_economy, business, first
|
|
122
|
+
currency = "EUR"
|
|
123
|
+
stops = "any" # any, nonstop, 1, 2
|
|
124
|
+
# max_price = 500 # maximum price (0 = no limit)
|
|
125
|
+
|
|
126
|
+
[search.date_range]
|
|
127
|
+
start = "2026-03-10"
|
|
128
|
+
end = "2026-03-20"
|
|
129
|
+
|
|
130
|
+
[safety]
|
|
131
|
+
risk_threshold = "risky" # hide flights at this level or above: avoid, risky, caution
|
|
132
|
+
|
|
133
|
+
[scoring]
|
|
134
|
+
price_weight = 1.0
|
|
135
|
+
duration_weight = 0.5
|
|
136
|
+
|
|
137
|
+
# Optional: if your final destination is one city, add ground travel time
|
|
138
|
+
# from each arrival airport. This helps scoring rank "fly to nearby city +
|
|
139
|
+
# short train" vs "fly direct but expensive".
|
|
140
|
+
[connections]
|
|
141
|
+
final_destination = "Hamburg"
|
|
142
|
+
|
|
143
|
+
[connections.transit_hours]
|
|
144
|
+
HAM = 0 # direct, no ground travel
|
|
145
|
+
HAJ = 1.5 # Hannover: 1.5h train to Hamburg
|
|
146
|
+
BER = 2 # Berlin: 2h train to Hamburg
|
|
147
|
+
FRA = 4 # Frankfurt: 4h train to Hamburg
|
|
148
|
+
MUC = 5.5
|
|
149
|
+
AMS = 5
|
|
150
|
+
CPH = 5
|
|
151
|
+
"""
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"updated": "2026-03-01",
|
|
5
|
+
"sources": [
|
|
6
|
+
"EASA Conflict Zone Information Bulletins (CZIB)",
|
|
7
|
+
"safeairspace.net",
|
|
8
|
+
"FAA NOTAM database"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"zones": [
|
|
12
|
+
{
|
|
13
|
+
"id": "ukraine",
|
|
14
|
+
"name": "Ukraine",
|
|
15
|
+
"risk_level": "do_not_fly",
|
|
16
|
+
"countries": ["UA"],
|
|
17
|
+
"airports": [],
|
|
18
|
+
"source": "EASA CZIB 2022-01 R16",
|
|
19
|
+
"details": "Active conflict zone. All Ukrainian airspace closed to civil aviation.",
|
|
20
|
+
"updated": "2026-02-28"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "iran",
|
|
24
|
+
"name": "Iran",
|
|
25
|
+
"risk_level": "do_not_fly",
|
|
26
|
+
"countries": ["IR"],
|
|
27
|
+
"airports": [],
|
|
28
|
+
"source": "EASA CZIB 2026-002",
|
|
29
|
+
"details": "Military strikes Feb 2026. FIRs OIIX, OIBB closed to overflights.",
|
|
30
|
+
"updated": "2026-02-28"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "iraq",
|
|
34
|
+
"name": "Iraq",
|
|
35
|
+
"risk_level": "do_not_fly",
|
|
36
|
+
"countries": ["IQ"],
|
|
37
|
+
"airports": [],
|
|
38
|
+
"source": "EASA CZIB 2026-002",
|
|
39
|
+
"details": "Airspace restrictions due to regional conflict.",
|
|
40
|
+
"updated": "2026-02-28"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "syria",
|
|
44
|
+
"name": "Syria",
|
|
45
|
+
"risk_level": "do_not_fly",
|
|
46
|
+
"countries": ["SY"],
|
|
47
|
+
"airports": [],
|
|
48
|
+
"source": "EASA CZIB 2024-003",
|
|
49
|
+
"details": "Ongoing conflict. Damascus FIR closed to overflights.",
|
|
50
|
+
"updated": "2026-02-28"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "israel",
|
|
54
|
+
"name": "Israel",
|
|
55
|
+
"risk_level": "do_not_fly",
|
|
56
|
+
"countries": ["IL"],
|
|
57
|
+
"airports": [],
|
|
58
|
+
"source": "EASA CZIB 2024-005",
|
|
59
|
+
"details": "Active conflict zone. Intermittent airspace closures.",
|
|
60
|
+
"updated": "2026-02-28"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "lebanon",
|
|
64
|
+
"name": "Lebanon",
|
|
65
|
+
"risk_level": "do_not_fly",
|
|
66
|
+
"countries": ["LB"],
|
|
67
|
+
"airports": [],
|
|
68
|
+
"source": "EASA CZIB 2024-006",
|
|
69
|
+
"details": "Beirut FIR affected by regional conflict.",
|
|
70
|
+
"updated": "2026-02-28"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "yemen",
|
|
74
|
+
"name": "Yemen",
|
|
75
|
+
"risk_level": "do_not_fly",
|
|
76
|
+
"countries": ["YE"],
|
|
77
|
+
"airports": [],
|
|
78
|
+
"source": "EASA CZIB 2024-007",
|
|
79
|
+
"details": "Ongoing conflict and Houthi activity affecting airspace.",
|
|
80
|
+
"updated": "2026-02-28"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "libya",
|
|
84
|
+
"name": "Libya",
|
|
85
|
+
"risk_level": "do_not_fly",
|
|
86
|
+
"countries": ["LY"],
|
|
87
|
+
"airports": [],
|
|
88
|
+
"source": "EASA CZIB 2023-001",
|
|
89
|
+
"details": "Unstable security situation.",
|
|
90
|
+
"updated": "2026-02-28"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "somalia",
|
|
94
|
+
"name": "Somalia",
|
|
95
|
+
"risk_level": "do_not_fly",
|
|
96
|
+
"countries": ["SO"],
|
|
97
|
+
"airports": [],
|
|
98
|
+
"source": "EASA CZIB 2023-002",
|
|
99
|
+
"details": "Ongoing conflict. Limited ATC capability.",
|
|
100
|
+
"updated": "2026-02-28"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "afghanistan",
|
|
104
|
+
"name": "Afghanistan",
|
|
105
|
+
"risk_level": "do_not_fly",
|
|
106
|
+
"countries": ["AF"],
|
|
107
|
+
"airports": [],
|
|
108
|
+
"source": "EASA CZIB 2022-002",
|
|
109
|
+
"details": "No functioning civil aviation authority.",
|
|
110
|
+
"updated": "2026-02-28"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "north_korea",
|
|
114
|
+
"name": "North Korea",
|
|
115
|
+
"risk_level": "do_not_fly",
|
|
116
|
+
"countries": ["KP"],
|
|
117
|
+
"airports": [],
|
|
118
|
+
"source": "FAA NOTAM",
|
|
119
|
+
"details": "Prohibited airspace. Missile launch risk.",
|
|
120
|
+
"updated": "2026-02-28"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "sudan",
|
|
124
|
+
"name": "Sudan",
|
|
125
|
+
"risk_level": "do_not_fly",
|
|
126
|
+
"countries": ["SD"],
|
|
127
|
+
"airports": [],
|
|
128
|
+
"source": "EASA CZIB 2023-003",
|
|
129
|
+
"details": "Civil war. Khartoum FIR dangerous.",
|
|
130
|
+
"updated": "2026-02-28"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "gulf_states",
|
|
134
|
+
"name": "Gulf States (Airspace Risk)",
|
|
135
|
+
"risk_level": "high_risk",
|
|
136
|
+
"countries": [],
|
|
137
|
+
"airports": ["DXB", "AUH", "SHJ", "RKT", "DOH", "BAH", "KWI", "MCT"],
|
|
138
|
+
"source": "safeairspace.net",
|
|
139
|
+
"details": "Adjacent to Iran/Iraq conflict. Overflights at risk during escalations. Gulf FIRs periodically restricted.",
|
|
140
|
+
"updated": "2026-02-28"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "eritrea",
|
|
144
|
+
"name": "Eritrea",
|
|
145
|
+
"risk_level": "high_risk",
|
|
146
|
+
"countries": ["ER"],
|
|
147
|
+
"airports": [],
|
|
148
|
+
"source": "EASA CZIB 2023-004",
|
|
149
|
+
"details": "Military activity near borders.",
|
|
150
|
+
"updated": "2026-02-28"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "ethiopia_partial",
|
|
154
|
+
"name": "Ethiopia (Northern Regions)",
|
|
155
|
+
"risk_level": "high_risk",
|
|
156
|
+
"countries": [],
|
|
157
|
+
"airports": ["MQX", "AXU", "GDQ", "SHC"],
|
|
158
|
+
"source": "EASA CZIB 2023-005",
|
|
159
|
+
"details": "Tigray region conflict aftermath. Northern airports affected.",
|
|
160
|
+
"updated": "2026-02-28"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"id": "mali",
|
|
164
|
+
"name": "Mali",
|
|
165
|
+
"risk_level": "high_risk",
|
|
166
|
+
"countries": ["ML"],
|
|
167
|
+
"airports": [],
|
|
168
|
+
"source": "EASA CZIB 2023-006",
|
|
169
|
+
"details": "Armed groups active. Limited ATC in northern regions.",
|
|
170
|
+
"updated": "2026-02-28"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "niger",
|
|
174
|
+
"name": "Niger",
|
|
175
|
+
"risk_level": "high_risk",
|
|
176
|
+
"countries": ["NE"],
|
|
177
|
+
"airports": [],
|
|
178
|
+
"source": "EASA CZIB 2023-007",
|
|
179
|
+
"details": "Post-coup instability.",
|
|
180
|
+
"updated": "2026-02-28"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"id": "pakistan_partial",
|
|
184
|
+
"name": "Pakistan (Border Regions)",
|
|
185
|
+
"risk_level": "caution",
|
|
186
|
+
"countries": [],
|
|
187
|
+
"airports": ["PEW", "UET", "TUK"],
|
|
188
|
+
"source": "safeairspace.net",
|
|
189
|
+
"details": "Western border areas near Afghanistan/Iran. Major airports (ISB, LHE, KHI) unaffected.",
|
|
190
|
+
"updated": "2026-02-28"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "russia_partial",
|
|
194
|
+
"name": "Russia (Southern/Western Border)",
|
|
195
|
+
"risk_level": "caution",
|
|
196
|
+
"countries": [],
|
|
197
|
+
"airports": ["ROV", "KRR", "VOZ", "BEL"],
|
|
198
|
+
"source": "EASA CZIB 2022-01",
|
|
199
|
+
"details": "Southern Russian airports near Ukraine conflict. Moscow/St. Petersburg unaffected.",
|
|
200
|
+
"updated": "2026-02-28"
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|