From a388e73d6f29eeaf2de67e4daa3e5504ad11f8fb Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Thu, 16 Jul 2026 03:06:47 -0300 Subject: [PATCH] feat(core): check latest package version on PyPI Closes #31 Co-authored-by: Cursor --- .../create-python-app-core/pyproject.toml | 1 + .../src/create_python_app_core/api.py | 16 +++- .../tests/test_pypi_version.py | 25 ++++++ pyproject.toml | 2 + uv.lock | 88 ++++++++++++++++++- 5 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 packages/create-python-app-core/tests/test_pypi_version.py diff --git a/packages/create-python-app-core/pyproject.toml b/packages/create-python-app-core/pyproject.toml index ee8b00a..67bd9e5 100644 --- a/packages/create-python-app-core/pyproject.toml +++ b/packages/create-python-app-core/pyproject.toml @@ -5,6 +5,7 @@ description = "Scaffolding engine for Create Awesome Python App (stub)" readme = "README.md" requires-python = ">=3.12" dependencies = [ + "httpx>=0.28.1", "packaging>=24.0", ] diff --git a/packages/create-python-app-core/src/create_python_app_core/api.py b/packages/create-python-app-core/src/create_python_app_core/api.py index 4609cbd..e606237 100644 --- a/packages/create-python-app-core/src/create_python_app_core/api.py +++ b/packages/create-python-app-core/src/create_python_app_core/api.py @@ -35,8 +35,20 @@ def check_python_version(required: str, package_name: str) -> None: async def check_for_latest_version(package_name: str) -> str | None: """Fetch latest version from PyPI. Returns None on failure.""" - _ = package_name - return None + import httpx + + url = f"https://pypi.org/pypi/{package_name}/json" + try: + async with httpx.AsyncClient( + headers={"User-Agent": CPA_USER_AGENT}, + timeout=10.0, + ) as client: + response = await client.get(url) + response.raise_for_status() + data = response.json() + return str(data["info"]["version"]) + except Exception: + return None def print_env_info() -> None: diff --git a/packages/create-python-app-core/tests/test_pypi_version.py b/packages/create-python-app-core/tests/test_pypi_version.py new file mode 100644 index 0000000..65a94db --- /dev/null +++ b/packages/create-python-app-core/tests/test_pypi_version.py @@ -0,0 +1,25 @@ +import pytest +from create_python_app_core.api import check_for_latest_version + + +@pytest.mark.asyncio +async def test_check_for_latest_version_handles_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import httpx + + class FakeClient: + def __init__(self, *args, **kwargs): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + + async def get(self, url: str): + raise httpx.ConnectError("offline") + + monkeypatch.setattr("httpx.AsyncClient", FakeClient) + assert await check_for_latest_version("create-awesome-python-app") is None diff --git a/pyproject.toml b/pyproject.toml index a731ad9..5668ab9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dev = [ "pyright>=1.1.400", "pre-commit>=4.0.0", "pytest-cov>=6.0.0", + "pytest-asyncio>=0.25.0", "pytest>=8.0.0", ] @@ -44,6 +45,7 @@ venv = ".venv" [tool.pytest.ini_options] +asyncio_mode = "auto" testpaths = ["packages"] pythonpath = ["packages/create-python-app-core/src", "packages/create-awesome-python-app/src"] diff --git a/uv.lock b/uv.lock index aa3c3b0..aae61f9 100644 --- a/uv.lock +++ b/uv.lock @@ -13,10 +13,33 @@ dev = [ { name = "pre-commit", specifier = ">=4.0.0" }, { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.12.0" }, ] +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -120,11 +143,15 @@ name = "create-python-app-core" version = "0.0.0" source = { editable = "packages/create-python-app-core" } dependencies = [ + { name = "httpx" }, { name = "packaging" }, ] [package.metadata] -requires-dist = [{ name = "packaging", specifier = ">=24.0" }] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "packaging", specifier = ">=24.0" }, +] [[package]] name = "distlib" @@ -144,6 +171,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/af/9b01bcf5c91e81899bb890b87bd9077732a9b3365c098e67fe77958c39ed/filelock-3.30.0-py3-none-any.whl", hash = "sha256:40632998f0772e64183bb819f086a1b9def6be1090cf1dcb9d45f46806ef279b", size = 93131, upload-time = "2026-07-16T03:53:56.727Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -153,6 +217,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -252,6 +325,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + [[package]] name = "pytest-cov" version = "7.1.0"