diff --git a/.github/workflows/python-linting.yml b/.github/workflows/python-linting.yml index 7db8a3e..dbafc06 100644 --- a/.github/workflows/python-linting.yml +++ b/.github/workflows/python-linting.yml @@ -41,4 +41,11 @@ jobs: # Run our linting - name: Lint code run: | - ./bin/task lint \ No newline at end of file + ./bin/task lint + + # Testing + - name: Test code + env: + TNT_EX1_OPENWEATHERMAP_API_KEY: ${{ secrets.TNT_EX1_OPENWEATHERMAP_API_KEY}} + run: + ./bin/task test \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index 6777cfc..f6e3708 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -24,4 +24,13 @@ tasks: run: desc: Run the Weather Data Fetcher cmds: - - poetry run python weather_data_fetcher.py \ No newline at end of file + - poetry run python weather_data_fetcher.py + + test: + desc: Runs tests on the code + cmds: + - > + poetry run pytest + -s + --cov=. + --cov-report=html \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index c46acea..101ad1f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,20 @@ wrapt = [ {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, ] +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] + [[package]] name = "black" version = "22.8.0" @@ -74,6 +88,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "dill" version = "0.3.5.1" @@ -134,6 +162,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "isort" version = "5.10.1" @@ -172,6 +208,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + [[package]] name = "pathspec" version = "0.10.1" @@ -192,6 +239,26 @@ python-versions = ">=3.7" docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pycodestyle" version = "2.9.1" @@ -231,6 +298,52 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\"" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + [[package]] name = "python-dotenv" version = "0.21.0" @@ -308,10 +421,11 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "049e00e3bf16294bb7953c44a74f918bdbde1daeeb1aa79a4bb13427577ef793" +content-hash = "5483a2c6ca977391136be22b414c80702942590cb9801e45331ca6e0db308b76" [metadata.files] astroid = [] +attrs = [] black = [] certifi = [] charset-normalizer = [] @@ -323,6 +437,7 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] +coverage = [] dill = [ {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, @@ -331,6 +446,10 @@ flake8 = [] geographiclib = [] geopy = [] idna = [] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, @@ -379,14 +498,32 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] pathspec = [] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] pycodestyle = [] pyflakes = [] pylint = [] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [] +pytest-cov = [] python-dotenv = [] requests = [] tomli = [ diff --git a/pyproject.toml b/pyproject.toml index f4557d7..c29580c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ python-dotenv = "^0.21.0" pylint = "^2.15.2" black = "^22.8.0" flake8 = "^5.0.4" +pytest = "^7.1.3" +pytest-cov = "^4.0.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/test_weather_data_fetcher.py b/test_weather_data_fetcher.py new file mode 100644 index 0000000..3cc826c --- /dev/null +++ b/test_weather_data_fetcher.py @@ -0,0 +1,61 @@ +import os +import dotenv +from unittest import mock + +from weather_data_fetcher import fetch_location_coords, fetch_weather_data, get_api_key + + +class TestWeatherDataFetcher: + def test_fetch_coords(self): + coords_berlin = fetch_location_coords("Berlin") + coords_difference = [sum(x) for x in zip(coords_berlin, (-52.520008, -13.404954))] + assert sum(list(coords_difference)) < 0.01 + + @mock.patch("weather_data_fetcher.fetch_new_data", autospec=True) + def test_fetch_data(self, request_mock): + request_mock.return_value = { + "cod": "200", + "message": 0, + "cnt": 40, + "list": [ + { + "dt": 1665824400, + "main": { + "temp": 12.95, + "feels_like": 12.76, + "temp_min": 12.95, + "temp_max": 13.93, + "pressure": 1000, + "sea_level": 1000, + "grnd_level": 1004, + "humidity": 94, + "temp_kf": -0.98, + }, + "weather": [ + {"id": 803, "main": "Clouds", "description": "broken clouds", "icon": "04d"} + ], + "clouds": {"all": 81}, + "wind": {"speed": 1.55, "deg": 215, "gust": 3.1}, + "visibility": 10000, + "pop": 0, + "sys": {"pod": "d"}, + "dt_txt": "2022-10-15 09:00:00", + }, + ], + "city": { + "id": 7576815, + "name": "Alt-Kölln", + "coord": {"lat": 52.517, "lon": 13.3889}, + "country": "DE", + "population": 2000, + "timezone": 7200, + "sunrise": 1665811879, + "sunset": 1665850372, + }, + } + if "TNT_EX1_OPENWEATHERMAP_API_KEY" not in os.environ: + dotenv_file = dotenv.find_dotenv() + dotenv.load_dotenv(dotenv_file) + + fetch_weather_data(os.environ["TNT_EX1_OPENWEATHERMAP_API_KEY"], (52.520008, 13.404954)) + assert request_mock.called diff --git a/weather_data_fetcher.py b/weather_data_fetcher.py index cbf05c1..e455650 100644 --- a/weather_data_fetcher.py +++ b/weather_data_fetcher.py @@ -8,7 +8,7 @@ from geopy.geocoders import Nominatim -def fetch_weather_data(api_key: str, location_coords: Tuple[int, int]) -> None: +def fetch_weather_data(api_key: str, location_coords: Tuple[float, float]) -> None: """ Fetches weather data for 25 timepoints from the url if no recent data is available Stores the data in the folder where it is executed for future use. @@ -47,19 +47,7 @@ def fetch_weather_data(api_key: str, location_coords: Tuple[int, int]) -> None: fetch_data_from_url = True if not os.path.exists(file_path_json) or fetch_data_from_url: - print("Getting new data from URL", file=sys.stderr) - response = requests.get( - "https://api.openweathermap.org/data/2.5/forecast", - params={ - "lat": location_coords[0], - "lon": location_coords[1], - "dt": 25, - "units": "metric", - "appid": api_key, - }, - timeout=15, - ) - data = response.json() + data = fetch_new_data(api_key, location_coords) with open(file_path_json, "w", encoding="utf-8") as file: json.dump(data, file, ensure_ascii=False, indent=4) else: @@ -81,6 +69,25 @@ def fetch_weather_data(api_key: str, location_coords: Tuple[int, int]) -> None: print(output) +def fetch_new_data(api_key: str, location_coords: Tuple[float, float]) -> json: + """ + Uses the request module to fetch new data + """ + print("Getting new data from URL", file=sys.stderr) + response = requests.get( + "https://api.openweathermap.org/data/2.5/forecast", + params={ + "lat": location_coords[0], + "lon": location_coords[1], + "dt": 25, + "units": "metric", + "appid": api_key, + }, + timeout=15, + ) + return response.json() + + def fetch_location_coords(city: str) -> Tuple[int, int]: """ Gets the (lat, long) coordinates of a city name. If the city cannot be found