diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 6d03445..98c192b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,24 +1,37 @@ name: Publish + on: - release: - branches: - - master - types: - - published + push: + tags: + - "v*" + +permissions: + contents: read + jobs: publish: - name: Publish + name: Build and publish runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout tag uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - name: Install dependencies + run: uv sync --all-extras --group dev - name: Build - run: uv build - - name: Publish + run: uv run python -m build + - name: Check distributions + run: uvx twine check dist/* + - name: Publish to PyPI env: - UV_PUBLISH_TOKEN: ${{ secrets.pypi_token }} + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN || secrets.pypi_token }} run: uv publish diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..1a9df32 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,34 @@ +name: Release + +on: + push: + branches: [main, master] + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + release: + name: Create release tag + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + - name: Run semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx --yes \ + -p semantic-release@25.0.3 \ + -p @semantic-release/commit-analyzer@13.0.1 \ + -p @semantic-release/release-notes-generator@14.1.1 \ + -p @semantic-release/github@12.0.8 \ + semantic-release diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1471a76..ab853a4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,13 +1,41 @@ name: Tests + on: push: - branches: - - "master" + branches: [main, master] pull_request: + +permissions: + contents: read + pull-requests: read + jobs: + conventional-pr-title: + name: Conventional PR title + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Validate title + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + build + chore + ci + docs + feat + fix + perf + refactor + revert + style + test + lint: name: Lint - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -17,11 +45,16 @@ jobs: python-version: "3.10" - name: Install uv uses: astral-sh/setup-uv@v6 - - name: Run Lint - run: uv tool run ruff check + with: + enable-cache: true + - name: Install dependencies + run: uv sync --all-extras --group dev + - name: Run lint + run: uv run ruff check wbdata tests + types: name: Types - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -31,30 +64,38 @@ jobs: python-version: "3.10" - name: Install uv uses: astral-sh/setup-uv@v6 - - name: Install Dependencies - run: uv sync --all-extras - - name: Check Types + with: + enable-cache: true + - name: Install dependencies + run: uv sync --all-extras --group dev + - name: Check types run: uv tool run ty check + test: name: Tests runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] - os: ["ubuntu-latest", "macos-latest", "windows-latest"] + os: [ubuntu-latest, macos-latest, windows-latest] defaults: run: shell: bash steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v6 - - name: Install Dependencies - run: uv sync --all-extras - - name: Run Tests + with: + enable-cache: true + - name: Install dependencies + run: uv sync --all-extras --group dev + - name: Run tests run: uv run pytest diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..561d41f --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,9 @@ +{ + "branches": ["main", "master"], + "tagFormat": "v${version}", + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/github" + ] +} diff --git a/AGENTS.md b/AGENTS.md index 434bb78..9669b67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,10 @@ # Repository Guidelines ## Project Structure & Module Organization -Source lives in `wbdata/` with clients, caching helpers, and API utilities; `wbdata/version.py` centralizes the library version and should be the single source when updating releases. Tests reside in `tests/` with `test_*.py` modules mirroring public APIs. Contributor-facing docs and MkDocs content sit in `docs/`, while packaging metadata and tooling configuration are in `pyproject.toml`. +Source lives in `wbdata/` with clients, caching helpers, and API utilities; package versions are derived from Git tags by `setuptools-scm`. Tests reside in `tests/` with `test_*.py` modules mirroring public APIs. Contributor-facing docs and MkDocs content sit in `docs/`, while packaging metadata and tooling configuration are in `pyproject.toml`. ## Build, Test, and Development Commands -Install dependencies with `uv sync --all-extras --group dev` so the docs, pandas extras, and developer tooling are available. Use `uv run pytest` for the default suite and coverage, matching the `--cov=wbdata` addopts in configuration. Run `uv run ruff check wbdata tests` to lint, and `uv run mypy wbdata` for type validation. During documentation work, serve the site locally via `uv run mkdocs serve`. +Install dependencies with `uv sync --all-extras --group dev` so the docs, pandas extras, and developer tooling are available. Use `uv run pytest` for the default suite and coverage, matching the `--cov=wbdata` addopts in configuration. Run `uv run ruff check wbdata tests` to lint, and `uv tool run ty check` for type validation. During documentation work, serve the site locally via `uv run mkdocs serve`. ## Coding Style & Naming Conventions Follow standard Python formatting with four-space indentation and readable, snake_case symbols. Public APIs exposed in `wbdata/__init__.py` should maintain descriptive, lowercase names; classes stay in CapWords. Ruff enforces PEP 8, import sorting, and selected Bugbear/Simplify rules—run it before committing. Keep modules typed, updating `py.typed` coverage when adding packages, and prefer explicit re-exports in `__all__` blocks where applicable. @@ -16,4 +16,4 @@ Write new tests under `tests/` using `pytest` conventions (`test_feature.py`, fu Aim for concise, imperative subjects, optionally prefixed with Conventional Commit types as seen in `git log` (e.g., `fix: improve caching`). Reference related issues or discussions with `(#123)` in the subject when merging via GitHub. Before opening a PR, ensure lint, typing, and tests pass, document user-facing changes, and provide a short summary plus reproduction or screenshots when behavior shifts. ## Documentation & Release Notes -Update `docs/` pages when modifying user workflows, and verify the navigation using the MkDocs preview. Release metadata lives in `wbdata/version.py`; bump it in sync with changelog entries and confirm that packaging files (`pyproject.toml`, `MANIFEST.in`) need no extra updates. +Update `docs/` pages when modifying user workflows, and verify the navigation using the MkDocs preview. Releases are created from Conventional Commits and Git tags; confirm that packaging files (`pyproject.toml`, `MANIFEST.in`) need no extra updates. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f370ede --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing + +## Pull request titles + +Pull request titles must use Conventional Commits syntax, because squash merges use +the PR title as the commit message that drives automated releases. + +Examples: + +- `feat: add a new data helper` -> minor release +- `fix: correct cache expiry` -> patch release +- `feat!: remove a deprecated API` -> major release +- `fix: handle missing values` with a `BREAKING CHANGE:` footer -> major release + +Prefer squash merges so the merged commit on `main`/`master` keeps the validated +PR title. + +## Releases and versions + +Versions come from git tags, not committed version files. Merged Conventional +Commits are analyzed on pushes to `main`/`master`; when a release is needed, +semantic-release creates a `vX.Y.Z` tag and GitHub release without committing a +version bump back to the branch. + +Package builds use `setuptools-scm`, so a build from `vX.Y.Z` produces version +`X.Y.Z`. Local development builds between tags receive setuptools-scm dev +versions. + +Publishing to PyPI happens only from release tags. The tag workflow builds with +uv and publishes with `uv publish`. + +## Local checks + +```bash +uv sync --all-extras --group dev +uv run pytest +uv tool run ty check +uv run python -m build +uvx twine check dist/* +``` diff --git a/MANIFEST.in b/MANIFEST.in index 5ea9f14..cfbf19d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -include *.txt -include *.rst -recursive-include docs *.rst +include LICENSE.txt README.md CONTRIBUTORS.md CONTRIBUTING.md mkdocs.yml +recursive-include docs *.md recursive-include tests *.py diff --git a/README.md b/README.md index 6ff22d3..74e2e05 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,5 @@ API](http://data.worldbank.org/developers/api-overview), and also adds some convenience functionality for searching and retrieving information. Documentation is available at . + +Contributing and release conventions are described in [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/mkdocs.yml b/mkdocs.yml index 5832bc0..520641a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,7 @@ plugins: options: show_source: false members_order: source - docstrings_options: + docstring_options: returns_named_value: false returns_multiple_items: false watch: diff --git a/pyproject.toml b/pyproject.toml index 1df2e28..c0d82b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [project] name = "wbdata" -version = "1.1.0" +dynamic = ["version"] description = "A library to access World Bank data" readme = "README.md" -license = {text = "GPL-2.0+"} +license = "GPL-2.0-or-later" requires-python = ">=3.10,<4" authors = [ {name = "Oliver Sherouse", email = "oliver@oliversherouse.com"}, @@ -41,7 +41,9 @@ Documentation = "https://wbdata.readthedocs.io/" [dependency-groups] dev = [ + "build>=1.2.2,<2", "pytest>=7.4.4,<8", + "ruff>=0.8.0", "pytest-cov>=4.1.0,<5", "types-cachetools>=5.3.0.7,<6", "types-tabulate>=0.9.0.20240106,<0.10", @@ -52,8 +54,18 @@ dev = [ ] [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["setuptools>=64", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["wbdata*"] + +[tool.setuptools.package-data] +wbdata = ["py.typed"] + +[tool.setuptools_scm] +fallback_version = "0+unknown" +local_scheme = "no-local-version" [tool.ruff.lint] select = [ diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..29271dc --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,7 @@ +from importlib.metadata import version + +import wbdata + + +def test_package_version_matches_distribution_metadata() -> None: + assert wbdata.__version__ == version("wbdata") diff --git a/uv.lock b/uv.lock index 9801e3c..056a57d 100644 --- a/uv.lock +++ b/uv.lock @@ -25,6 +25,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -301,6 +317,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -775,6 +803,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, ] +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + [[package]] name = "pytest" version = "7.4.4" @@ -1024,6 +1061,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + [[package]] name = "shelved-cache" version = "0.3.1" @@ -1223,7 +1285,6 @@ wheels = [ [[package]] name = "wbdata" -version = "1.1.0" source = { editable = "." } dependencies = [ { name = "appdirs" }, @@ -1247,8 +1308,10 @@ pandas = [ [package.dev-dependencies] dev = [ + { name = "build" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "ruff" }, { name = "types-appdirs" }, { name = "types-cachetools" }, { name = "types-dateparser" }, @@ -1275,8 +1338,10 @@ provides-extras = ["pandas", "docs"] [package.metadata.requires-dev] dev = [ + { name = "build", specifier = ">=1.2.2,<2" }, { name = "pytest", specifier = ">=7.4.4,<8" }, { name = "pytest-cov", specifier = ">=4.1.0,<5" }, + { name = "ruff", specifier = ">=0.8.0" }, { name = "types-appdirs", specifier = ">=1.4.3.5,<1.5" }, { name = "types-cachetools", specifier = ">=5.3.0.7,<6" }, { name = "types-dateparser", specifier = ">=1.1.4.20240106,<1.2" }, @@ -1284,3 +1349,12 @@ dev = [ { name = "types-requests", specifier = ">=2.31.0.20240106,<2.32" }, { name = "types-tabulate", specifier = ">=0.9.0.20240106,<0.10" }, ] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] diff --git a/wbdata/__init__.py b/wbdata/__init__.py index 323a2b1..8f63fe4 100644 --- a/wbdata/__init__.py +++ b/wbdata/__init__.py @@ -4,8 +4,10 @@ from functools import lru_cache +from ._metadata import get_version from .client import Client -from .version import __version__ + +__version__ = get_version() @lru_cache(maxsize=1) diff --git a/wbdata/_metadata.py b/wbdata/_metadata.py new file mode 100644 index 0000000..d4fb6dc --- /dev/null +++ b/wbdata/_metadata.py @@ -0,0 +1,86 @@ +"""Package metadata helpers.""" + +from __future__ import annotations + +import json +import re +import subprocess +from importlib.metadata import PackageNotFoundError, distribution +from pathlib import Path +from urllib.parse import unquote, urlparse + +_PACKAGE_NAME = "wbdata" +_VERSION_TAG_RE = re.compile( + r"^v?(?P\d+(?:\.\d+)+)(?:-(?P\d+)-g[0-9a-f]+)?$" +) + + +def get_version() -> str: + """Return the installed package version, with a source-tree fallback.""" + package_root = Path(__file__).resolve().parent + try: + dist = distribution(_PACKAGE_NAME) + except PackageNotFoundError: + return _scm_version(package_root.parent) + + dist_root = Path(str(dist.locate_file(""))).resolve() + if package_root.is_relative_to(dist_root): + return dist.version + + direct_url = dist.read_text("direct_url.json") + if direct_url is not None: + try: + source_root = Path(unquote(urlparse(json.loads(direct_url)["url"]).path)) + except (KeyError, TypeError, ValueError): + source_root = None + if source_root is not None and package_root.is_relative_to(source_root): + return dist.version + + # In direct source-tree imports, avoid reporting an unrelated installed + # distribution's version. Editable installs have matching metadata and take + # one of the branches above; raw checkouts fall back to git-derived text. + return _scm_version(package_root.parent) + + +def _scm_version(repo_root: Path) -> str: + try: + describe = subprocess.check_output( + [ + "git", + "describe", + "--tags", + "--long", + "--match", + "v[0-9]*", + "--dirty", + ], + cwd=repo_root, + stderr=subprocess.DEVNULL, + text=True, + ).strip() + except (OSError, subprocess.CalledProcessError): + return "0+unknown" + + dirty = describe.endswith("-dirty") + if dirty: + describe = describe.removesuffix("-dirty") + + match = _VERSION_TAG_RE.match(describe) + if match is None: + return "0+unknown" + + version = match.group("version") + distance = match.group("distance") + if distance in (None, "0"): + if dirty: + parts = version.split(".") + parts[-1] = str(int(parts[-1]) + 1) + return f"{'.'.join(parts)}.dev0+dirty" + return version + + parts = version.split(".") + parts[-1] = str(int(parts[-1]) + 1) + dev_version = f"{'.'.join(parts)}.dev{distance}" + if dirty: + return f"{dev_version}+dirty" + return dev_version diff --git a/wbdata/cache.py b/wbdata/cache.py index 60fc272..4abdef0 100644 --- a/wbdata/cache.py +++ b/wbdata/cache.py @@ -12,15 +12,15 @@ import cachetools import shelved_cache # type: ignore[import-untyped] -from .version import __version__ +from ._metadata import get_version log = logging.getLogger(__name__) +_version = get_version() + CACHE_PATH = os.getenv( "WBDATA_CACHE_PATH", - os.path.join( - appdirs.user_cache_dir(appname="wbdata", version=__version__), "cache" - ), + os.path.join(appdirs.user_cache_dir(appname="wbdata", version=_version), "cache"), ) try: diff --git a/wbdata/client.py b/wbdata/client.py index f64e38f..bea525e 100644 --- a/wbdata/client.py +++ b/wbdata/client.py @@ -17,7 +17,7 @@ try: import pandas as pd # type: ignore[import-untyped] except ImportError: - pd = None # type: ignore[assignment] + pd = None # ty: ignore[invalid-assignment] from . import cache, dates, fetcher @@ -473,7 +473,7 @@ def get_series( ) df = pd.DataFrame( [[i["country"]["value"], i["date"], i["value"]] for i in raw_data], - columns=["country", "date", name], + columns=["country", "date", name], # ty: ignore[invalid-argument-type] ) df[name] = df[name].map(_cast_float) if not keep_levels and len(df["country"].unique()) == 1: diff --git a/wbdata/version.py b/wbdata/version.py deleted file mode 100644 index 6849410..0000000 --- a/wbdata/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.1.0"