diff --git a/.darglint b/.darglint new file mode 100644 index 0000000..355d713 --- /dev/null +++ b/.darglint @@ -0,0 +1,3 @@ +[darglint] + +docstring_style=google diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1c0beb9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,44 @@ +name: Publish Python Package + +on: + release: + types: [created] + workflow_dispatch: + +permissions: + contents: read + id-token: write + +concurrency: + group: "publish" + cancel-in-progress: true + +jobs: + publish: + timeout-minutes: 10 + name: Build and publish + + # We don't need to run on all platforms since this package is + # platform-agnostic. The output wheel is something like + # "kbrain--py3-none-any.whl". + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build wheel + + - name: Build package + run: python -m build --sdist --wheel --outdir dist/ . + + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..04aa24d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,65 @@ +name: Python Checks + +on: + push: + branches: + - master + pull_request: + branches: + - master + types: + - opened + - reopened + - synchronize + - ready_for_review + +concurrency: + group: tests-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + run-base-tests: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Restore cache + id: restore-cache + uses: actions/cache/restore@v3 + with: + path: | + ${{ env.pythonLocation }} + .mypy_cache/ + key: python-requirements-${{ env.pythonLocation }}-${{ github.event.pull_request.base.sha || github.sha }} + restore-keys: | + python-requirements-${{ env.pythonLocation }} + python-requirements- + + - name: Install package + run: | + pip install --upgrade --upgrade-strategy eager --extra-index-url https://download.pytorch.org/whl/cpu -e '.[dev]' + + - name: Run static checks + run: | + mkdir -p .mypy_cache + make static-checks + + - name: Run unit tests + run: | + make test + + - name: Save cache + uses: actions/cache/save@v3 + if: github.ref == 'refs/heads/master' + with: + path: | + ${{ env.pythonLocation }} + .mypy_cache/ + key: ${{ steps.restore-cache.outputs.cache-primary-key }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae8e28a --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# .gitignore + +# Python +*.py[oc] +__pycache__/ +*.egg-info +.eggs/ +.mypy_cache/* +.pyre/ +.pytest_cache/ +.ruff_cache/ +.dmypy.json + +# Databases +*.db + +# Build artifacts +build/ +dist/ +*.so +out*/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2681745 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[workspace] + +members = [ + "kbrain/bindings", + "kbrain/rust", +] +resolver = "2" + +[workspace.package] + +version = "0.1.0" +authors = ["Wesley Maa ", "Pawel Budzianowski ", "Benjamin Bolte "] +edition = "2021" +license = "MIT" +repository = "https://github.com/kscalelabs/kbrain" +documentation = "https://docs.kscale.dev/software/actuators/overview" +readme = "README.md" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3efb886 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Benjamin Bolte + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4875cb1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include kbrain/ *.py *.txt py.typed MANIFEST.in diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..77077ef --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# Makefile + +define HELP_MESSAGE +k-brain + +# Installing + +1. Create a new Conda environment: `conda create --name k-brain python=3.11` +2. Activate the environment: `conda activate k-brain` +3. Install the package: `make install-dev` + +# Running Tests + +1. Run autoformatting: `make format` +2. Run static checks: `make static-checks` +3. Run unit tests: `make test` + +endef +export HELP_MESSAGE + +all: + @echo "$$HELP_MESSAGE" +.PHONY: all + +# ------------------------ # +# PyPI Build # +# ------------------------ # + +build-for-pypi: + @pip install --verbose build wheel twine + @python -m build --sdist --wheel --outdir dist/ . + @twine upload dist/* +.PHONY: build-for-pypi + +push-to-pypi: build-for-pypi + @twine upload dist/* +.PHONY: push-to-pypi + +# ------------------------ # +# Static Checks # +# ------------------------ # + +py-files := $(shell find . -name '*.py') + +format: + @black $(py-files) + @ruff format $(py-files) +.PHONY: format + +static-checks: + @black --diff --check $(py-files) + @ruff check $(py-files) + @mypy --install-types --non-interactive $(py-files) +.PHONY: lint + +# ------------------------ # +# Unit tests # +# ------------------------ # + +test: + python -m pytest +.PHONY: test diff --git a/README.md b/README.md new file mode 100644 index 0000000..306e57d --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# k-brain + +Welcome to the k-brain project! diff --git a/kbrain/__init__.py b/kbrain/__init__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/kbrain/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/kbrain/bindings/Cargo.toml b/kbrain/bindings/Cargo.toml new file mode 100644 index 0000000..d760878 --- /dev/null +++ b/kbrain/bindings/Cargo.toml @@ -0,0 +1,29 @@ +[package] + +name = "bindings" +version.workspace = true +edition.workspace = true +description.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +readme.workspace = true + +[lib] + +name = "bindings" +crate-type = ["cdylib", "rlib"] + +[dependencies] + +pyo3 = { version = ">= 0.21.0", features = ["extension-module"] } +pyo3-stub-gen = ">= 0.6.0" +tokio = { version = "1.28.0", features = ["full"] } +async-trait = "0.1.68" +futures = "0.3" +log = "0.4" +env_logger = "0.11.5" +serialport = "4.5.1" + +# Other packages in the workspace. +kbrain = { path = "../rust" } diff --git a/kbrain/py.typed b/kbrain/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/kbrain/requirements-dev.txt b/kbrain/requirements-dev.txt new file mode 100644 index 0000000..e36b648 --- /dev/null +++ b/kbrain/requirements-dev.txt @@ -0,0 +1,7 @@ +# requirements-dev.txt + +black +darglint +mypy +pytest +ruff diff --git a/kbrain/requirements.txt b/kbrain/requirements.txt new file mode 100644 index 0000000..2ee1e7b --- /dev/null +++ b/kbrain/requirements.txt @@ -0,0 +1,2 @@ +# requirements.txt + diff --git a/kbrain/rs/Cargo.toml b/kbrain/rs/Cargo.toml new file mode 100644 index 0000000..234aaa6 --- /dev/null +++ b/kbrain/rs/Cargo.toml @@ -0,0 +1,35 @@ +[package] + +name = "kbrain" +readme = "README.md" +version = "0.0.1" +description = "The brain for K-Scale's humanoid robots" + +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] + +name = "kbrain" +crate-type = ["cdylib", "rlib"] + +[dependencies] + +serialport = "^4.5.1" +ctrlc = "^3.4.5" +lazy_static = "^1.4.0" +spin_sleep = "^1.2.1" +nix = "^0.26.2" +log = "^0.4.22" + +[[bin]] + +name = "motors" +path = "src/bin/motors.rs" + +[dependencies.clap] + +version = "4.3" +features = ["derive"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0c590c8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,72 @@ +[tool.black] + +line-length = 120 +target-version = ["py311"] +include = '\.pyi?$' + +[tool.pytest.ini_options] + +addopts = "-rx -rf -x -q --full-trace" +testpaths = ["tests"] + +markers = [ + "slow: Marks test as being slow", +] + +[tool.mypy] + +pretty = true +show_column_numbers = true +show_error_context = true +show_error_codes = true +show_traceback = true +disallow_untyped_defs = true +strict_equality = true +allow_redefinition = true + +warn_unused_ignores = true +warn_redundant_casts = true + +incremental = true +namespace_packages = false + +# Uncomment to exclude modules from Mypy. +# [[tool.mypy.overrides]] +# module = [] +# ignore_missing_imports = true + +[tool.isort] + +profile = "black" + +[tool.ruff] + +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] + +select = ["ANN", "D", "E", "F", "I", "N", "PGH", "PLC", "PLE", "PLR", "PLW", "W"] + +ignore = [ + "ANN101", "ANN102", + "D101", "D102", "D103", "D104", "D105", "D106", "D107", + "N812", "N817", + "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004", + "PLW0603", "PLW2901", +] + +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.per-file-ignores] + +"__init__.py" = ["E402", "F401", "F403", "F811"] + +[tool.ruff.lint.isort] + +known-first-party = ["kbrain", "tests"] +combine-as-imports = true + +[tool.ruff.lint.pydocstyle] + +convention = "google" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..af3ce38 --- /dev/null +++ b/setup.py @@ -0,0 +1,60 @@ +# mypy: disable-error-code="import-untyped" +#!/usr/bin/env python +"""Setup script for the project.""" + +import re +import subprocess + +from setuptools import find_packages, setup +from setuptools.command.build_ext import build_ext +from setuptools_rust import Binding, RustExtension + +with open("README.md", "r", encoding="utf-8") as f: + long_description: str = f.read() + + +with open("kbrain/requirements.txt", "r", encoding="utf-8") as f: + requirements: list[str] = f.read().splitlines() + + +with open("kbrain/requirements-dev.txt", "r", encoding="utf-8") as f: + requirements_dev: list[str] = f.read().splitlines() + + +with open("kbrain/__init__.py", "r", encoding="utf-8") as fh: + version_re = re.search(r"^__version__ = \"([^\"]*)\"", fh.read(), re.MULTILINE) +assert version_re is not None, "Could not find version in kbrain/__init__.py" +version: str = version_re.group(1) + + +class RustBuildExt(build_ext): + def run(self) -> None: + # Run the stub generator + subprocess.run(["cargo", "run", "--bin", "stub_gen"], check=True) + # Call the original build_ext command + super().run() + + +setup( + name="k-brain", + version=version, + description="The k-brain project", + author="Benjamin Bolte", + url="https://github.com/kscalelabs/kbrain", + rust_extensions=[ + RustExtension( + target="actuator.bindings", + path="actuator/bindings/Cargo.toml", + binding=Binding.PyO3, + ), + ], + setup_requires=["setuptools-rust"], + long_description=long_description, + long_description_content_type="text/markdown", + python_requires=">=3.11", + install_requires=requirements, + tests_require=requirements_dev, + extras_require={"dev": requirements_dev}, + packages=find_packages(include=["actuator"]), + cmdclass={"build_ext": RustBuildExt}, +) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fdc62be --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +"""Defines PyTest configuration for the project.""" + +import random + +import pytest +from _pytest.python import Function + + +@pytest.fixture(autouse=True) +def set_random_seed() -> None: + random.seed(1337) + + +def pytest_collection_modifyitems(items: list[Function]) -> None: + items.sort(key=lambda x: x.get_closest_marker("slow") is not None) diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 0000000..284dabe --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,12 @@ +"""Defines a dummy test.""" + +import pytest + + +def test_dummy() -> None: + assert True + + +@pytest.mark.slow +def test_slow() -> None: + assert True