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

Update methods are temporarily deprecated until non-auth source can be found. 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))
Notams(code: str | None = None, coord: avwx.structs.Coord | None = None)
147    def __init__(self, code: str | None = None, coord: Coord | None = None):
148        super().__init__(code, coord)
149        # self.service = FaaNotam("notam")
data: list[avwx.structs.NotamData] | None = None
radius: int = 10
@staticmethod
def sanitize(report: str) -> str:
173    @staticmethod
174    def sanitize(report: str) -> str:
175        """Sanitize a NOTAM string."""
176        return sanitize(report)

Sanitize a NOTAM string.

def update(self, timeout: int = 10, *, disable_post: bool = False) -> bool:
179    def update(self, timeout: int = 10, *, disable_post: bool = False) -> bool:
180        raise NotImplementedError(_DEP_MSG)

Update report data by fetching and parsing the report.

Returns True if new reports are available, else False

async def async_update(self, timeout: int = 10, *, disable_post: bool = False) -> bool:
183    async def async_update(self, timeout: int = 10, *, disable_post: bool = False) -> bool:
184        """Async updates report data by fetching and parsing the report."""
185        raise NotImplementedError(_DEP_MSG)
186        # reports = await self.service.async_fetch(  # type: ignore
187        #     icao=self.code, coord=self.coord, radius=self.radius, timeout=timeout
188        # )
189        # self.source = self.service.root
190        # return await self._update(reports, None, disable_post=disable_post)

Async updates report data by fetching and parsing the report.

ALL_KEYS_PATTERN = re.compile('\\b[A-GQ]\\) ')
KEY_PATTERNS = {'Q': re.compile('\\b[A-G]\\) '), 'A': re.compile('\\b[B-G]\\) '), 'B': re.compile('\\b[C-G]\\) '), 'C': re.compile('\\b[D-G]\\) '), 'D': re.compile('\\b[E-G]\\) '), 'E': re.compile('\\b[FG]\\) '), 'F': re.compile('\\bG\\) ')}
def make_year_timestamp( value: str, repr: str, tzname: str | None = None) -> avwx.structs.Timestamp | avwx.structs.Code | None:
328def make_year_timestamp(
329    value: str,
330    repr: str,  # noqa: A002
331    tzname: str | None = None,
332) -> Timestamp | Code | None:
333    """Convert NOTAM timestamp which includes year and month."""
334    values = value.strip().split()
335    if not values:
336        return None
337    value = values[0]
338    if code := CODES.get(value):
339        return Code(value, code)
340    tz = _tz_offset_for(tzname) or timezone.utc
341    raw = datetime.strptime(value[:10], r"%y%m%d%H%M")  # noqa: DTZ007
342    date = datetime(raw.year, raw.month, raw.day, raw.hour, raw.minute, tzinfo=tz)
343    return Timestamp(repr, date)

Convert NOTAM timestamp which includes year and month.

def parse_linked_times( start: str, end: str) -> tuple[avwx.structs.Timestamp | avwx.structs.Code | None, avwx.structs.Timestamp | avwx.structs.Code | None]:
346def parse_linked_times(start: str, end: str) -> tuple[Timestamp | Code | None, Timestamp | Code | None]:
347    """Parse start and end times sharing any found timezone."""
348    start, end = start.strip(), end.strip()
349    start_raw, end_raw, tzname = start, end, None
350    if len(start) > 10:
351        start, tzname = start[:-3], start[-3:]
352    if len(end) > 10:
353        end, tzname = end[:-3], end[-3:]
354    return make_year_timestamp(start, start_raw, tzname), make_year_timestamp(end, end_raw, tzname)

Parse start and end times sharing any found timezone.

def make_altitude( value: str | None, units: avwx.structs.Units) -> avwx.structs.Number | None:
357def make_altitude(value: str | None, units: Units) -> Number | None:
358    """Parse NOTAM altitudes."""
359    if not value:
360        return None
361    if trimmed := value.split()[0].strip(" ."):
362        if "(" in trimmed:
363            trimmed = trimmed[trimmed.find("(") + 1 :]
364        if trimmed in SPECIAL_NUMBERS or trimmed[0].isdigit():
365            return core.make_altitude(trimmed, units, repr=value)[0]
366    return None

Parse NOTAM altitudes.

def parse( report: str, issued: avwx.structs.Timestamp | None = None) -> tuple[avwx.structs.NotamData, avwx.structs.Units]:
369def parse(report: str, issued: Timestamp | None = None) -> tuple[NotamData, Units]:
370    """Parse NOTAM report string."""
371    units = Units.international()
372    sanitized = sanitize(report)
373    qualifiers, station, start_time, end_time = None, None, None, None
374    body, number, replaces, report_type = "", None, None, None
375    schedule, lower, upper, text = None, None, None, sanitized
376    match = ALL_KEYS_PATTERN.search(text)
377    # Type and number here
378    if match and match.start() > 0:
379        number, report_type, replaces = _header(text[: match.start()])
380    start_text, end_text = "", ""
381    while match:
382        tag = match.group()[0]
383        text = text[match.end() :]
384        try:
385            match = KEY_PATTERNS[tag].search(text)
386        except KeyError:
387            match = None
388        item = (text[: match.start()] if match else text).strip()
389        if tag == "Q":
390            qualifiers = _qualifiers(item, units)
391        elif tag == "A":
392            station = item
393        elif tag == "B":
394            start_text = item
395        elif tag == "C":
396            end_text = item
397        elif tag == "D":
398            schedule = item
399        elif tag == "E":
400            body = item
401        elif tag == "F":
402            lower = make_altitude(item, units)
403        elif tag == "G":
404            upper = make_altitude(item, units)
405    start_time, end_time = parse_linked_times(start_text, end_text)
406    return (
407        NotamData(
408            raw=report,
409            sanitized=sanitized,
410            station=station,
411            time=issued,
412            remarks=None,
413            number=number,
414            replaces=replaces,
415            type=report_type,
416            qualifiers=qualifiers,
417            start_time=start_time,
418            end_time=end_time,
419            schedule=schedule,
420            body=body,
421            lower=lower,
422            upper=upper,
423        ),
424        units,
425    )

Parse NOTAM report string.

def sanitize(report: str) -> str:
428def sanitize(report: str) -> str:
429    """Retun a sanitized report ready for parsing."""
430    return report.replace("\r", "").strip()

Retun a sanitized report ready for parsing.