diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9fc916c..ac679da 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,8 +47,14 @@ jobs: pip install --requirement=requirements-test.txt pip install --editable=.[service] - - name: Run tests - run: pytest + - name: Run linters + if: matrix.python-version != '3.6' && matrix.python-version != '3.7' + run: | + poe lint + + - name: Run software tests + run: | + poe test - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/apicast/__init__.py b/apicast/__init__.py index 47e6780..e737040 100644 --- a/apicast/__init__.py +++ b/apicast/__init__.py @@ -1,3 +1,4 @@ """Access bee flight forecast information published by Deutscher Wetterdienst (DWD)""" + __appname__ = "apicast" __version__ = "0.8.6" diff --git a/apicast/api.py b/apicast/api.py index 329b78f..1e3ce2f 100644 --- a/apicast/api.py +++ b/apicast/api.py @@ -2,14 +2,19 @@ # (c) 2020-2021 Andreas Motl # License: GNU Affero General Public License, Version 3 import logging -from typing import List, Dict +from typing import Dict, List from fastapi import FastAPI, Query from fastapi.responses import HTMLResponse, PlainTextResponse from apicast import __appname__, __version__ -from apicast.core import (DwdBeeflightForecast, dwd_copyright, dwd_source, - producer_link, producer_name) +from apicast.core import ( + DwdBeeflightForecast, + dwd_copyright, + dwd_source, + producer_link, + producer_name, +) from apicast.format import Formatter app = FastAPI() @@ -21,9 +26,11 @@ @app.get("/", response_class=HTMLResponse) def index(): - appname = f"{__appname__} {__version__}" - description = "Apicast acquires bee flight forecast information published by Deutscher Wetterdienst (DWD)." + description = ( + "Apicast acquires bee flight forecast information " + "published by Deutscher Wetterdienst (DWD)." + ) data_index_items = [] for location in dbf.get_station_slugs(): @@ -44,7 +51,7 @@ def index():
- """ + """ # noqa: E501 data_index_items.append(item) data_index_items_html = "\n".join(data_index_items) @@ -105,12 +112,12 @@ def index(): - """ + """ # noqa: E501 @app.get("/robots.txt", response_class=PlainTextResponse) def robots(): - return f""" + return """ User-agent: * Disallow: /beeflight/ """.strip() @@ -135,7 +142,6 @@ def beeflight_forecast_by_slug( format: str = Query(default="json"), translate: bool = Query(default=False), ): - station_slug = f"{state}/{station}" try: @@ -164,7 +170,7 @@ def beeflight_forecast_by_slug( def make_json_response(data: List[Dict], location: str = None): - response = { + return { "meta": { "source": dwd_source, "producer": f"{producer_name} - {producer_link}", @@ -175,7 +181,6 @@ def make_json_response(data: List[Dict], location: str = None): }, "data": data, } - return response def start_service(listen_address, reload: bool = False): diff --git a/apicast/cli.py b/apicast/cli.py index fc46bd9..267fc53 100644 --- a/apicast/cli.py +++ b/apicast/cli.py @@ -58,7 +58,7 @@ def run(): # Start HTTP service with dynamic code reloading apicast service --reload - """ + """ # noqa: E501 name = f"{__appname__} {__version__}" @@ -89,7 +89,6 @@ def run(): # Run command. if options.stations: - if options.slugs: result = dbf.get_station_slugs() print("\n".join(result)) @@ -106,7 +105,6 @@ def run(): def format_beeflight_forecast(result, format="json"): - if not result.data: raise ValueError("No data found or unable to parse") diff --git a/apicast/core.py b/apicast/core.py index 375f83f..c8b1813 100644 --- a/apicast/core.py +++ b/apicast/core.py @@ -9,6 +9,7 @@ See also https://community.hiveeyes.org/t/dwd-prognose-bienenflug/787 """ + import dataclasses from typing import List @@ -20,7 +21,6 @@ from apicast import __appname__, __version__ - user_agent = f"{__appname__}/{__version__}" dwd_source = "https://www.dwd.de/DE/leistungen/biene_flug/bienenflug.html" dwd_copyright = "© Deutscher Wetterdienst (DWD), Agricultural Meteorology Department" @@ -54,7 +54,6 @@ def copy(self): class DwdBeeflightForecast: - baseurl = "https://www.dwd.de/DE/leistungen/biene_flug/bienenflug.json?cl2Categories_LeistungsId=bienenflug" session = requests.Session() @@ -62,7 +61,6 @@ class DwdBeeflightForecast: @ttl_cache(60 * 60 * 24) def get_states(self) -> List[State]: - states: List[State] = [] # Request federal states. @@ -75,18 +73,16 @@ def get_states(self) -> List[State]: states.append(state) - # sites.sort(key=operator.attrgetter("label")) + # sites.sort(key=operator.attrgetter("label")) # noqa: ERA001 return states @ttl_cache(60 * 60 * 24) def get_stations(self) -> List[Station]: - stations: List[Station] = [] # Request federal states. for state in self.get_states(): - # Request sites. response = self.session.get( self.baseurl, @@ -111,7 +107,7 @@ def get_stations(self) -> List[Station]: stations.append(station) - # sites.sort(key=operator.attrgetter("label")) + # sites.sort(key=operator.attrgetter("label")) # noqa: ERA001 return stations @@ -130,7 +126,6 @@ def get_station_by_slug(self, slug): @ttl_cache(60 * 60) def get_data(self, station: Station) -> Result: - response = self.session.get( self.baseurl, params={ @@ -155,10 +150,7 @@ def get_data(self, station: Station) -> Result: data = self.parse_html_table(table) # Ready. - result = Result( - station=station, station_name=station_name, data=data, footnote=footnote - ) - return result + return Result(station=station, station_name=station_name, data=data, footnote=footnote) @staticmethod def parse_html_table(html): diff --git a/apicast/format.py b/apicast/format.py index b03a111..7d236d5 100644 --- a/apicast/format.py +++ b/apicast/format.py @@ -48,10 +48,10 @@ class Formatter: def __init__(self, result): self.result = deepcopy(result) self.data = self.result.data - self.title = u"### Prognose des Bienenfluges in {}".format(self.result.station_name) + self.title = "### Prognose des Bienenfluges in {}".format(self.result.station_name) def translate(self): - self.title = u"### Beeflight forecast for {}".format(self.result.station_name) + self.title = "### Beeflight forecast for {}".format(self.result.station_name) for item in self.data: for index, slot in enumerate(item): for key, value in self.LABEL_MAP.items(): @@ -79,9 +79,9 @@ def machinify(self): item[key] = value return data + # ruff: noqa: T201 def table_markdown(self): with io.StringIO() as buffer, redirect_stdout(buffer): - # Report about weather station / observation location print(self.title) print() diff --git a/apicast/util.py b/apicast/util.py index 4670b5f..6aacfb1 100644 --- a/apicast/util.py +++ b/apicast/util.py @@ -32,7 +32,6 @@ def configure_http_logging(options): def normalize_options(options): normalized = {} for key, value in options.items(): - # Add primary variant. key = key.strip("--<>") normalized[key] = value diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..69f45b7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,94 @@ +[tool.pytest.ini_options] +addopts = """ + -ra -q --verbosity=3 + --cov --cov-report=term-missing --cov-report=xml +""" +minversion = "2.0" +log_level = "DEBUG" +log_cli_level = "DEBUG" +testpaths = [ + "apicast", + "test", +] +xfail_strict = true + +[tool.coverage.run] +source = ["apicast"] + +[tool.coverage.report] +show_missing = true +fail_under = 0 +omit = [ + "test/*", +] + +[tool.ruff] +line-length = 100 +extend-exclude = [ +] + +[tool.ruff.lint] +select = [ + # Pycodestyle + "E", + "W", + # Pyflakes + "F", + # isort + "I", + # Bandit + "S", + # flake8-quotes + "Q", + # eradicate + "ERA", + # flake8-2020 + "YTT", + # print + "T20", + # return + "RET", + # pyupgrade + # "UP", + # flake8-commas + "COM", + # future-annotations + # "FA", + # flake8-type-checking + "TCH", + # flake8-unused-arguments + "ARG", + # flake8-use-pathlib + # "PTH" +] +extend-ignore = [ + # Unnecessary `elif` after `return` or `raise` statement. + "RET505", + "RET506", + # No trailing commas. + "COM812" +] +unfixable = ["ERA", "F401", "F841", "T20", "ERA001"] + +[tool.ruff.lint.per-file-ignores] +"apicast/cli.py" = ["T201"] +"test/*" = ["S101"] + + +# =================== +# Tasks configuration +# =================== + +[tool.poe.tasks] +format = [ + {cmd="ruff format"}, + {cmd="ruff check --fix"}, +] +lint = [ + {cmd="ruff check"}, +] +test = [ + {cmd="pytest"}, +] +build = {cmd="python -m build"} +check = ["lint", "test"] diff --git a/requirements-test.txt b/requirements-test.txt index 4dc0af4..c084824 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,7 @@ datadiff>=2.0,<3 fastapi[test] marko<3 +poethepoet<0.26 pytest>=6.1.0,<8 pytest-cov<6 +ruff<0.4;python_version>='3.7' diff --git a/setup.py b/setup.py index fcdd145..e0fa995 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import os from io import open -from setuptools import setup, find_packages + +from setuptools import find_packages, setup here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, "README.rst"), encoding="UTF-8").read() @@ -10,8 +11,8 @@ name="apicast", version="0.8.6", description="Python client and HTTP service to access bee flight forecast " - "information published by Deutscher Wetterdienst (DWD), the " - "federal meteorological service in Germany.", + "information published by Deutscher Wetterdienst (DWD), the " + "federal meteorological service in Germany.", long_description=README, license="AGPL 3, EUPL 1.2", classifiers=[ diff --git a/test/test_api.py b/test/test_api.py index 7bb2532..40926ea 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -2,7 +2,6 @@ from apicast.api import app - client = TestClient(app) diff --git a/test/test_data.py b/test/test_data.py index 2b198d9..55bbb60 100644 --- a/test/test_data.py +++ b/test/test_data.py @@ -3,18 +3,15 @@ import pytest from datadiff.tools import assert_equal -from apicast.core import DwdBeeflightForecast, Station, State +from apicast.core import DwdBeeflightForecast, State, Station dbf = DwdBeeflightForecast() @pytest.mark.data def test_get_data_success(): - stations = dbf.get_stations() - bavaria_hof = [ - station for station in stations if station.identifier == "bifl_0042" - ][0] + bavaria_hof = [station for station in stations if station.identifier == "bifl_0042"][0] data_hof = dbf.get_data(station=bavaria_hof) @@ -47,10 +44,10 @@ def test_get_data_success(): @pytest.mark.data def test_get_data_invalid_station(): station = Station( - state=State(label='Nordrhein-Westfalen', identifier='bifl_bl999'), - label='Bielefeld', - identifier='bifl_bl999', - slug='nordrhein-westfalen/bielefeld', + state=State(label="Nordrhein-Westfalen", identifier="bifl_bl999"), + label="Bielefeld", + identifier="bifl_bl999", + slug="nordrhein-westfalen/bielefeld", ) with pytest.raises(ValueError) as ex: dbf.get_data(station=station) @@ -59,11 +56,8 @@ def test_get_data_invalid_station(): @pytest.mark.data def test_get_data_copy(): - stations = dbf.get_stations() - bavaria_hof = [ - station for station in stations if station.identifier == "bifl_0042" - ][0] + bavaria_hof = [station for station in stations if station.identifier == "bifl_0042"][0] data_hof = dbf.get_data(station=bavaria_hof) data_hof_copy = data_hof.copy() diff --git a/test/test_format.py b/test/test_format.py index f8d79d4..057ae77 100644 --- a/test/test_format.py +++ b/test/test_format.py @@ -1,20 +1,23 @@ import pytest -from apicast.core import Result, Station, State +from apicast.core import Result, State, Station from apicast.format import Formatter - sample_result = Result( station=Station( - state=State(label='Brandenburg', identifier='bifl_bl04'), label='Potsdam', identifier='bifl_0021', - slug='brandenburg/potsdam'), station_name='Potsdam', + state=State(label="Brandenburg", identifier="bifl_bl04"), + label="Potsdam", + identifier="bifl_0021", + slug="brandenburg/potsdam", + ), + station_name="Potsdam", data=[ - ['Datum', 'morgens', 'mittags', 'abends'], - ['Sa 30.03.', 'mittel', 'sehr hoch', 'hoch'], - ['So 31.03.', 'mittel', 'hoch', 'hoch'], - ['Mo 01.04.', 'mittel', 'hoch', 'mittel'], + ["Datum", "morgens", "mittags", "abends"], + ["Sa 30.03.", "mittel", "sehr hoch", "hoch"], + ["So 31.03.", "mittel", "hoch", "hoch"], + ["Mo 01.04.", "mittel", "hoch", "mittel"], ], - footnote='© Deutscher Wetterdienst, erstellt 30.03.2024 05:00 UTC. Alle Angaben ohne Gewähr!', + footnote="© Deutscher Wetterdienst, erstellt 30.03.2024 05:00 UTC. Alle Angaben ohne Gewähr!", ) @@ -30,21 +33,28 @@ def test_format_basic(formatter: Formatter): def test_format_normalize(formatter: Formatter): assert formatter.normalize() == [ - {'Datum': 'Sa 30.03.', 'morgens': 'mittel', 'mittags': 'sehr hoch', 'abends': 'hoch'}, - {'Datum': 'So 31.03.', 'morgens': 'mittel', 'mittags': 'hoch', 'abends': 'hoch'}, - {'Datum': 'Mo 01.04.', 'morgens': 'mittel', 'mittags': 'hoch', 'abends': 'mittel'}] + {"Datum": "Sa 30.03.", "morgens": "mittel", "mittags": "sehr hoch", "abends": "hoch"}, + {"Datum": "So 31.03.", "morgens": "mittel", "mittags": "hoch", "abends": "hoch"}, + {"Datum": "Mo 01.04.", "morgens": "mittel", "mittags": "hoch", "abends": "mittel"}, + ] def test_format_machinify(formatter: Formatter): - assert formatter.machinify() == [{'date': '2024-03-30', 'evening': 3, 'morning': 2, 'noon': 4}, - {'date': '2024-03-31', 'evening': 3, 'morning': 2, 'noon': 3}, - {'date': '2024-04-01', 'evening': 2, 'morning': 2, 'noon': 3}] + assert formatter.machinify() == [ + {"date": "2024-03-30", "evening": 3, "morning": 2, "noon": 4}, + {"date": "2024-03-31", "evening": 3, "morning": 2, "noon": 3}, + {"date": "2024-04-01", "evening": 2, "morning": 2, "noon": 3}, + ] def test_format_translate(formatter: Formatter): formatter.translate() - assert formatter.data == [['date', 'morning', 'noon', 'evening'], ['Sa 30.03.', 'medium', 'intensive', 'strong'], - ['So 31.03.', 'medium', 'strong', 'strong'], ['Mo 01.04.', 'medium', 'strong', 'medium']] + assert formatter.data == [ + ["date", "morning", "noon", "evening"], + ["Sa 30.03.", "medium", "intensive", "strong"], + ["So 31.03.", "medium", "strong", "strong"], + ["Mo 01.04.", "medium", "strong", "medium"], + ] def test_format_markdown(formatter: Formatter): diff --git a/test/test_locations.py b/test/test_locations.py index b213a0c..7b2c29d 100644 --- a/test/test_locations.py +++ b/test/test_locations.py @@ -3,13 +3,12 @@ import pytest from datadiff.tools import assert_equal -from apicast.core import DwdBeeflightForecast, Station, State +from apicast.core import DwdBeeflightForecast, State, Station dbf = DwdBeeflightForecast() def test_get_states(): - states = dbf.get_states() bavaria = [state for state in states if state.identifier == "bifl_bl02"][0] @@ -23,14 +22,9 @@ def test_get_states(): def test_get_stations(): - stations = dbf.get_stations() - bavaria_hof = [ - station for station in stations if station.identifier == "bifl_0042" - ][0] - berlin_tempelhof = [ - station for station in stations if station.identifier == "bifl_0019" - ][0] + bavaria_hof = [station for station in stations if station.identifier == "bifl_0042"][0] + berlin_tempelhof = [station for station in stations if station.identifier == "bifl_0019"][0] assert_equal( dataclasses.asdict(bavaria_hof), @@ -63,10 +57,10 @@ def test_get_station_by_slug_success(): station = dbf.get_station_by_slug("brandenburg/potsdam") reference = Station( - state=State(label='Brandenburg', identifier='bifl_bl04'), - label='Potsdam', - identifier='bifl_0021', - slug='brandenburg/potsdam', + state=State(label="Brandenburg", identifier="bifl_bl04"), + label="Potsdam", + identifier="bifl_0021", + slug="brandenburg/potsdam", ) assert station == reference