diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14c4ad9..5a028af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: jobs: build: - name: ${{ matrix.os }} python-${{ matrix.python-version }} + name: ${{ matrix.os }} strategy: matrix: @@ -17,32 +17,38 @@ jobs: - ubuntu-latest - windows-latest - macos-latest - python-version: ["3.10", "3.11", "3.12", "3.13"] fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install Python + uses: actions/setup-python@v6 with: - python-version: ${{ matrix.python-version }} + python-version-file: ".python-version" - name: Install uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.3" + version: "0.9.11" enable-cache: true cache-dependency-glob: "uv.lock" - python-version: ${{ matrix.python-version }} - name: Install the project - run: uv sync --dev --frozen + run: uv sync --locked + + - name: Lint + uses: astral-sh/ruff-action@v3 - - name: Run tests & build + - name: Run tests + run: | + uv run nox -s tests + + - name: Build & smoke test + shell: bash run: | - uv run pytest uv build + uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9795de0..ddb8988 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,11 +13,11 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - name: Install Python and dependencies + - name: Install Python uses: actions/setup-python@v5 with: python-version-file: ".python-version" @@ -25,14 +25,14 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.3" + version: "0.9.11" - name: Set up Quarto uses: quarto-dev/quarto-actions/setup@v2 - name: Build docs run: | - uv sync --dev --frozen + uv sync --locked cd docs && uv run quartodoc build - name: Render and publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2fb80d..e50ee30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,19 +11,31 @@ jobs: environment: release permissions: id-token: write + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.3" + version: "0.9.11" + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version-file: ".python-version" - name: Build for distribution run: uv build + - name: Smoke test (wheel) + run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py + + - name: Smoke test (source distribution) + run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py + - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + run: uv publish diff --git a/.gitignore b/.gitignore index 58d6f8a..cab2e52 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ .pytest_cache .ruff_cache .coverage +.nox htmlcov # Generated version files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4d2209..9091425 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,11 @@ repos: -- repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.14.5 - hooks: - # Run the linter. - - id: ruff-check - # Run the formatter. - - id: ruff-format \ No newline at end of file + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.5 + hooks: + - id: ruff-check + args: [ --fix ] + - id: ruff-format + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.9.10 + hooks: + - id: uv-lock \ No newline at end of file diff --git a/docs/development.qmd b/docs/development.qmd index 3834593..e013865 100644 --- a/docs/development.qmd +++ b/docs/development.qmd @@ -15,10 +15,6 @@ dependencies. First, be sure you ```bash # Install dependencies and set up the virtual environment uv sync - -# Install the pre-commit hooks -uv run pre-commit install - ``` For more details on how to use `uv`, head over to the [`uv` docs](https://docs.astral.sh/uv/). @@ -27,33 +23,46 @@ For more details on how to use `uv`, head over to the [`uv` docs](https://docs.a ### Code `ruff` is used for linting and formatting, automatically executed in a `pre-commit` hook. + +```bash +# Install the pre-commit hooks +uv run pre-commit install +``` -To run `ruff` manually from the command line or editor, see the [documentation](https://docs.astral.sh/ruff/) on how to install and use`ruff`. +To run `ruff` manually from the command line or editor, see the [`ruff` documentation](https://docs.astral.sh/ruff/). ### Docstrings Follow the [`NumPy`](https://numpydoc.readthedocs.io/en/latest/format.html#overview) style guide when writing docstrings. This is important for being able to [build the documentation](#building-the-documentation). ## Running tests -Tests are run using `pytest`, typically via `uv run`: +For fast iteration on specific tests, use `uv run pytest`: ```bash -uv run pytest # all tests -uv run pytest -s tests/test_client.py # one test file +uv run pytest -s tests/test_client.py # one test file uv run pytest -s tests/test_client.py::test_call_raises_http_error # one specific test ``` -To check test coverage: +To run the complete test suite across all supported Python versions, use `nox`: +```bash +uv run nox # for the entire suite +uv run nox -s tests # same thing, just being explicit about the session +``` +To check test coverage: ```bash -uv run pytest --cov=src --cov-report=term +uv run pytest --cov=pxweb # for terminal output +uv run pytest --cov=pxweb --cov-report=html # to get a report ``` -Some tests utilize [`syrupy`](https://github.com/syrupy-project/syrupy) for snapshot testing to ensure consistency. If snapshots for some reason need to be updated run `uv run pytest --snapshot-update`. +Some tests utilize [`syrupy`](https://github.com/syrupy-project/syrupy) for snapshot testing to ensure consistency. If snapshots need to be updated: +```bash +uv run pytest --snapshot-update +``` ## Building the documentation Documentation is built using [`quartodoc`](https://github.com/machow/quartodoc), thus you also need [`quarto`](https://quarto.org/docs/get-started/) installed on your machine if you wish to build the documentation locally. This is usually a good idea to check that the output looks as expected. The documentation is automatically built and published upon each release. -With `quarto` installed, building the documentation is done in two steps: +With `quarto` installed the documentation can be rendered: ```bash cd docs @@ -67,8 +76,5 @@ The rendered output is found in the folder called `_site`. When you’re ready to contribute: 1. Clone the repository and create a new branch for your changes. -2. Ensure that: - * All tests pass locally (`uv run pytest`) - * Code is linted and formatted (`uv run ruff check && uv run ruff format`) - * Documentation (if relevant) builds successfully +2. Ensure tests pass and code is properly formatted. 3. Open a pull request to the `main` branch. diff --git a/docs/version_config.py b/docs/version_config.py index 5cc51a2..eeb2f45 100644 --- a/docs/version_config.py +++ b/docs/version_config.py @@ -1,4 +1,4 @@ -from importlib_metadata import version as get_version +from importlib.metadata import version as get_version version = get_version("pxwebpy") diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..ed4dcba --- /dev/null +++ b/noxfile.py @@ -0,0 +1,13 @@ +from nox import Session, options +from nox_uv import session + +options.default_venv_backend = "uv" + + +@session( + name="tests", + python=["3.10", "3.11", "3.12", "3.13"], + uv_groups=["dev"], +) +def test(s: Session) -> None: + s.run("python", "-m", "pytest", "-vv") diff --git a/pyproject.toml b/pyproject.toml index 6f664f0..fc1d24c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ authors = [ { name = "Stefan Furne", email = "stefan@furne.net" } ] dependencies = [ + "packaging>=25.0", "requests>=2.32.3", "requests-cache>=1.2.1", ] @@ -58,6 +59,8 @@ dev = [ "ipykernel>=6.29.5", "nbclient>=0.10.2", "nbformat>=5.10.4", + "nox>=2025.11.12", + "nox-uv>=0.6.3", "pandas>=2.2.3", "pandas-stubs>=2.2.2.240807", "polars>=1.19.0", diff --git a/tests/smoke_test.py b/tests/smoke_test.py new file mode 100644 index 0000000..0da0160 --- /dev/null +++ b/tests/smoke_test.py @@ -0,0 +1,12 @@ +""" +Check some basics to ensure functionality. +""" + +from pxweb import get_known_apis + +apis = get_known_apis() + +if not isinstance(apis, dict) or not apis: + raise RuntimeError("Smoke test failed") +else: + print("Smoke test passed") diff --git a/uv.lock b/uv.lock index 8638a0d..b7c0a0e 100644 --- a/uv.lock +++ b/uv.lock @@ -25,6 +25,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + [[package]] name = "asttokens" version = "3.0.0" @@ -311,6 +320,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + [[package]] name = "comm" version = "0.2.3" @@ -462,6 +483,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] +[[package]] +name = "dependency-groups" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -522,6 +556,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, ] +[[package]] +name = "humanize" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/43/50033d25ad96a7f3845f40999b4778f753c3901a11808a584fed7c00d9f5/humanize-4.14.0.tar.gz", hash = "sha256:2fa092705ea640d605c435b1ca82b2866a1b601cdf96f076d70b79a855eba90d", size = 82939, upload-time = "2025-10-15T13:04:51.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/5b/9512c5fb6c8218332b530f13500c6ff5f3ce3342f35e0dd7be9ac3856fd3/humanize-4.14.0-py3-none-any.whl", hash = "sha256:d57701248d040ad456092820e6fde56c930f17749956ac47f4f655c0c547bfff", size = 132092, upload-time = "2025-10-15T13:04:49.404Z" }, +] + [[package]] name = "identify" version = "2.6.15" @@ -817,6 +860,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] +[[package]] +name = "nox" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "attrs" }, + { name = "colorlog" }, + { name = "dependency-groups" }, + { name = "humanize" }, + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/a8/e169497599266d176832e2232c08557ffba97eef87bf8a18f9f918e0c6aa/nox-2025.11.12.tar.gz", hash = "sha256:3d317f9e61f49d6bde39cf2f59695bb4e1722960457eee3ae19dacfe03c07259", size = 4030561, upload-time = "2025-11-12T18:39:03.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/34/434c594e0125a16b05a7bedaea33e63c90abbfbe47e5729a735a8a8a90ea/nox-2025.11.12-py3-none-any.whl", hash = "sha256:707171f9f63bc685da9d00edd8c2ceec8405b8e38b5fb4e46114a860070ef0ff", size = 74447, upload-time = "2025-11-12T18:39:01.575Z" }, +] + +[[package]] +name = "nox-uv" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nox" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/af/ebc522c51facc3b7d26df1c5bc0b72baadb33ce4d539cf66758f22008f6c/nox_uv-0.6.3.tar.gz", hash = "sha256:7940dc4fed7326d00c9687d6dac65d625db21064dd02631af64cfdfa738e817b", size = 4992, upload-time = "2025-10-23T01:32:53.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/cc/32317fd187febd9b2c5dc6cad4d25b358bd3c289958a00b78d94dbddf481/nox_uv-0.6.3-py3-none-any.whl", hash = "sha256:650cc4dafcde281a77d0526ac9ff9c92de5cd7759ecc9fdbf98393e4ab24490f", size = 5315, upload-time = "2025-10-23T01:32:52.977Z" }, +] + [[package]] name = "numpy" version = "2.2.6" @@ -1206,6 +1280,7 @@ wheels = [ name = "pxwebpy" source = { editable = "." } dependencies = [ + { name = "packaging" }, { name = "requests" }, { name = "requests-cache" }, ] @@ -1215,6 +1290,8 @@ dev = [ { name = "ipykernel" }, { name = "nbclient" }, { name = "nbformat" }, + { name = "nox" }, + { name = "nox-uv" }, { name = "pandas" }, { name = "pandas-stubs" }, { name = "polars" }, @@ -1230,6 +1307,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "packaging", specifier = ">=25.0" }, { name = "requests", specifier = ">=2.32.3" }, { name = "requests-cache", specifier = ">=1.2.1" }, ] @@ -1239,6 +1317,8 @@ dev = [ { name = "ipykernel", specifier = ">=6.29.5" }, { name = "nbclient", specifier = ">=0.10.2" }, { name = "nbformat", specifier = ">=5.10.4" }, + { name = "nox", specifier = ">=2025.11.12" }, + { name = "nox-uv", specifier = ">=0.6.3" }, { name = "pandas", specifier = ">=2.2.3" }, { name = "pandas-stubs", specifier = ">=2.2.2.240807" }, { name = "polars", specifier = ">=1.19.0" },