diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8efce62 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4134b4c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,70 @@ +name: "Tests" + +on: + push: + branches: [ main ] + pull_request: ~ + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + test-cpython: + name: " + CPython ${{ matrix.python-version }} + " + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ['ubuntu-22.04'] + python-version: ['3.7', '3.13'] + + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + + services: + cratedb: + image: crate/crate:nightly + ports: + - 4200:4200 + + steps: + + - name: Acquire sources + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + cache: 'pip' + cache-dependency-path: | + pyproject.toml + requirements*.txt + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Set up project tools + run: uv pip install --system --requirement=requirements.txt --requirement=requirements-dev.txt + + - name: Run linters and software tests + run: poe test + + # https://github.com/codecov/codecov-action + - name: Upload coverage results to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./coverage.xml + flags: main + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 5998a24..aed59fe 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ .env .idea __pycache__ +.coverage* +coverage.xml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4390d0c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,129 @@ +[tool.ruff] +line-length = 100 + +extend-exclude = [ +] + +lint.select = [ + # Builtins + "A", + # Bugbear + "B", + # comprehensions + "C4", + # Pycodestyle + "E", + # eradicate + "ERA", + # Pyflakes + "F", + # isort + "I", + # pandas-vet + "PD", + # return + "RET", + # Bandit + "S", + # print + "T20", + "W", + # flake8-2020 + "YTT", +] + +lint.extend-ignore = [ + # zip() without an explicit strict= parameter + "B905", + # df is a bad variable name. Be kinder to your future self. + "PD901", + # Unnecessary variable assignment before `return` statement + "RET504", + # Unnecessary `elif` after `return` statement + "RET505", + # Probable insecure usage of temporary file or directory + "S108", + # Possible SQL injection vector through string-based query construction + "S608", +] + +lint.per-file-ignores."examples/*" = [ + "ERA001", # Found commented-out code + "T201", # Allow `print` +] + +# =================== +# Tasks configuration +# =================== +lint.per-file-ignores."tests/*" = [ + "S101", # Allow use of `assert`, and `print`. +] + +[tool.pytest.ini_options] +addopts = """ + -rfEXs -p pytester --strict-markers --verbosity=3 + --cov --cov-report=term-missing --cov-report=xml + """ +minversion = "2.0" +log_level = "DEBUG" +log_cli_level = "DEBUG" +log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s" +xfail_strict = true + + +[tool.coverage.paths2] +source = [ + ".", + "examples", +] + +[tool.coverage.run] +branch = false +source = [ + ".", + "examples", +] +omit = [ + "tests/*", +] + +[tool.coverage.report] +fail_under = 0 +show_missing = true +exclude_lines = [ + "# pragma: no cover", + "raise NotImplemented", +] + + +[tool.poe.tasks] + +check = [ + "lint", + "test", +] + +format = [ + { cmd = "ruff format ." }, + # Configure Ruff not to auto-fix (remove!): + # unused imports (F401), unused variables (F841), `print` statements (T201), and commented-out code (ERA001). + { cmd = "ruff check --fix --ignore=ERA --ignore=F401 --ignore=F841 --ignore=T20 --ignore=ERA001 ." }, + { cmd = "pyproject-fmt --keep-full-version pyproject.toml" }, +] + +lint = [ + { cmd = "ruff format --check ." }, + { cmd = "ruff check ." }, + { cmd = "validate-pyproject pyproject.toml" }, +] + + +[tool.poe.tasks.test] +cmd = "pytest" +help = "Invoke software tests" + +[tool.poe.tasks.test.args.expression] +options = [ "-k" ] + +[tool.poe.tasks.test.args.marker] +options = [ "-m" ] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..e73e8dc --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +poethepoet +pyproject-fmt<3 +pytest<9 +pytest-cov<7 +ruff<0.8 +validate-pyproject<0.23 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..d5dd5f6 --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,46 @@ +import os +import subprocess +from pathlib import Path + +import pytest + + +@pytest.fixture(autouse=True) +def boot(): + """ + Add current working dir to Python module search path. + + This is needed to make the interpreter pick up `cratedb.py` + in the top-level directory. + """ + os.environ["PYTHONPATH"] = str(Path.cwd()) + + +def test_example_usage(capfd): + """ + Validate `examples/example_usage.py` runs to completion. + """ + subprocess.check_call(["python", "examples/example_usage.py"]) + out, err = capfd.readouterr() + assert "Create table" in out + assert "Drop table" in out + + +def test_object_examples(capfd): + """ + Validate `examples/object_examples.py` runs to completion. + """ + subprocess.check_call(["python", "examples/object_examples.py"]) + out, err = capfd.readouterr() + assert "Create table" in out + assert "Drop table" in out + + +def test_picow_demo(capfd): + """ + Validate `examples/picow_demo.py` fails, because it needs real hardware. + """ + returncode = subprocess.call(["python", "examples/picow_demo.py"]) + assert returncode == 1 + out, err = capfd.readouterr() + assert "ModuleNotFoundError: No module named 'machine'" in err