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"