diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..2589cc0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "name": "bodyloop-sdk", + "image": "mcr.microsoft.com/devcontainers/python:3.12", + "features": { + "ghcr.io/astral-sh/uv/devcontainer-feature/uv:latest": {} + }, + "postCreateCommand": "uv sync", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "GitHub.copilot", + "GitHub.copilot-chat" + ], + "settings": { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "ms-python.python" + } + } + } + } +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..0c2dc83 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,44 @@ +# GitHub Copilot Instructions + +## Project Overview + +`bodyloop-sdk-*` is the pattern for the repositories of the SDKs +for the BodyLoop API for certain languages. + +This repository, `bodyloop-sdk-python` implements the one for Python. + +## Spec-Driven Development Workflow + +REVIEW: Defect, major: Does not apply SDD in a correct way. Does not describe the workflow we intend. + +1. **Spec** – Write a detailed docstring describing the function/class contract + (inputs, outputs, raised exceptions, edge cases) before any implementation. +2. **Implement** – Ask Copilot to generate the implementation that satisfies the spec. +3. **Test** – Ask Copilot to generate `pytest` tests covering the spec. +4. **Review** – Verify Copilot output matches the spec; adjust as needed. + +## Code Style + +- Python ≥ 3.11; use type hints on all public APIs. +- Keep the `src/bodyloop_sdk/` layout; add new modules inside that package. +- Use `snake_case` for functions/variables, `PascalCase` for classes. +- Keep public APIs small and composable; prefer pure functions. + +## Testing + +- All tests live in `tests/`; mirror the source module structure. +- Every public function must have at least one happy-path test and one + error/edge-case test. +- Run tests with: `uv run pytest` + +## Dependency Management + +- Manage dependencies with `uv`; edit `pyproject.toml` directly. +- Add runtime deps: `uv add ` +- Add dev deps: `uv add --dev ` + +## Release Process + +- Bump `__version__` in `src/bodyloop_sdk/__init__.py`. +- Create a GitHub release tagged `vX.Y.Z`. +- The `release.yml` workflow will build and publish to PyPI automatically. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..97d1324 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync + + - name: Run tests + run: uv run pytest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..17ce1d7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: Release + +on: + release: + types: [published] + +permissions: + id-token: write # required for PyPI trusted publishing + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + + - name: Build package + run: uv build + + - name: Upload distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: build + environment: + name: pypi + url: https://pypi.org/project/bodyloop-sdk/ + + steps: + - name: Download distribution artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..583f01e --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg +*.egg-info/ +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg +lib/ +lib64/ + +# Virtual environments +.venv/ +venv/ +ENV/ + +# uv +.uv/ + +# pytest +.pytest_cache/ +.coverage +htmlcov/ + +# Editors +.vscode/settings.json +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index 7153038..32b078e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ -# bodyloop-pypi +# BodyLoop SDK for Python + Python SDK for the BodyLoop API for seamless ecosystem integration + +The URL of the source repository has suffix `python` to enable adding SDKs for other lanuages such as JavaScript/TypeScript, C/C++, Rust, etc. + +The distribution name (install) of the Python Package name is `bodyloop-sdk` and is available in the global Package Index PyPi at . It omits the suffix since PyPi already tells us that we are in the Python ecosystem. + +Examples how to get the package are: + +```bash +pip install bodyloop-sdk + +poetry add bodyloop-sdk + +uv add bodyloop-sdk + +pipx install bodyloop-sdk + +uv tool install bodyloop-sdk +``` + +The top-level import of the package is the Python module `bodyloop`. + +Usage: + +```python +import bodyloop +from bodyloop import Viatar, Proband +from bodyloop import System as BodyLoopSystem +``` + +## Contribute + +Local workflow + +```bash +git clone git@github.com:BodyLoop/bodyloop-sdk-python.git +cd bodyloop-sdk-python + +uv sync +uv run pytest +uv build +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4099474 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "bodyloop-sdk" +version = "2026.03.05.1" +description = "Python SDK for the BodyLoop API for seamless ecosystem integration" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.11" +dependencies = [] + +[project.urls] +Homepage = "https://github.com/BodyLoop/bodyloop-pypi" +Repository = "https://github.com/BodyLoop/bodyloop-pypi" +Issues = "https://github.com/BodyLoop/bodyloop-pypi/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/bodyloop_sdk"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", +] diff --git a/src/bodyloop_sdk/__init__.py b/src/bodyloop_sdk/__init__.py new file mode 100644 index 0000000..a55e47b --- /dev/null +++ b/src/bodyloop_sdk/__init__.py @@ -0,0 +1,17 @@ +"""BodyLoop SDK – Python client for the BodyLoop API.""" + +from __future__ import annotations + +try: + # Python 3.8+ + from importlib.metadata import version as _pkg_version +except ImportError: # pragma: no cover (for very old Pythons) + from importlib_metadata import version as _pkg_version # type: ignore + + +def _read_version() -> str: + # This must match the distribution name in pyproject.toml ([project].name) + return _pkg_version("bodyloop-sdk") + + +__version__ = _read_version() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_bodyloop_sdk.py b/tests/test_bodyloop_sdk.py new file mode 100644 index 0000000..372e829 --- /dev/null +++ b/tests/test_bodyloop_sdk.py @@ -0,0 +1,16 @@ +"""Tests for the bodyloop_sdk package.""" + +import bodyloop_sdk + + +def test_version_exists(): + """The package exposes a __version__ string.""" + assert hasattr(bodyloop_sdk, "__version__") + assert isinstance(bodyloop_sdk.__version__, str) + + +def test_version_format(): + """__version__ follows semver major.minor.patch format.""" + parts = bodyloop_sdk.__version__.split(".") + assert len(parts) == 4 + assert all(part.isdigit() for part in parts) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ea8dec0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,79 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "bodyloop-sdk" +version = "2026.3.5.1" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0.0" }] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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 = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +]