Skip to content

Commit d3ba5ec

Browse files
sheauradrianmo
andauthored
Add User-Agent (#8)
* Add User-Agent * Replace setup.py by pyproject.toml for dynamic package versioning. * update user-agent * Enhance tests to verify User-Agent header is set correctly in requests * update workflows * Update coverage publishing step to specify GitHub service --------- Co-authored-by: Adrian <adrian@morenomartinez.com>
1 parent 83f385e commit d3ba5ec

7 files changed

Lines changed: 91 additions & 51 deletions

File tree

.github/workflows/build_test.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,21 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
python -m pip install --upgrade pip
25-
pip install setuptools coveralls
25+
pip install coveralls
2626
pip install -r test-requirements.txt
2727
- name: Lint with flake8
2828
run: |
2929
# stop the build if there are Python syntax errors or undefined names
3030
flake8 meteoclimatic --count --max-complexity=10 --max-line-length=127 --statistics
3131
- name: Install
3232
run: |
33-
python setup.py install
33+
pip install -e .
3434
- name: Test with pytest
3535
run: |
3636
pytest --cov=meteoclimatic/ -v tests/
3737
- name: Publish Coverage
3838
env:
39+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3940
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
4041
run: |
41-
coveralls
42+
coveralls --service=github

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
python -m pip install --upgrade pip
25-
pip install setuptools wheel twine
25+
pip install build
2626
- name: Build
2727
run: |
28-
python setup.py sdist bdist_wheel
28+
python -m build
2929
- name: Publish package to PyPI
3030
uses: pypa/gh-action-pypi-publish@release/v1

meteoclimatic/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
__version__ = "0.1.1"
2+
13
from meteoclimatic.weather import Weather, Condition # noqa: F401
24
from meteoclimatic.station import Station # noqa: F401
35
from meteoclimatic.observation import Observation # noqa: F401

meteoclimatic/client.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from urllib.request import urlopen
1+
from urllib.request import Request, urlopen
22
from urllib.error import HTTPError
33
from bs4 import BeautifulSoup
44

55
from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound
66
from meteoclimatic import Observation
7+
from meteoclimatic import __version__
78

89

910
class MeteoclimaticClient(object):
@@ -16,14 +17,18 @@ class MeteoclimaticClient(object):
1617
def weather_at_station(self, station_code):
1718
url = self._base_url.format(station_code=station_code)
1819

20+
req = Request(url, headers={"User-Agent": f"pymeteoclimatic/{__version__}"})
21+
1922
try:
20-
parse_xml_url = urlopen(url)
23+
parse_xml_url = urlopen(req)
2124
except HTTPError as exc:
22-
raise MeteoclimaticError("Error fetching station data [status_code=%d]" %
23-
(exc.getcode(), )) from exc
25+
raise MeteoclimaticError(
26+
"Error fetching station data [status_code=%d]" % (exc.getcode(),)
27+
) from exc
2428

2529
xml_page = parse_xml_url.read()
2630
parse_xml_url.close()
31+
2732
soup_page = BeautifulSoup(xml_page, "xml")
2833
items = soup_page.findAll("item")
2934

pyproject.toml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "pymeteoclimatic"
7+
dynamic = ["version"]
8+
description = "A Python wrapper around the Meteoclimatic service"
9+
readme = "README.md"
10+
requires-python = ">=3.8"
11+
license = {text = "MIT"}
12+
authors = [
13+
{name = "Adrián Moreno", email = "adrian@morenomartinez.com"}
14+
]
15+
keywords = ["meteoclimatic", "client", "library", "api", "weather"]
16+
classifiers = [
17+
"License :: OSI Approved :: MIT License",
18+
"Programming Language :: Python",
19+
"Programming Language :: Python :: 3",
20+
"Programming Language :: Python :: 3 :: Only",
21+
"Programming Language :: Python :: 3.8",
22+
"Programming Language :: Python :: 3.9",
23+
"Programming Language :: Python :: 3.10",
24+
"Programming Language :: Python :: 3.11",
25+
"Programming Language :: Python :: 3.12",
26+
"Natural Language :: English",
27+
"Operating System :: OS Independent",
28+
"Development Status :: 5 - Production/Stable",
29+
"Intended Audience :: Developers",
30+
"Topic :: Software Development :: Libraries",
31+
]
32+
dependencies = [
33+
"lxml>=4.5",
34+
"beautifulsoup4>=4.9",
35+
]
36+
37+
[project.urls]
38+
Homepage = "https://github.com/adrianmo/pymeteoclimatic"
39+
Repository = "https://github.com/adrianmo/pymeteoclimatic"
40+
41+
[tool.setuptools.packages.find]
42+
include = ["meteoclimatic*"]
43+
44+
[tool.setuptools.dynamic]
45+
version = {attr = "meteoclimatic.__version__"}

setup.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

tests/test_client.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import os
22
import unittest
33
from urllib.error import HTTPError
4+
from urllib.request import Request
45
from meteoclimatic.exceptions import StationNotFound, MeteoclimaticError
56
from meteoclimatic import MeteoclimaticClient
6-
from unittest.mock import patch
7+
from meteoclimatic import __version__
8+
from unittest.mock import patch, MagicMock
79

810

911
class TestMeteoclimaticClient(unittest.TestCase):
@@ -19,8 +21,14 @@ def test_get_station_info_ok(self, mock_urlopen):
1921

2022
res = self.client.weather_at_station("ESCAT4300000043206B")
2123

22-
mock_urlopen.assert_called_with(
23-
"https://www.meteoclimatic.net/feed/rss/ESCAT4300000043206B")
24+
# Verify urlopen was called with a Request object
25+
self.assertEqual(mock_urlopen.call_count, 1)
26+
called_request = mock_urlopen.call_args[0][0]
27+
self.assertIsInstance(called_request, Request)
28+
self.assertEqual(called_request.full_url,
29+
"https://www.meteoclimatic.net/feed/rss/ESCAT4300000043206B")
30+
self.assertEqual(called_request.get_header("User-agent"),
31+
f"pymeteoclimatic/{__version__}")
2432
self.assertEqual(res.station.code, "ESCAT4300000043206B")
2533

2634
@patch('meteoclimatic.client.urlopen', autospec=True)
@@ -41,3 +49,21 @@ def test_get_station_info_404(self, mock_urlopen):
4149
self.client.weather_at_station("ESCAT4300000043206B")
4250
self.assertEqual(str(
4351
error.exception), "Error fetching station data [status_code=404]")
52+
53+
@patch('meteoclimatic.client.urlopen', autospec=True)
54+
def test_user_agent_header_is_set(self, mock_urlopen):
55+
"""Test that the User-Agent header is correctly set with version"""
56+
mock_response = MagicMock()
57+
mock_response.read.return_value = open(os.path.join(
58+
os.path.dirname(__file__), "feeds", "full_station.xml")).read()
59+
mock_urlopen.return_value = mock_response
60+
61+
self.client.weather_at_station("ESCAT4300000043206B")
62+
63+
# Verify the Request object has the correct User-Agent
64+
called_request = mock_urlopen.call_args[0][0]
65+
self.assertIsInstance(called_request, Request)
66+
user_agent = called_request.get_header("User-agent")
67+
self.assertEqual(user_agent, f"pymeteoclimatic/{__version__}")
68+
# Verify it follows the expected format
69+
self.assertTrue(user_agent.startswith("pymeteoclimatic/"))

0 commit comments

Comments
 (0)