From 0cec41b264e886f10f3886a2b6b1870555209932 Mon Sep 17 00:00:00 2001 From: Christian Grovdal Date: Fri, 3 Jan 2025 23:48:06 +0100 Subject: [PATCH 1/2] Issue #184: Change to pyproject.toml --- .github/workflows/ci.yml | 21 +++++++++-------- README.md | 14 ++++++----- pyproject.toml | 48 ++++++++++++++++++++++++++++++++++++++ requirements-typing.txt | 1 - setup.cfg | 7 ------ setup.py | 50 ---------------------------------------- tox.ini | 2 +- 7 files changed, 69 insertions(+), 74 deletions(-) create mode 100644 pyproject.toml delete mode 100644 requirements-typing.txt delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28a4539e..5c388a4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,8 @@ jobs: - '3.9' - '3.10' - '3.11' - - '3.12-dev' + - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -19,15 +20,17 @@ jobs: 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: @@ -35,13 +38,13 @@ jobs: 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 diff --git a/README.md b/README.md index 6bbce44a..9a1f9348 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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/.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/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 -------------------- @@ -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: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..4c9f9485 --- /dev/null +++ b/pyproject.toml @@ -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" +] diff --git a/requirements-typing.txt b/requirements-typing.txt deleted file mode 100644 index a45e2c90..00000000 --- a/requirements-typing.txt +++ /dev/null @@ -1 +0,0 @@ -mypy==1.3.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 9f1142cd..00000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[aliases] -test = pytest - -[tool:pytest] -pep8ignore = - test/test_*.py E241 E131 -pep8maxlinelength = 90 diff --git a/setup.py b/setup.py deleted file mode 100755 index d660fc5b..00000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Setup script for the metar package. - - Usage: python setup.py install -""" -from setuptools import setup -from metar import __version__ - -DESCRIPTION = "Metar - a package to parse METAR-coded weather reports" - -LONG_DESCRIPTION = """ -Metar is a python package for interpreting METAR and SPECI weather reports. - -METAR is an international format for reporting weather observations. -The standard specification for the METAR and SPECI codes is given -in the WMO Manual on Codes, vol I.1, Part A (WMO-306 I.i.A). US -conventions for METAR/SPECI reports are described in chapter 12 of -the Federal Meteorological Handbook No.1. (FMC-H1-2017), issued by -NOAA. See http://www.ofcm.gov/publications/fmh/FMH1/FMH1.pdf - -This module extracts the data recorded in the main-body groups of -reports that follow the WMO spec or the US conventions, except for -the runway state and trend groups, which are parsed but ignored. -The most useful remark groups defined in the US spec are parsed, -as well, such as the cumulative precipitation, min/max temperature, -peak wind and sea-level pressure groups. No other regional conventions -are formally supported, but a large number of variant formats found -in international reports are accepted.""" - -setup( - name="metar", - version=__version__, - author="Tom Pollard", - author_email="pollard@alum.mit.edu", - url="https://github.com/python-metar/python-metar", - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - license="BSD", - packages=["metar"], - package_data={"metar": ["nsd_cccc.txt", "py.typed", "*.pyi"]}, - platforms="Python 2.5 and later.", - extras_require={"test": ["pytest"]}, - 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", - ], -) diff --git a/tox.ini b/tox.ini index 43901bf6..14080fdc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] downloadcache = {toxworkdir}/_download/ -envlist = py36, py35, py34, py33, py27 +envlist = py38, py39, py310, py311, py312, py313 [testenv] deps = pytest From 193a33d4b914802e0294255b6b24e2cd0e498f9f Mon Sep 17 00:00:00 2001 From: Christian Grovdal Date: Fri, 3 Jan 2025 21:56:06 +0100 Subject: [PATCH 2/2] Return timezone aware datetime objects, returning to 1.10.0 behavior This relates to the fix for issue #175, commit 7dfbe07, which accidentally changes the datetime objects from python-metar from timezone aware to non-timezone-aware. A new field ._zulu is added to distinguish whether the Z char is used as suffix of the timestamp in the METAR, but the timestamp is always assumed to be in UTC. --- metar/Metar.py | 41 ++++++++++++++++++++++------------------- test/test_metar.py | 11 +++++++++++ 2 files changed, 33 insertions(+), 19 deletions(-) mode change 100644 => 100755 metar/Metar.py mode change 100644 => 100755 test/test_metar.py diff --git a/metar/Metar.py b/metar/Metar.py old mode 100644 new mode 100755 index 3859417b..a00524f6 --- a/metar/Metar.py +++ b/metar/Metar.py @@ -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 @@ -40,7 +40,7 @@ class ParserError(Exception): TIME_RE = re.compile( r"""^(?P\d\d) (?P\d\d) - (?P\d\d)Z?\s+""", + (?P\d\d)(?PZ?)\s+""", re.VERBOSE, ) MODIFIER_RE = re.compile(r"^(?PAUTO|COR AUTO|FINO|NIL|TEST|CORR?|RTD|CC[A-G])\s+") @@ -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. @@ -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 + self._now = datetime.now(timezone.utc) self._month = month self._year = year @@ -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: @@ -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 @@ -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) @@ -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)" diff --git a/test/test_metar.py b/test/test_metar.py old mode 100644 new mode 100755 index c68dc104..e3063f55 --- a/test/test_metar.py +++ b/test/test_metar.py @@ -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: @@ -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."""