diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..317a27c --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,20 @@ +name: Code Quality + +on: + push: {} + pull_request: + branches: [ main ] + +jobs: + code-quality-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ vars.CI_PYTHON_VERSION }} + + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d48dbf8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: Release Package + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + +jobs: + release-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Verify current tag is on main branch + run: | + # Exit with error if current tag is not on main + git merge-base --is-ancestor ${{ github.sha }} origin/main || exit 1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ vars.CI_PYTHON_VERSION }} + + - name: Install Poetry + uses: abatilo/actions-poetry@v3 + with: + poetry-version: ${{ vars.CI_POETRY_VERSION }} + + - name: Install Poetry plugins + run: | + poetry self add "poetry-dynamic-versioning[plugin]" + + - name: Build package + run: | + poetry build + ls -la dist/ + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + retention-days: 1 + + pypi-publish: + runs-on: ubuntu-latest + + needs: + - release-build + + permissions: + id-token: write + + environment: + name: pypi + url: https://pypi.org/project/ipybox/ + + steps: + - name: Retrieve release distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish package distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2d2dd17 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: Tests + +on: + push: {} + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Cache conda + uses: actions/cache@v4 + env: + CACHE_NUMBER: 0 + with: + path: ~/conda_pkgs_dir + key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} + + - name: Setup Conda + uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + environment-file: environment.yml + + - name: Install Poetry + uses: abatilo/actions-poetry@v3 + with: + poetry-version: ${{ vars.CI_POETRY_VERSION }} + + - name: Install dependencies + shell: bash -l {0} + run: | + poetry env info + poetry install + pip list + + - name: Run tests + shell: bash -l {0} + run: | + poetry run pytest -s diff --git a/.github/workflows/test_package.yml b/.github/workflows/test_package.yml new file mode 100644 index 0000000..ad1b66d --- /dev/null +++ b/.github/workflows/test_package.yml @@ -0,0 +1,109 @@ +name: Package Installation Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + package-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ vars.CI_PYTHON_VERSION }} + + - name: Install Poetry + uses: abatilo/actions-poetry@v3 + with: + poetry-version: ${{ vars.CI_POETRY_VERSION }} + + - name: Install Poetry plugins + run: | + poetry self add "poetry-dynamic-versioning[plugin]" + + - name: Build package + run: | + poetry build + + - name: Upload dist artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 1 + + package-test: + needs: package-build + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.11', '3.12'] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Download package + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Test wheel installation (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $wheel = Get-ChildItem dist/*.whl | Select-Object -First 1 + pip install $wheel + python -c "import ipybox" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + pip uninstall -y ipybox + + - name: Test wheel installation (Unix) + if: runner.os != 'Windows' + run: | + pip install dist/*.whl + python -c "import ipybox" + pip uninstall -y ipybox + + - name: Test tarball installation (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $tarball = Get-ChildItem dist/*.tar.gz | Select-Object -First 1 + pip install $tarball + python -c "import ipybox" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + pip uninstall -y ipybox + + - name: Test tarball installation (Unix) + if: runner.os != 'Windows' + run: | + pip install dist/*.tar.gz + python -c "import ipybox" + pip uninstall -y ipybox + + - name: Run smoke test (Linux) + if: runner.os == 'Linux' + run: | + pip install dist/*.whl + pip install pytest pytest-asyncio + pytest tests/test_basic.py + pip uninstall -y ipybox diff --git a/pyproject.toml b/pyproject.toml index 80ef522..0861cc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" [tool.poetry] name = "ipybox" -version = "0.3.1" +version = "0.0.0" description = "Python code execution sandbox based on IPython and Docker" homepage = "https://github.com/gradion-ai/ipybox" readme = "README.md" @@ -60,3 +60,9 @@ module = [ "aiofiles.os", ] ignore_missing_imports = true + +[tool.poetry-dynamic-versioning] +enable = true +vcs = "git" +pattern = "default-unprefixed" +style = "pep440" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..879531a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import tempfile + +import pytest + + +@pytest.fixture(scope="module") +async def workspace(): + with tempfile.TemporaryDirectory() as temp_dir: + yield temp_dir diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..71afedb --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,19 @@ +import pytest + +from ipybox import ExecutionClient, ExecutionContainer + + +@pytest.fixture(scope="module") +async def executor(workspace: str): + async with ExecutionContainer( + tag="ghcr.io/gradion-ai/ipybox:minimal", + binds={workspace: "workspace"}, + ) as container: + async with ExecutionClient(host="localhost", port=container.port) as client: + yield client + + +@pytest.mark.asyncio(loop_scope="module") +async def test_basic_functionality(executor): + result = await executor.execute("print('Hello, world!')") + assert result.text == "Hello, world!" diff --git a/tests/ipybox_test.py b/tests/test_ipybox.py similarity index 97% rename from tests/ipybox_test.py rename to tests/test_ipybox.py index 3485739..3791931 100644 --- a/tests/ipybox_test.py +++ b/tests/test_ipybox.py @@ -1,7 +1,6 @@ import asyncio import re import subprocess -import tempfile from pathlib import Path from typing import Generator @@ -12,12 +11,6 @@ from ipybox import DEFAULT_TAG, ExecutionClient, ExecutionContainer, ExecutionError -@pytest.fixture(scope="module") -async def workspace(): - with tempfile.TemporaryDirectory() as temp_dir: - yield temp_dir - - @pytest.fixture( scope="module", params=["test-root", "test"],