From eedabbcde7bfc2f5fbe01b1eadaf4313d19ba81e Mon Sep 17 00:00:00 2001 From: PierreSelim Date: Tue, 12 May 2026 09:20:16 +0200 Subject: [PATCH] Modern tooling for more recent Python Tooling: - uv, ruff, hathcling - unit test with pytest Code: - attrdict is deprecated -> PageviewResponse with same behaviour - formatting Adding CI with github action - unit test all supported version of python - codecov python 3.14 Modernize the README too. --- .github/workflows/ci.yml | 26 +++++ .gitignore | 12 +++ .travis.yml | 8 -- MANIFEST.in | 1 - README.md | 6 +- pageviewapi/__init__.py | 24 ++++- pageviewapi/client.py | 219 ++++++++++++++++++++++++--------------- pageviewapi/period.py | 56 +++++----- pyproject.toml | 31 ++++++ setup.py | 42 -------- tests/__init__.py | 0 tests/test_client.py | 211 +++++++++++++++++++++++++++++++++++++ tests/test_period.py | 69 ++++++++++++ tox.ini | 10 -- 14 files changed, 534 insertions(+), 181 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml delete mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_client.py create mode 100644 tests/test_period.py delete mode 100644 tox.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e53997f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - run: uv run ruff check . + - run: uv run ruff format --check . + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - run: uv run --python ${{ matrix.python-version }} pytest tests/ --cov=pageviewapi --cov-report=term-missing --cov-report=xml + - uses: codecov/codecov-action@v5 + if: matrix.python-version == '3.14' + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 0b57a8e..6324efa 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,15 @@ cover/ # Typical virtual environments .venv venv + +# uv +uv.lock +.python-version + +# pytest / coverage +.coverage +htmlcov/ +.pytest_cache/ + +# ruff +.ruff_cache/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 29b8d5a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: python -python: 2.7 -env: -- TOX_ENV=flake8 -install: -- pip install tox -script: -- tox -e $TOX_ENV diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index efa752e..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include *.md diff --git a/README.md b/README.md index 38e18bd..46cdf87 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # pageview-api -[![Build Status](https://travis-ci.org/Commonists/pageview-api.svg?branch=master)](https://travis-ci.org/Commonists/pageview-api) -[![Code Health](https://landscape.io/github/Commonists/pageview-api/master/landscape.svg?style=flat)](https://landscape.io/github/Commonists/pageview-api/master) +[![CI](https://github.com/Commonists/pageview-api/actions/workflows/ci.yml/badge.svg)](https://github.com/Commonists/pageview-api/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/Commonists/pageview-api/branch/master/graph/badge.svg)](https://codecov.io/gh/Commonists/pageview-api) [![Pypi](https://img.shields.io/pypi/v/pageviewapi.svg?style=flat)](https://pypi.python.org/pypi/pageviewapi) [![License](http://img.shields.io/badge/license-MIT-orange.svg?style=flat)](http://opensource.org/licenses/MIT) -Wikimedia Pageview API client +Wikimedia Pageview API client for Python 3.10+ Installation ------------ diff --git a/pageviewapi/__init__.py b/pageviewapi/__init__.py index acbd389..776528e 100644 --- a/pageviewapi/__init__.py +++ b/pageviewapi/__init__.py @@ -1,9 +1,25 @@ -"""Python client for wikimedia pageview api.""" +"""Python client for the Wikimedia Pageview API.""" from pageviewapi.client import ( + PageviewResponse, + ThrottlingException, + ZeroOrDataNotLoadedException, + __version__, + aggregate, + legacy_pagecounts, per_article, top, - aggregate, unique_devices, - legacy_pagecounts, - __version__) +) + +__all__ = [ + "__version__", + "aggregate", + "PageviewResponse", + "legacy_pagecounts", + "per_article", + "ThrottlingException", + "top", + "unique_devices", + "ZeroOrDataNotLoadedException", +] diff --git a/pageviewapi/client.py b/pageviewapi/client.py index aacb6e6..e55c852 100644 --- a/pageviewapi/client.py +++ b/pageviewapi/client.py @@ -5,150 +5,197 @@ - per-article - top - aggregate +- unique-devices +- legacy/pagecounts """ -from attrdict import AttrDict +from importlib.metadata import PackageNotFoundError, version +from typing import Any + import requests -__version__ = "0.4.0" +try: + __version__ = version("pageviewapi") +except PackageNotFoundError: + __version__ = "0.4.0" -# User-agent PROJECT_URL = "https://github.com/Commonists/pageview-api" -UA = "Python pageview-api client v{version} <{url}>" -USER_AGENT = { - 'User-Agent': UA.format(url=PROJECT_URL, version=__version__) -} +USER_AGENT = {"User-Agent": f"Python pageview-api client v{__version__} <{PROJECT_URL}>"} API_BASE_URL = "https://wikimedia.org/api/rest_v1/metrics" -# Per article + PA_ENDPOINT = "pageviews/per-article" PA_ARGS = "{project}/{access}/{agent}/{page}/{granularity}/{start}/{end}" -# Top TOP_ENDPOINT = "pageviews/top" TOP_ARGS = "{project}/{access}/{year}/{month}/{day}" -# aggregate AG_ENDPOINT = "pageviews/aggregate" AG_ARGS = "{project}/{access}/{agent}/{granularity}/{start}/{end}" -# unique-devices UD_ENDPOINT = "unique-devices" UD_ARGS = "{project}/{access}/{granularity}/{start}/{end}" -# legacy pagecounts PC_ENDPOINT = "legacy/pagecounts/aggregate" PC_ARGS = "{project}/{access_site}/{granularity}/{start}/{end}" +class PageviewResponse(dict): # type: ignore[type-arg] + """A Wikimedia Pageview API response with attribute-style read access. + + Recursively wraps nested dicts and lists so the full response tree is + navigable via attributes. Dict data keys always take priority over built-in + dict methods, so ``response.items`` returns the ``items`` data value rather + than the ``dict.items`` method when that key is present. + + Use ``from_json`` rather than the constructor directly when the input may + contain nested dicts or lists — the constructor only wraps the top level. + """ + + def __getattribute__(self, key: str) -> Any: + if not key.startswith("_"): + try: + return dict.__getitem__(self, key) + except KeyError: + pass + return super().__getattribute__(key) + + @classmethod + def from_json(cls, obj: Any) -> Any: + """Recursively convert a JSON value into a ``PageviewResponse`` tree.""" + if isinstance(obj, dict): + return cls({k: cls.from_json(v) for k, v in obj.items()}) + if isinstance(obj, list): + return [cls.from_json(item) for item in obj] + return obj + + class ZeroOrDataNotLoadedException(Exception): - """Raised for 404 Error + """Raised on 404 — no data or data not yet filled. - 404 may happen when there is no data or data has not been filled yet. https://wikitech.wikimedia.org/wiki/Analytics/PageviewAPI#Gotchas """ - pass class ThrottlingException(Exception): - """Raise for 429 Error + """Raised on 429 — client is sending too many requests. - Client doing too many request may be subject to throttling. - Requests in cache are not throttled (throttling is done at storage layer). https://wikitech.wikimedia.org/wiki/Analytics/PageviewAPI#Gotchas """ -def per_article(project, page, start, end, - access='all-access', agent='all-agents', granularity='daily'): - """Per article API. +def per_article( + project: str, + page: str, + start: str, + end: str, + access: str = "all-access", + agent: str = "all-agents", + granularity: str = "daily", +) -> dict[str, Any]: + """Per-article pageview counts. >>> import pageviewapi - >>> pageview.per_article('en.wikipedia', 'Paris', '20151106', '20151120') - will requests views for Paris article between 2015-11-06 and 2015-11-20 + >>> pageviewapi.per_article('en.wikipedia', 'Paris', '20151106', '20151120') """ - args = PA_ARGS.format(project=project, - page=page, - start=start, - end=end, - access=access, - agent=agent, - granularity=granularity) - return __api__(PA_ENDPOINT, args) - - -def top(project, year, month, day, access='all-access'): - """Top 1000 most visited articles from project on a given date. + args = PA_ARGS.format( + project=project, + page=page, + start=start, + end=end, + access=access, + agent=agent, + granularity=granularity, + ) + return _api(PA_ENDPOINT, args) + + +def top( + project: str, + year: int | str, + month: int | str, + day: int | str, + access: str = "all-access", +) -> dict[str, Any]: + """Top 1000 most visited articles for a project on a given date. >>> import pageviewapi >>> views = pageviewapi.top('fr.wikipedia', 2015, 11, 14) >>> views['items'][0]['articles'][0] - {u'article': u'Wikip\xe9dia:Accueil_principal', u'rank': 1, - u'views': 1600547} + {'article': 'Wikipédia:Accueil_principal', 'rank': 1, 'views': 1600547} """ - args = TOP_ARGS.format(project=project, - access=access, - year=year, - month=month, - day=day) - return __api__(TOP_ENDPOINT, args) + args = TOP_ARGS.format(project=project, access=access, year=year, month=month, day=day) + return _api(TOP_ENDPOINT, args) -def aggregate(project, start, end, - access='all-access', agent='all-agents', granularity='daily'): - """Aggregate API. +def aggregate( + project: str, + start: str, + end: str, + access: str = "all-access", + agent: str = "all-agents", + granularity: str = "daily", +) -> dict[str, Any]: + """Aggregate pageview counts for a project. >>> import pageviewapi >>> pageviewapi.aggregate('fr.wikipedia', '2015100100', '2015103100') """ - args = AG_ARGS.format(project=project, - start=start, - end=end, - access=access, - agent=agent, - granularity=granularity) - return __api__(AG_ENDPOINT, args) - - -def unique_devices(project, start, end, - access='all-access', granularity='daily'): - """Unique devices.""" - args = UD_ARGS.format(project=project, - start=start, - end=end, - access=access, - granularity=granularity) - return __api__(UD_ENDPOINT, args) - - -def legacy_pagecounts(project, start, end, - access_site='all-sites', granularity='daily'): - """Legacy pagecounts + args = AG_ARGS.format( + project=project, + start=start, + end=end, + access=access, + agent=agent, + granularity=granularity, + ) + return _api(AG_ENDPOINT, args) + + +def unique_devices( + project: str, + start: str, + end: str, + access: str = "all-access", + granularity: str = "daily", +) -> dict[str, Any]: + """Unique devices accessing a project.""" + args = UD_ARGS.format(project=project, start=start, end=end, access=access, granularity=granularity) + return _api(UD_ENDPOINT, args) + + +def legacy_pagecounts( + project: str, + start: str, + end: str, + access_site: str = "all-sites", + granularity: str = "daily", +) -> dict[str, Any]: + """Legacy pagecounts aggregate. >>> import pageviewapi >>> pageviewapi.legacy_pagecounts('fr.wikipedia', '2010010100', '2011010100') """ - project_arg = 'all-projects' - if project != 'all-projects': - project_arg = '{}.org'.format(project) - args = PC_ARGS.format(project=project_arg, - start=start, - end=end, - access_site=access_site, - granularity=granularity) - return __api__(PC_ENDPOINT, args) - - -def __api__(end_point, args, api_url=API_BASE_URL): - """Calling API.""" + project_arg = "all-projects" if project == "all-projects" else f"{project}.org" + args = PC_ARGS.format( + project=project_arg, + start=start, + end=end, + access_site=access_site, + granularity=granularity, + ) + return _api(PC_ENDPOINT, args) + + +def _api(end_point: str, args: str, api_url: str = API_BASE_URL) -> dict[str, Any]: url = "/".join([api_url, end_point, args]) response = requests.get(url, headers=USER_AGENT) if response.status_code == 200: - # Everything went fine! - return AttrDict(response.json()) + return PageviewResponse.from_json(response.json()) elif response.status_code == 404: - raise ZeroOrDataNotLoadedException + raise ZeroOrDataNotLoadedException() elif response.status_code == 429: - raise ThrottlingException + raise ThrottlingException() else: response.raise_for_status() + return {} # unreachable, satisfies type checker diff --git a/pageviewapi/period.py b/pageviewapi/period.py index 03d67f4..f16db03 100644 --- a/pageviewapi/period.py +++ b/pageviewapi/period.py @@ -1,39 +1,41 @@ -"""Helper functions on period.""" +"""Helper functions for aggregating pageviews over a time period.""" + import datetime + import pageviewapi.client -def sum_last(project, page, last=30, agent='all-agents', access='all-access'): - """Page views during last days.""" - views = pageviewapi.client.per_article(project, page, - __days_ago__(last), - __today__(), - access=access, agent=agent) - return sum([daily['views'] for daily in views['items']]) +def sum_last( + project: str, + page: str, + last: int = 30, + agent: str = "all-agents", + access: str = "all-access", +) -> int: + """Total pageviews for a page over the last N days.""" + views = pageviewapi.client.per_article(project, page, _days_ago(last), _today(), access=access, agent=agent) + return sum(daily["views"] for daily in views["items"]) -def avg_last(project, page, last=30, agent='all-agents', access='all-access'): - """Page views during last days.""" - views = pageviewapi.client.per_article(project, page, - __days_ago__(last), - __today__(), - access=access, agent=agent) - return __avg__([daily['views'] for daily in views['items']]) +def avg_last( + project: str, + page: str, + last: int = 30, + agent: str = "all-agents", + access: str = "all-access", +) -> float: + """Average daily pageviews for a page over the last N days.""" + views = pageviewapi.client.per_article(project, page, _days_ago(last), _today(), access=access, agent=agent) + return _avg([daily["views"] for daily in views["items"]]) -def __today__(): - """Date of the day as YYYYmmdd format.""" - return datetime.date.today().strftime('%Y%m%d') +def _today() -> str: + return datetime.date.today().strftime("%Y%m%d") -def __days_ago__(days): - """Days ago as YYYYmmdd format.""" - today = datetime.date.today() - delta = datetime.timedelta(days=days) - ago = today - delta - return ago.strftime('%Y%m%d') +def _days_ago(days: int) -> str: + return (datetime.date.today() - datetime.timedelta(days=days)).strftime("%Y%m%d") -def __avg__(numericlist): - """Basic average function.""" - return sum(numericlist) / float(len(numericlist)) +def _avg(values: list[int | float]) -> float: + return sum(values) / len(values) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c1ceedf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "pageviewapi" +version = "0.4.0" +description = "Wikimedia Pageview API client" +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "Commonists", email = "ps.huard@gmail.com" }] +requires-python = ">=3.10" +dependencies = ["requests>=2.0"] + +[project.urls] +Homepage = "https://github.com/Commonists/pageview-api" + +[dependency-groups] +dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.9", "pytest-mock>=3.0"] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.coverage.run] +source = ["pageviewapi"] diff --git a/setup.py b/setup.py deleted file mode 100644 index e02e814..0000000 --- a/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin-1 -*- - -"""Setup script.""" - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -try: - import pageviewapi - version = pageviewapi.__version__ -except ImportError: - version = 'Undefined' - - -classifiers = [ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Utilities' -] -packages = ['pageviewapi'] -requires = ['requests', 'attrdict'] - -setup( - name='pageviewapi', - version=version, - author='Commonists', - author_email='ps.huard@gmail.com', - url='http://github.com/Commonists/pageview-api', - description='Wikimedia Pageview API client', - long_description=open('README.md').read(), - license='MIT', - packages=packages, - install_requires=requires, - classifiers=classifiers -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..5d4271d --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,211 @@ +"""Tests for pageviewapi.client.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from pageviewapi.client import ( + API_BASE_URL, + USER_AGENT, + PageviewResponse, + ThrottlingException, + ZeroOrDataNotLoadedException, + _api, + aggregate, + legacy_pagecounts, + per_article, + top, + unique_devices, +) + +ITEMS_RESPONSE = {"items": [{"views": 1234}]} + + +@pytest.fixture +def ok_response() -> MagicMock: + r = MagicMock() + r.status_code = 200 + r.json.return_value = ITEMS_RESPONSE + return r + + +@pytest.fixture +def not_found_response() -> MagicMock: + r = MagicMock() + r.status_code = 404 + return r + + +@pytest.fixture +def throttled_response() -> MagicMock: + r = MagicMock() + r.status_code = 429 + return r + + +def test_per_article_returns_json(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + result = per_article("en.wikipedia", "Paris", "20151106", "20151120") + assert result == ITEMS_RESPONSE + url = mock_get.call_args[0][0] + assert "pageviews/per-article" in url + assert "en.wikipedia" in url + assert "Paris" in url + assert "20151106" in url + assert "20151120" in url + + +def test_per_article_default_args_in_url(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + per_article("en.wikipedia", "Paris", "20151106", "20151120") + url = mock_get.call_args[0][0] + assert "all-access" in url + assert "all-agents" in url + assert "daily" in url + + +def test_per_article_custom_args(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + per_article( + "en.wikipedia", "Paris", "20151106", "20151120", access="desktop", agent="user", granularity="monthly" + ) + url = mock_get.call_args[0][0] + assert "desktop" in url + assert "user" in url + assert "monthly" in url + + +def test_per_article_404_raises(not_found_response): + with patch("requests.get", return_value=not_found_response): + with pytest.raises(ZeroOrDataNotLoadedException): + per_article("en.wikipedia", "Paris", "20151106", "20151120") + + +def test_per_article_429_raises(throttled_response): + with patch("requests.get", return_value=throttled_response): + with pytest.raises(ThrottlingException): + per_article("en.wikipedia", "Paris", "20151106", "20151120") + + +def test_top_returns_json(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + result = top("fr.wikipedia", 2015, 11, 14) + assert result == ITEMS_RESPONSE + url = mock_get.call_args[0][0] + assert "pageviews/top" in url + assert "fr.wikipedia" in url + + +def test_top_url_contains_date(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + top("fr.wikipedia", 2015, 11, 14) + url = mock_get.call_args[0][0] + assert "2015" in url + assert "11" in url + assert "14" in url + + +def test_aggregate_returns_json(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + result = aggregate("fr.wikipedia", "2015100100", "2015103100") + assert result == ITEMS_RESPONSE + url = mock_get.call_args[0][0] + assert "pageviews/aggregate" in url + + +def test_unique_devices_returns_json(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + result = unique_devices("en.wikipedia", "20200101", "20200131") + assert result == ITEMS_RESPONSE + url = mock_get.call_args[0][0] + assert "unique-devices" in url + + +def test_legacy_pagecounts_returns_json(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + result = legacy_pagecounts("fr.wikipedia", "2010010100", "2011010100") + assert result == ITEMS_RESPONSE + url = mock_get.call_args[0][0] + assert "legacy/pagecounts" in url + assert "fr.wikipedia.org" in url + + +def test_legacy_pagecounts_all_projects(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + legacy_pagecounts("all-projects", "2010010100", "2011010100") + url = mock_get.call_args[0][0] + assert "all-projects" in url + assert "all-projects.org" not in url + + +def test_api_sends_user_agent(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + _api("pageviews/per-article", "en.wikipedia/all-access/all-agents/Paris/daily/20200101/20200131") + _, kwargs = mock_get.call_args + assert kwargs["headers"] == USER_AGENT + + +def test_api_other_error_raises(ok_response): + ok_response.status_code = 500 + ok_response.raise_for_status.side_effect = Exception("server error") + with patch("requests.get", return_value=ok_response): + with pytest.raises(Exception, match="server error"): + _api("pageviews/per-article", "bad/args") + + +def test_api_constructs_url_correctly(ok_response): + with patch("requests.get", return_value=ok_response) as mock_get: + _api("my/endpoint", "arg1/arg2") + url = mock_get.call_args[0][0] + assert url == f"{API_BASE_URL}/my/endpoint/arg1/arg2" + + +def test_pageview_response_attribute_access(): + d = PageviewResponse({"rank": 1, "views": 42}) + assert d.rank == 1 + assert d.views == 42 + + +def test_pageview_response_dict_access_still_works(): + d = PageviewResponse({"rank": 1}) + assert d["rank"] == 1 + + +def test_pageview_response_missing_key_raises_attribute_error(): + d = PageviewResponse({"a": 1}) + with pytest.raises(AttributeError): + _ = d.missing + + +def test_pageview_response_items_key_accessible_as_attribute(): + d = PageviewResponse.from_json({"items": [{"views": 1}]}) + assert isinstance(d.items, list) + assert d.items[0].views == 1 + + +def test_pageview_response_keys_key_accessible_as_attribute(): + d = PageviewResponse({"keys": ["a", "b"]}) + assert d.keys == ["a", "b"] + + +def test_pageview_response_dict_method_accessible_when_key_absent(): + d = PageviewResponse({"rank": 1}) + assert callable(d.items) + + +def test_pageview_response_nested_dict_is_wrapped(): + d = PageviewResponse.from_json({"outer": {"inner": 99}}) + assert isinstance(d.outer, PageviewResponse) + assert d.outer.inner == 99 + + +def test_pageview_response_nested_list_of_dicts_is_wrapped(): + d = PageviewResponse.from_json({"articles": [{"title": "Paris", "views": 10}]}) + assert isinstance(d["articles"][0], PageviewResponse) + assert d["articles"][0].title == "Paris" + + +def test_api_returns_pageview_response(ok_response): + with patch("requests.get", return_value=ok_response): + result = per_article("en.wikipedia", "Paris", "20151106", "20151120") + assert isinstance(result, PageviewResponse) diff --git a/tests/test_period.py b/tests/test_period.py new file mode 100644 index 0000000..1ed196c --- /dev/null +++ b/tests/test_period.py @@ -0,0 +1,69 @@ +"""Tests for pageviewapi.period.""" + +from unittest.mock import patch + +import pytest + +from pageviewapi.period import _avg, _days_ago, _today, avg_last, sum_last + +MOCK_VIEWS = {"items": [{"views": 100}, {"views": 200}, {"views": 300}]} + + +def test_today_format(): + result = _today() + assert len(result) == 8 + assert result.isdigit() + + +def test_days_ago_format(): + result = _days_ago(30) + assert len(result) == 8 + assert result.isdigit() + + +def test_days_ago_is_before_today(): + assert _days_ago(1) < _today() + + +def test_days_ago_zero_equals_today(): + assert _days_ago(0) == _today() + + +def test_avg_integers(): + assert _avg([10, 20, 30]) == 20.0 + + +def test_avg_single_value(): + assert _avg([42]) == 42.0 + + +def test_avg_floats(): + assert _avg([1.5, 2.5]) == pytest.approx(2.0) + + +def test_sum_last_returns_total(): + with patch("pageviewapi.client.per_article", return_value=MOCK_VIEWS): + result = sum_last("en.wikipedia", "Python_(programming_language)") + assert result == 600 + + +def test_sum_last_passes_date_range(): + with patch("pageviewapi.client.per_article", return_value=MOCK_VIEWS) as mock_pa: + sum_last("en.wikipedia", "Python_(programming_language)", last=7) + call_args = mock_pa.call_args[0] + start, end = call_args[2], call_args[3] + assert start < end + + +def test_avg_last_returns_average(): + with patch("pageviewapi.client.per_article", return_value=MOCK_VIEWS): + result = avg_last("en.wikipedia", "Python_(programming_language)") + assert result == pytest.approx(200.0) + + +def test_avg_last_passes_access_and_agent(): + with patch("pageviewapi.client.per_article", return_value=MOCK_VIEWS) as mock_pa: + avg_last("en.wikipedia", "Python", access="desktop", agent="user") + _, kwargs = mock_pa.call_args + assert kwargs["access"] == "desktop" + assert kwargs["agent"] == "user" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 64f75ea..0000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = flake8 - -[testenv:flake8] -deps = flake8 -commands = flake8 - -[flake8] -exclude = .venv,.tox,dist,doc,build,*.egg,docs,setup.py -ignore = E501,F401,F841