10000 #175 Restore timezone aware datetime objects when time specified with Z (zulu) by chrgro · Pull Request #186 · python-metar/python-metar · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,40 @@ jobs:
- '3.9'
- '3.10'
- '3.11'
- '3.12-dev'
- '3.12'
- '3.13'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Package
run: |
python -m pip install --upgrade setuptools setuptools_scm wheel
python -m pip install .
python -m pip install .[tests]
- name: Run Tests
run: |
python -m pip install pytest pytest-runner pytest-cov pytest-runner
python setup.py test --addopts " --cov=metar"
if [[ ${{ matrix.python-version }} == "3.8" ]]; then
pip install codecov
if [[ ${{ matrix.python-version }} == "3.13" ]]; then
# For one of the python versions, check code coverage during pytest
pip install .[codecov]
python -m pytest --cov metar
codecov
else
# For all other python versions, just run pytest
python -m pytest
fi

typing:
name: Typing Check
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11"]
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -r requirements-typing.txt
run: python -m pip install .[typing]
- name: Run mypy
run: mypy metar
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Python-Metar
============

Python-metar is a python package for interpreting METAR and SPECI coded
weather reports.
weather reports.

METAR and SPECI are coded aviation weather reports. The official
coding schemes are specified in the World Meteorological Organization
Expand Down Expand Up @@ -35,16 +35,16 @@ The current METAR report for a given airport is available at the URL

http://tgftp.nws.noaa.gov/data/observations/metar/stations/<station>.TXT

where `station` is the four-letter ICAO airport station code. The
where `station` is the four-letter ICAO airport station code. The
accompanying script get_report.py will download and decode the
current report for any specified station.
current report for any specified station.

The METAR reports for all stations (worldwide) for any "cycle" (i.e., hour)
The METAR reports for all stations (worldwide) for any "cycle" (i.e., hour)
in the last 24 hours are available in a single file at the URL

http://tgftp.nws.noaa.gov/data/observations/metar/cycles/<cycle>Z.TXT

where `cycle` is a 2-digit cycle number (`00` thru `23`).
where `cycle` is a 2-digit cycle number (`00` thru `23`).

METAR specifications
--------------------
Expand Down Expand Up @@ -134,7 +134,9 @@ METAR: METAR KEWR 111851Z VRB03G19KT 2SM R04R/3000VP6000FT TSRA BR FEW015 BKN040
Tests
------------------------------------------------------------------------

The library is tested against Python 3.7-3.10. A [tox](https://tox.readthedocs.io/en/latest/)
The library is tested against Python 3.8-3.13.

A [tox](https://tox.readthedocs.io/en/latest/)
configuration file is included to easily run tests against each of these
environments. To run tests against all environments, install tox and run:

Expand Down
41 changes: 22 additions & 19 deletions metar/Metar.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
A Metar object represents the weather report encoded by a single METAR code.
"""
import re
import datetime
from datetime import datetime, timezone, timedelta
import warnings
import logging

Expand Down Expand Up @@ -40,7 +40,7 @@ class ParserError(Exception):
TIME_RE = re.compile(
r"""^(?P<day>\d\d)
(?P<hour>\d\d)
(?P<min>\d\d)Z?\s+""",
(?P<min>\d\d)(?P<zulu>Z?)\s+""",
re.VERBOSE,
)
MODIFIER_RE = re.compile(r"^(?P<mod>AUTO|COR AUTO|FINO|NIL|TEST|CORR?|RTD|CC[A-G])\s+")
Expand Down Expand Up @@ -362,8 +362,8 @@ def __init__(self, metarcode, month=None, year=None, utcdelta=None, strict=True)
month, year : int, optional
Date values to be used when parsing a non-current METAR code. If not
provided, then the month and year are guessed from the current date.
utcdelta : int or datetime.timedelta, optional
An int of hours or a timedelta object used to specify the timezone.
utcdelta : any
Deprecated, not currently nor previously in use
strict : bool (default is True)
This option determines if a ``ParserError`` is raised when
unparsable groups are found or an unexpected exception is encountered.
Expand Down Expand Up @@ -418,11 +418,7 @@ def __init__(self, metarcode, month=None, year=None, utcdelta=None, strict=True)
self._unparsed_groups = []
self._unparsed_remarks = []

self._now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
if utcdelta:
self._utcdelta = utcdelta
else:
self._utcdelta = datetime.datetime.now() - self._now
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what this self._utcdelta piece of code did. It does not seem to be referenced anywhere else in the codebase. I kept the argument to not break backwards compatibility, but removed the dead code.

self._now = datetime.now(timezone.utc)

self._month = month
self._year = year
Expand Down Expand Up @@ -579,6 +575,7 @@ def _handleTime(self, d):
_day [int]
_hour [int]
_min [int]
_zulu [bool]
"""
self._day = int(d["day"])
if not self._month:
Expand All @@ -596,8 +593,14 @@ def _handleTime(self, d):
self._year = self._year - 1
self._hour = int(d["hour"])
self._min = int(d["min"])
self.time = datetime.datetime(
self._year, self._month, self._day, self._hour, self._min
self._zulu = bool(d["zulu"])
self.time = datetime(
year = self._year,
month = self._month,
day = self._day,
hour = self._hour,
minute = self._min,
tzinfo = timezone.utc,
)
if self._min < 45:
self.cycle = self._hour
Expand Down Expand Up @@ -944,14 +947,14 @@ def _handlePeakWindRemark(self, d):
peak_hour = int(d["hour"])
else:
peak_hour = self._hour
self.peak_wind_time = datetime.datetime(
self._year, self._month, self._day, peak_hour, peak_min
self.peak_wind_time = datetime(
self._year, self._month, self._day, peak_hour, peak_min, tzinfo=self.time.tzinfo
)
if self.peak_wind_time > self.time:
if peak_hour > self._hour:
self.peak_wind_time -= datetime.timedelta(hours=24)
self.peak_wind_time -= timedelta(hours=24)
else:
self.peak_wind_time -= datetime.timedelta(hours=1)
self.peak_wind_time -= timedelta(hours=1)
self._remarks.append(
"peak wind %dkt from %d degrees at %d:%02d"
% (peak_speed, peak_dir, peak_hour, peak_min)
Expand All @@ -966,14 +969,14 @@ def _handleWindShiftRemark(self, d):
else:
wshft_hour = self._hour
wshft_min = int(d["min"])
self.wind_shift_time = datetime.datetime(
self._year, self._month, self._day, wshft_hour, wshft_min
self.wind_shift_time = datetime(
self._year, self._month, self._day, wshft_hour, wshft_min, tzinfo=self.time.tzinfo
)
if self.wind_shift_time > self.time:
if wshft_hour > self._hour:
self.wind_shift_time -= datetime.timedelta(hours=24)
self.wind_shift_time -= timedelta(hours=24)
else:
self.wind_shift_time -= datetime.timedelta(hours=1)
self.wind_shift_time -= timedelta(hours=1)
text = "wind shift at %d:%02d" % (wshft_hour, wshft_min)
if d["front"]:
text += " (front)"
Expand Down
48 changes: 48 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[project]
name = "python-metar"
version = "1.11.0"
description = "Metar - a package to parse METAR-coded weather reports"

authors = [
{"name"="Tom Pollard", email="pollard@alum.mit.edu"}
]
license = {file = "LICENSE"}

requires-python = ">= 3.8"
readme = "README.md"
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Intended Audience :: Science/Research",
"Topic :: Software Development :: Libraries :: Python Modules",
]

dependencies = []

[tool.setuptools]
packages = ["metar"]

[tool.setuptools.package-data]
metar = ["nsd_cccc.txt", "py.typed", "*.pyi"]

[project.urls]
Repository = "https://github.com/python-metar/python-metar/"

[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"

[project.optional-dependencies]
tests = [
"pytest"
]
codecov = [
"pytest-cov",
"pytest",
"codecov",
]
typing = [
"mypy==1.3.0"
]
1 change: 0 additions & 1 deletion requirements-typing.txt

This file was deleted.

7 changes: 0 additions & 7 deletions setup.cfg

This file was deleted.

50 changes: 0 additions & 50 deletions setup.py

This file was deleted.

11 changes: 11 additions & 0 deletions test/test_metar.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ def test_030_parseTime_legal():
assert report.time.day == 10
assert report.time.hour == 16
assert report.time.minute == 51
assert report.time.tzinfo == timezone.utc
assert report._zulu == True
if today.day > 10 or (today.hour > 16 and today.day == 10):
assert report.time.month == today.month
if today.month > 1 or today.day > 10:
Expand Down Expand Up @@ -242,6 +244,15 @@ def test_035_parseTime_suppress_auto_month():
else:
assert report.time.year == last_year

def test_036_parseTime_timezone_naive_times():
"""Check that timestamps without a trailing Z has zulu member set to false"""

report = Metar.Metar("KEWR 101651")
assert report.time.day == 10
assert report.time.hour == 16
assert report.time.minute == 51
assert report._zulu == False


def test_040_parseModifier_default():
"""Check default 'modifier' value."""
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
downloadcache = {toxworkdir}/_download/
envlist = py36, py35, py34, py33, py27
envlist = py38, py39, py310, py311, py312, py313

[testenv]
deps = pytest
Expand Down
Loading
0