avwx.current.notam
A NOTAM (Notice to Air Missions) is a report detailing special events or conditions affecting airport and flight operations. These can include, but are in no way limitted to:
- Runway closures
- Lack of radar services
- Rocket launches
- Hazard locations
- Airspace restrictions
- Construction updates
- Unusual aircraft activity
NOTAMs have varius classifications and apply to certain types or size of aircraft. Some apply only to IFR operations, like when an ILS is out of service. Others apply only to airport operations the en route aircraft can ignore.
Every NOTAM has a start and end date and time. Additional NOTAMs may be issued to update, replace, or cancel existing NOTAMs as well. Some NOTAMs may still be served up to 10 days after the end date, so it's up to the developer to include or filter these reports.
1""" 2A NOTAM (Notice to Air Missions) is a report detailing special events or 3conditions affecting airport and flight operations. These can include, but are 4in no way limitted to: 5 6- Runway closures 7- Lack of radar services 8- Rocket launches 9- Hazard locations 10- Airspace restrictions 11- Construction updates 12- Unusual aircraft activity 13 14NOTAMs have varius classifications and apply to certain types or size of 15aircraft. Some apply only to IFR operations, like when an ILS is out of 16service. Others apply only to airport operations the en route aircraft can 17ignore. 18 19Every NOTAM has a start and end date and time. Additional NOTAMs may be issued 20to update, replace, or cancel existing NOTAMs as well. Some NOTAMs may still be 21served up to 10 days after the end date, so it's up to the developer to include 22or filter these reports. 23""" 24 25# pylint: disable=invalid-name 26 27# stdlib 28import re 29from contextlib import suppress 30from datetime import datetime, timezone 31from typing import List, Optional, Union 32 33# library 34from dateutil.tz import gettz 35from avwx import exceptions 36 37# module 38from avwx.current.base import Reports 39from avwx.parsing import core 40from avwx.service import FAA_NOTAM 41from avwx.static.core import SPECIAL_NUMBERS 42from avwx.static.notam import ( 43 CODES, 44 CONDITION, 45 PURPOSE, 46 REPORT_TYPE, 47 SCOPE, 48 SUBJECT, 49 TRAFFIC_TYPE, 50) 51from avwx.structs import ( 52 Code, 53 Coord, 54 NotamData, 55 Number, 56 Qualifiers, 57 Timestamp, 58 Tuple, 59 Units, 60) 61 62# https://www.navcanada.ca/en/briefing-on-the-transition-to-icao-notam-format.pdf 63# https://www.faa.gov/air_traffic/flight_info/aeronav/notams/media/2021-09-07_ICAO_NOTAM_101_Presentation_for_Airport_Operators.pdf 64 65 66class Notams(Reports): 67 ''' 68 The Notams class provides two ways of requesting all applicable NOTAMs in 69 an area: airport code and coordinate. The service will fetch all reports 70 within 10 nautical miles of the desired center point. You can change the 71 distance by updating the `Notams.radius` member before calling `update()`. 72 73 ```python 74 >>> from pprint import pprint 75 >>> from avwx import Notams 76 >>> from avwx.structs import Coord 77 >>> 78 >>> kjfk = Notams("KJFK") 79 >>> kjfk.update() 80 True 81 >>> kjfk.last_updated 82 datetime.datetime(2022, 5, 26, 0, 43, 22, 44753, tzinfo=datetime.timezone.utc) 83 >>> print(kjfk.data[0].raw) 84 01/113 NOTAMN 85 Q) ZNY/QMXLC/IV/NBO/A/000/999/4038N07346W005 86 A) KJFK 87 B) 2101081328 88 C) 2209301100 89 90 E) TWY TB BTN TERMINAL 8 RAMP AND TWY A CLSD 91 >>> pprint(kjfk.data[0].qualifiers) 92 Qualifiers(repr='ZNY/QMXLC/IV/NBO/A/000/999/4038N07346W005', 93 fir='ZNY', 94 subject=Code(repr='MX', value='Taxiway'), 95 condition=Code(repr='LC', value='Closed'), 96 traffic=Code(repr='IV', value='IFR and VFR'), 97 purpose=[Code(repr='N', value='Immediate'), 98 Code(repr='B', value='Briefing'), 99 Code(repr='O', value='Flight Operations')], 100 scope=[Code(repr='A', value='Aerodrome')], 101 lower=Number(repr='000', value=0, spoken='zero'), 102 upper=Number(repr='999', value=999, spoken='nine nine nine'), 103 coord=Coord(lat=40.38, lon=-73.46, repr='4038N07346W'), 104 radius=Number(repr='005', value=5, spoken='five')) 105 >>> 106 >>> coord = Notams(coord=Coord(lat=52, lon=-0.23)) 107 >>> coord.update() 108 True 109 >>> coord.data[0].station 110 'EGSS' 111 >>> print(coord.data[0].body) 112 LONDON STANSTED ATC SURVEILLANCE MINIMUM ALTITUDE CHART - IN 113 FREQUENCY BOX RENAME ESSEX RADAR TO STANSTED RADAR. 114 UK AIP AD 2.EGSS-5-1 REFERS 115 ``` 116 117 The `parse` and `from_report` methods can parse a report string if you want 118 to override the normal fetching process. 119 120 ```python 121 >>> from avwx import Notams 122 >>> report = """ 123 05/295 NOTAMR 124 Q) ZNY/QMNHW/IV/NBO/A/000/999/4038N07346W005 125 A) KJFK 126 B) 2205201527 127 C) 2205271100 128 129 E) APRON TERMINAL 4 RAMP CONST WIP S SIDE TAXILANE G LGTD AND BARRICADED 130 """ 131 >>> kjfk = Notams.from_report(report) 132 >>> kjfk.data[0].type 133 Code(repr='NOTAMR', value='Replace') 134 >>> kjfk.data[0].start_time 135 Timestamp(repr='2205201527', dt=datetime.datetime(2022, 5, 20, 15, 27, tzinfo=datetime.timezone.utc)) 136 ``` 137 ''' 138 139 data: Optional[List[NotamData]] = None # type: ignore 140 radius: int = 10 141 142 def __init__(self, code: Optional[str] = None, coord: Optional[Coord] = None): 143 super().__init__(code, coord) 144 self.service = FAA_NOTAM("notam") 145 146 async def _post_update(self) -> None: 147 self._post_parse() 148 149 def _post_parse(self) -> None: 150 self.data, units = [], None 151 if self.raw is None: 152 return 153 for report in self.raw: 154 if "||" in report: 155 issue_text, report = report.split("||") 156 issued_value = datetime.strptime(issue_text, r"%m/%d/%Y %H%M") 157 issued = Timestamp(issue_text, issued_value) 158 else: 159 issued = None 160 try: 161 data, units = parse(report, issued=issued) 162 self.data.append(data) 163 except Exception as exc: # pylint: disable=broad-except 164 exceptions.exception_intercept(exc, raw=report) # type: ignore 165 if units: 166 self.units = units 167 168 @staticmethod 169 def sanitize(report: str) -> str: 170 """Sanitizes a NOTAM string""" 171 return sanitize(report) 172 173 async def async_update(self, timeout: int = 10, disable_post: bool = False) -> bool: 174 """Async updates report data by fetching and parsing the report""" 175 reports = await self.service.async_fetch( # type: ignore 176 icao=self.code, coord=self.coord, radius=self.radius, timeout=timeout 177 ) 178 self.source = self.service.root 179 return await self._update(reports, None, disable_post) 180 181 182ALL_KEYS_PATTERN = re.compile(r"\b[A-GQ]\) ") 183KEY_PATTERNS = { 184 "Q": re.compile(r"\b[A-G]\) "), 185 "A": re.compile(r"\b[B-G]\) "), 186 "B": re.compile(r"\b[C-G]\) "), 187 "C": re.compile(r"\b[D-G]\) "), 188 "D": re.compile(r"\b[E-G]\) "), 189 "E": re.compile(r"\b[FG]\) "), 190 "F": re.compile(r"\bG\) "), 191 # No "G" 192} 193 194 195def _rear_coord(value: str) -> Optional[Coord]: 196 """Convert coord strings with direction characters at the end: 5126N00036W""" 197 if len(value) != 11: 198 return None 199 try: 200 lat = float(f"{value[:2]}.{value[2:4]}") 201 lon = float(f"{value[5:8]}.{value[8:10]}") 202 except ValueError: 203 return None 204 if value[4] == "S": 205 lat *= -1 206 if value[10] == "W": 207 lon *= -1 208 return Coord(lat, lon, value) 209 210 211def _split_location( 212 location: Optional[str], 213) -> Tuple[Optional[Coord], Optional[Number]]: 214 """Identify coordinate and radius from location element""" 215 if not location: 216 return None, None 217 coord, radius = None, None 218 if len(location) == 14 and location[-3:].isdigit(): 219 radius = core.make_number(location[-3:]) 220 location = location[:-3] 221 if len(location) == 11 and location[-1] in {"E", "W"}: 222 coord = _rear_coord(location) 223 return coord, radius 224 225 226def _header(value: str) -> Tuple[str, Optional[Code], Optional[str]]: 227 """Parse pre-tag headers""" 228 header = value.strip().split() 229 replaces = None 230 if len(header) == 3: 231 number, type_text, replaces = header 232 else: 233 number, type_text = header 234 report_type = Code.from_dict(type_text, REPORT_TYPE) 235 return number, report_type, replaces 236 237 238def _find_q_codes( 239 codes: List[str], 240) -> Tuple[ 241 Optional[Code], 242 List[Code], 243 List[Code], 244 Optional[str], 245 Optional[str], 246 Optional[str], 247]: 248 """Identify traffic, purpose, and scope codes""" 249 # The 'K' code can be both purpose and scope, but they have the same value 250 traffic, lower, upper, location = None, None, None, None 251 purpose: List[Code] = [] 252 scope: List[Code] = [] 253 for code in codes: 254 if not code: 255 continue 256 # Altitudes can be int or float values 257 with suppress(ValueError): 258 float(code) 259 if not lower: 260 lower = code 261 else: 262 upper = code 263 continue 264 # location will always be the longest element if available 265 if len(code) > 10: 266 location = code 267 continue 268 # Remaining elements must match known value dictionary combinations 269 if not traffic and code in TRAFFIC_TYPE: 270 traffic = Code.from_dict(code, TRAFFIC_TYPE) 271 continue 272 if not purpose: 273 purpose = Code.from_list(code, PURPOSE, exclusive=True) 274 if not scope: 275 scope = Code.from_list(code, SCOPE, exclusive=True) 276 return traffic, purpose, scope, lower, upper, location 277 278 279def _qualifiers(value: str, units: Units) -> Qualifiers: 280 """Parse the NOTAM Q) line into components""" 281 # pylint: disable=too-many-locals 282 fir, q_code, *codes = [i.strip() for i in re.split("/| ", value.strip())] 283 traffic, purpose, scope, lower, upper, location = _find_q_codes(codes) 284 subject, condition = None, None 285 if q_code.startswith("Q"): 286 subject = Code.from_dict(q_code[1:3], SUBJECT) 287 condition_code = q_code[3:] 288 if condition_code.startswith("XX"): 289 condition = Code("XX", (condition_code[2:] or "Unknown").strip()) 290 else: 291 condition = Code.from_dict(condition_code, CONDITION, error=False) 292 coord, radius = _split_location(location) 293 return Qualifiers( 294 repr=value, 295 fir=fir, 296 subject=subject, 297 condition=condition, 298 traffic=traffic, 299 purpose=purpose, 300 scope=scope, 301 lower=make_altitude(lower, units), 302 upper=make_altitude(upper, units), 303 coord=coord, 304 radius=radius, 305 ) 306 307 308def _tz_offset_for(name: Optional[str]) -> Optional[timezone]: 309 """Generates a timezone from tz string name""" 310 if not name: 311 return None 312 if tz := gettz(name): 313 if offset := tz.utcoffset(datetime.now(timezone.utc)): 314 return timezone(offset) 315 return None 316 317 318def make_year_timestamp( 319 value: str, 320 repr: str, # pylint: disable=redefined-builtin 321 tzname: Optional[str] = None, 322) -> Union[Timestamp, Code, None]: 323 """Convert NOTAM timestamp which includes year and month""" 324 values = value.strip().split() 325 if not values: 326 return None 327 value = values[0] 328 if code := CODES.get(value): 329 return Code(value, code) 330 tz = _tz_offset_for(tzname) or timezone.utc 331 raw = datetime.strptime(value[:10], r"%y%m%d%H%M") 332 date = datetime(raw.year, raw.month, raw.day, raw.hour, raw.minute, tzinfo=tz) 333 return Timestamp(repr, date) 334 335 336def parse_linked_times( 337 start: str, end: str 338) -> Tuple[Union[Timestamp, Code, None], Union[Timestamp, Code, None]]: 339 """Parse start and end times sharing any found timezone""" 340 start, end = start.strip(), end.strip() 341 start_raw, end_raw, tzname = start, end, None 342 if len(start) > 10: 343 start, tzname = start[:-3], start[-3:] 344 if len(end) > 10: 345 end, tzname = end[:-3], end[-3:] 346 return make_year_timestamp(start, start_raw, tzname), make_year_timestamp( 347 end, end_raw, tzname 348 ) 349 350 351def make_altitude(value: Optional[str], units: Units) -> Optional[Number]: 352 """Parse NOTAM altitudes""" 353 if not value: 354 return None 355 if trimmed := value.split()[0].strip(" ."): 356 if trimmed in SPECIAL_NUMBERS or trimmed[0].isdigit(): 357 return core.make_altitude(trimmed, units, repr=value)[0] 358 return None 359 360 361def parse(report: str, issued: Optional[Timestamp] = None) -> Tuple[NotamData, Units]: 362 """Parse NOTAM report string""" 363 # pylint: disable=too-many-locals 364 units = Units.international() 365 sanitized = sanitize(report) 366 qualifiers, station, start_time, end_time = None, None, None, None 367 body, number, replaces, report_type = "", None, None, None 368 schedule, lower, upper, text = None, None, None, sanitized 369 match = ALL_KEYS_PATTERN.search(text) 370 # Type and number here 371 if match and match.start() > 0: 372 number, report_type, replaces = _header(text[: match.start()]) 373 start_text, end_text = "", "" 374 while match: 375 tag = match.group()[0] 376 text = text[match.end() :] 377 try: 378 match = KEY_PATTERNS[tag].search(text) 379 except KeyError: 380 match = None 381 item = (text[: match.start()] if match else text).strip() 382 if tag == "Q": 383 qualifiers = _qualifiers(item, units) 384 elif tag == "A": 385 station = item 386 elif tag == "B": 387 start_text = item 388 elif tag == "C": 389 end_text = item 390 elif tag == "D": 391 schedule = item 392 elif tag == "E": 393 body = item 394 elif tag == "F": 395 lower = make_altitude(item, units) 396 elif tag == "G": 397 upper = make_altitude(item, units) 398 start_time, end_time = parse_linked_times(start_text, end_text) 399 return ( 400 NotamData( 401 raw=report, 402 sanitized=sanitized, 403 station=station, 404 time=issued, 405 remarks=None, 406 number=number, 407 replaces=replaces, 408 type=report_type, 409 qualifiers=qualifiers, 410 start_time=start_time, 411 end_time=end_time, 412 schedule=schedule, 413 body=body, 414 lower=lower, 415 upper=upper, 416 ), 417 units, 418 ) 419 420 421def sanitize(report: str) -> str: 422 """Retuns a sanitized report ready for parsing""" 423 return report.replace("\r", "").strip()
67class Notams(Reports): 68 ''' 69 The Notams class provides two ways of requesting all applicable NOTAMs in 70 an area: airport code and coordinate. The service will fetch all reports 71 within 10 nautical miles of the desired center point. You can change the 72 distance by updating the `Notams.radius` member before calling `update()`. 73 74 ```python 75 >>> from pprint import pprint 76 >>> from avwx import Notams 77 >>> from avwx.structs import Coord 78 >>> 79 >>> kjfk = Notams("KJFK") 80 >>> kjfk.update() 81 True 82 >>> kjfk.last_updated 83 datetime.datetime(2022, 5, 26, 0, 43, 22, 44753, tzinfo=datetime.timezone.utc) 84 >>> print(kjfk.data[0].raw) 85 01/113 NOTAMN 86 Q) ZNY/QMXLC/IV/NBO/A/000/999/4038N07346W005 87 A) KJFK 88 B) 2101081328 89 C) 2209301100 90 91 E) TWY TB BTN TERMINAL 8 RAMP AND TWY A CLSD 92 >>> pprint(kjfk.data[0].qualifiers) 93 Qualifiers(repr='ZNY/QMXLC/IV/NBO/A/000/999/4038N07346W005', 94 fir='ZNY', 95 subject=Code(repr='MX', value='Taxiway'), 96 condition=Code(repr='LC', value='Closed'), 97 traffic=Code(repr='IV', value='IFR and VFR'), 98 purpose=[Code(repr='N', value='Immediate'), 99 Code(repr='B', value='Briefing'), 100 Code(repr='O', value='Flight Operations')], 101 scope=[Code(repr='A', value='Aerodrome')], 102 lower=Number(repr='000', value=0, spoken='zero'), 103 upper=Number(repr='999', value=999, spoken='nine nine nine'), 104 coord=Coord(lat=40.38, lon=-73.46, repr='4038N07346W'), 105 radius=Number(repr='005', value=5, spoken='five')) 106 >>> 107 >>> coord = Notams(coord=Coord(lat=52, lon=-0.23)) 108 >>> coord.update() 109 True 110 >>> coord.data[0].station 111 'EGSS' 112 >>> print(coord.data[0].body) 113 LONDON STANSTED ATC SURVEILLANCE MINIMUM ALTITUDE CHART - IN 114 FREQUENCY BOX RENAME ESSEX RADAR TO STANSTED RADAR. 115 UK AIP AD 2.EGSS-5-1 REFERS 116 ``` 117 118 The `parse` and `from_report` methods can parse a report string if you want 119 to override the normal fetching process. 120 121 ```python 122 >>> from avwx import Notams 123 >>> report = """ 124 05/295 NOTAMR 125 Q) ZNY/QMNHW/IV/NBO/A/000/999/4038N07346W005 126 A) KJFK 127 B) 2205201527 128 C) 2205271100 129 130 E) APRON TERMINAL 4 RAMP CONST WIP S SIDE TAXILANE G LGTD AND BARRICADED 131 """ 132 >>> kjfk = Notams.from_report(report) 133 >>> kjfk.data[0].type 134 Code(repr='NOTAMR', value='Replace') 135 >>> kjfk.data[0].start_time 136 Timestamp(repr='2205201527', dt=datetime.datetime(2022, 5, 20, 15, 27, tzinfo=datetime.timezone.utc)) 137 ``` 138 ''' 139 140 data: Optional[List[NotamData]] = None # type: ignore 141 radius: int = 10 142 143 def __init__(self, code: Optional[str] = None, coord: Optional[Coord] = None): 144 super().__init__(code, coord) 145 self.service = FAA_NOTAM("notam") 146 147 async def _post_update(self) -> None: 148 self._post_parse() 149 150 def _post_parse(self) -> None: 151 self.data, units = [], None 152 if self.raw is None: 153 return 154 for report in self.raw: 155 if "||" in report: 156 issue_text, report = report.split("||") 157 issued_value = datetime.strptime(issue_text, r"%m/%d/%Y %H%M") 158 issued = Timestamp(issue_text, issued_value) 159 else: 160 issued = None 161 try: 162 data, units = parse(report, issued=issued) 163 self.data.append(data) 164 except Exception as exc: # pylint: disable=broad-except 165 exceptions.exception_intercept(exc, raw=report) # type: ignore 166 if units: 167 self.units = units 168 169 @staticmethod 170 def sanitize(report: str) -> str: 171 """Sanitizes a NOTAM string""" 172 return sanitize(report) 173 174 async def async_update(self, timeout: int = 10, disable_post: bool = False) -> bool: 175 """Async updates report data by fetching and parsing the report""" 176 reports = await self.service.async_fetch( # type: ignore 177 icao=self.code, coord=self.coord, radius=self.radius, timeout=timeout 178 ) 179 self.source = self.service.root 180 return await self._update(reports, None, disable_post)
The Notams class provides two ways of requesting all applicable NOTAMs in
an area: airport code and coordinate. The service will fetch all reports
within 10 nautical miles of the desired center point. You can change the
distance by updating the Notams.radius
member before calling update()
.
>>> from pprint import pprint
>>> from avwx import Notams
>>> from avwx.structs import Coord
>>>
>>> kjfk = Notams("KJFK")
>>> kjfk.update()
True
>>> kjfk.last_updated
datetime.datetime(2022, 5, 26, 0, 43, 22, 44753, tzinfo=datetime.timezone.utc)
>>> print(kjfk.data[0].raw)
01/113 NOTAMN
Q) ZNY/QMXLC/IV/NBO/A/000/999/4038N07346W005
A) KJFK
B) 2101081328
C) 2209301100
E) TWY TB BTN TERMINAL 8 RAMP AND TWY A CLSD
>>> pprint(kjfk.data[0].qualifiers)
Qualifiers(repr='ZNY/QMXLC/IV/NBO/A/000/999/4038N07346W005',
fir='ZNY',
subject=Code(repr='MX', value='Taxiway'),
condition=Code(repr='LC', value='Closed'),
traffic=Code(repr='IV', value='IFR and VFR'),
purpose=[Code(repr='N', value='Immediate'),
Code(repr='B', value='Briefing'),
Code(repr='O', value='Flight Operations')],
scope=[Code(repr='A', value='Aerodrome')],
lower=Number(repr='000', value=0, spoken='zero'),
upper=Number(repr='999', value=999, spoken='nine nine nine'),
coord=Coord(lat=40.38, lon=-73.46, repr='4038N07346W'),
radius=Number(repr='005', value=5, spoken='five'))
>>>
>>> coord = Notams(coord=Coord(lat=52, lon=-0.23))
>>> coord.update()
True
>>> coord.data[0].station
'EGSS'
>>> print(coord.data[0].body)
LONDON STANSTED ATC SURVEILLANCE MINIMUM ALTITUDE CHART - IN
FREQUENCY BOX RENAME ESSEX RADAR TO STANSTED RADAR.
UK AIP AD 2.EGSS-5-1 REFERS
The parse
and from_report
methods can parse a report string if you want
to override the normal fetching process.
>>> from avwx import Notams
>>> report = """
05/295 NOTAMR
Q) ZNY/QMNHW/IV/NBO/A/000/999/4038N07346W005
A) KJFK
B) 2205201527
C) 2205271100
E) APRON TERMINAL 4 RAMP CONST WIP S SIDE TAXILANE G LGTD AND BARRICADED
"""
>>> kjfk = Notams.from_report(report)
>>> kjfk.data[0].type
Code(repr='NOTAMR', value='Replace')
>>> kjfk.data[0].start_time
Timestamp(repr='2205201527', dt=datetime.datetime(2022, 5, 20, 15, 27, tzinfo=datetime.timezone.utc))
169 @staticmethod 170 def sanitize(report: str) -> str: 171 """Sanitizes a NOTAM string""" 172 return sanitize(report)
Sanitizes a NOTAM string
174 async def async_update(self, timeout: int = 10, disable_post: bool = False) -> bool: 175 """Async updates report data by fetching and parsing the report""" 176 reports = await self.service.async_fetch( # type: ignore 177 icao=self.code, coord=self.coord, radius=self.radius, timeout=timeout 178 ) 179 self.source = self.service.root 180 return await self._update(reports, None, disable_post)
Async updates report data by fetching and parsing the report
319def make_year_timestamp( 320 value: str, 321 repr: str, # pylint: disable=redefined-builtin 322 tzname: Optional[str] = None, 323) -> Union[Timestamp, Code, None]: 324 """Convert NOTAM timestamp which includes year and month""" 325 values = value.strip().split() 326 if not values: 327 return None 328 value = values[0] 329 if code := CODES.get(value): 330 return Code(value, code) 331 tz = _tz_offset_for(tzname) or timezone.utc 332 raw = datetime.strptime(value[:10], r"%y%m%d%H%M") 333 date = datetime(raw.year, raw.month, raw.day, raw.hour, raw.minute, tzinfo=tz) 334 return Timestamp(repr, date)
Convert NOTAM timestamp which includes year and month
337def parse_linked_times( 338 start: str, end: str 339) -> Tuple[Union[Timestamp, Code, None], Union[Timestamp, Code, None]]: 340 """Parse start and end times sharing any found timezone""" 341 start, end = start.strip(), end.strip() 342 start_raw, end_raw, tzname = start, end, None 343 if len(start) > 10: 344 start, tzname = start[:-3], start[-3:] 345 if len(end) > 10: 346 end, tzname = end[:-3], end[-3:] 347 return make_year_timestamp(start, start_raw, tzname), make_year_timestamp( 348 end, end_raw, tzname 349 )
Parse start and end times sharing any found timezone
352def make_altitude(value: Optional[str], units: Units) -> Optional[Number]: 353 """Parse NOTAM altitudes""" 354 if not value: 355 return None 356 if trimmed := value.split()[0].strip(" ."): 357 if trimmed in SPECIAL_NUMBERS or trimmed[0].isdigit(): 358 return core.make_altitude(trimmed, units, repr=value)[0] 359 return None
Parse NOTAM altitudes
362def parse(report: str, issued: Optional[Timestamp] = None) -> Tuple[NotamData, Units]: 363 """Parse NOTAM report string""" 364 # pylint: disable=too-many-locals 365 units = Units.international() 366 sanitized = sanitize(report) 367 qualifiers, station, start_time, end_time = None, None, None, None 368 body, number, replaces, report_type = "", None, None, None 369 schedule, lower, upper, text = None, None, None, sanitized 370 match = ALL_KEYS_PATTERN.search(text) 371 # Type and number here 372 if match and match.start() > 0: 373 number, report_type, replaces = _header(text[: match.start()]) 374 start_text, end_text = "", "" 375 while match: 376 tag = match.group()[0] 377 text = text[match.end() :] 378 try: 379 match = KEY_PATTERNS[tag].search(text) 380 except KeyError: 381 match = None 382 item = (text[: match.start()] if match else text).strip() 383 if tag == "Q": 384 qualifiers = _qualifiers(item, units) 385 elif tag == "A": 386 station = item 387 elif tag == "B": 388 start_text = item 389 elif tag == "C": 390 end_text = item 391 elif tag == "D": 392 schedule = item 393 elif tag == "E": 394 body = item 395 elif tag == "F": 396 lower = make_altitude(item, units) 397 elif tag == "G": 398 upper = make_altitude(item, units) 399 start_time, end_time = parse_linked_times(start_text, end_text) 400 return ( 401 NotamData( 402 raw=report, 403 sanitized=sanitized, 404 station=station, 405 time=issued, 406 remarks=None, 407 number=number, 408 replaces=replaces, 409 type=report_type, 410 qualifiers=qualifiers, 411 start_time=start_time, 412 end_time=end_time, 413 schedule=schedule, 414 body=body, 415 lower=lower, 416 upper=upper, 417 ), 418 units, 419 )
Parse NOTAM report string
422def sanitize(report: str) -> str: 423 """Retuns a sanitized report ready for parsing""" 424 return report.replace("\r", "").strip()
Retuns a sanitized report ready for parsing