diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml new file mode 100644 index 0000000..d26667c --- /dev/null +++ b/.github/workflows/development.yml @@ -0,0 +1,139 @@ +name: Development + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + quality-checks: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run quality checks + run: tox -e quality + + type-checks: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run quality checks + run: tox -e types + + precommit-checks: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install pre-commit + - name: Run pre-commit checks + run: pre-commit run --all-files + + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run unit tests + run: tox -e test-unit -- -m "smoke or sanity" + + integration-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run integration tests + run: tox -e test-integration -- -m smoke + + build: + runs-on: ubuntu-latest + permissions: + pull-requests: write + strategy: + matrix: + python: ["3.9"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Build the package + run: | + export SPECULATORS_BUILD_TYPE=dev + export SPECULATORS_BUILD_ITERATION=${{ github.event.pull_request.number }} + tox -e build + - name: Upload build artifacts + id: artifact-upload + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: dist/* + compression-level: 6 + if-no-files-found: error + retention-days: 30 + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.GH_NM_REDHAT_AUTOMATION_APP_ID }} + private-key: ${{ secrets.GH_NM_REDHAT_AUTOMATION_APP_PRIVATE_KEY }} + - name: Comment Install instructions + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `📦 **Build Artifacts Available** + The build artifacts (\`.whl\` and \`.tar.gz\`) have been successfully generated and are available for download: ${{ steps.artifact-upload.outputs.artifact-url }}. + They will be retained for **up to 30 days**. + ` + }) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..6c2e498 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,87 @@ +name: Main + +on: + push: + branches: + - main + +jobs: + quality-checks: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run quality checks + run: tox -e quality + + type-checks: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run quality checks + run: tox -e types + + precommit-checks: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install pre-commit + - name: Run pre-commit checks + run: pre-commit run --all-files + + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run unit tests + run: tox -e test-unit -- -m "smoke or sanity" + + integration-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run integration tests + run: tox -e test-integration -- -m smoke diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..c6f006e --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,108 @@ +name: Nightly + +on: + schedule: + - cron: '0 0 * * *' # Runs at midnight every night + +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run unit tests + run: tox -e test-unit -- --cov=speculators --cov-report=term-missing --cov-fail-under=75 + + integration-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run integration tests + run: tox -e test-integration -- -m "smoke or sanity" + + e2e-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run integration tests + run: tox -e test-e2e -- -m smoke + + build-and-publish: + needs: [unit-tests, integration-tests, e2e-tests] + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Build the package + run: | + export SPECULATORS_BUILD_TYPE=nightly + tox -e build + - name: Find wheel artifact + id: find-asset-whl + run: | + echo "::set-output name=asset::$(find dist -name '*.whl')" + - name: Find tar.gz artifact + id: find-asset-targz + run: | + echo "::set-output name=asset::$(find dist -name '*.tar.gz')" + - name: Push wheel to PyPI + uses: neuralmagic/nm-actions/actions/publish-whl@v1.0.0 + with: + username: ${{ secrets.PYPI_PUBLIC_USER }} + password: ${{ secrets.PYPI_PUBLIC_AUTH }} + whl: ${{ steps.find-asset-whl.outputs.asset }} + - name: Push tar.gz to PyPI + uses: neuralmagic/nm-actions/actions/publish-whl@v1.0.0 + with: + username: ${{ secrets.PYPI_PUBLIC_USER }} + password: ${{ secrets.PYPI_PUBLIC_AUTH }} + whl: ${{ steps.find-asset-targz.outputs.asset }} + - name: Upload build artifacts + id: artifact-upload + uses: actions/upload-artifact@v4 + with: + name: nightly-build-artifacts + path: dist/* + compression-level: 6 + if-no-files-found: error + retention-days: 30 + - name: Log artifact location + run: | + echo "Artifacts uploaded to: ${{ steps.artifact-upload.outputs.artifact-url }}" diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 0000000..443d96e --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,101 @@ +name: Release Candidate + +on: + push: + branches: + - 'release/*' + +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run unit tests + run: tox -e test-unit + + integration-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run integration tests + run: tox -e test-integration + + e2e-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run end-to-end tests + run: tox -e test-e2e + + build-and-publish: + needs: [unit-tests, integration-tests, e2e-tests] + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Build the package + run: | + export SPECULATORS_BUILD_TYPE=candidate + tox -e build + - name: Upload build artifacts + id: artifact-upload + uses: actions/upload-artifact@v4 + with: + name: release-candidate-artifacts + path: dist/* + compression-level: 6 + if-no-files-found: error + retention-days: 30 + - name: Log artifact location + run: | + echo "Artifacts uploaded to: ${{ steps.artifact-upload.outputs.artifact-url }}" + - name: Push wheel to PyPI + uses: neuralmagic/nm-actions/actions/publish-whl@v1.0.0 + with: + username: ${{ secrets.PYPI_PUBLIC_USER }} + password: ${{ secrets.PYPI_PUBLIC_AUTH }} + whl: $(find dist -name '*.whl') + - name: Push tar.gz to PyPI + uses: neuralmagic/nm-actions/actions/publish-whl@v1.0.0 + with: + username: ${{ secrets.PYPI_PUBLIC_USER }} + password: ${{ secrets.PYPI_PUBLIC_AUTH }} + whl: $(find dist -name '*.tar.gz') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c6e75a2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,156 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + build-and-publish: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Build the package + run: | + export SPECULATORS_BUILD_TYPE=release + tox -e build + - name: Upload build artifacts + id: artifact-upload + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: dist/* + compression-level: 6 + if-no-files-found: error + retention-days: 90 + - name: Log artifact location + run: | + echo "Artifacts uploaded to: Artifacts uploaded to: ${{ steps.artifact-upload.outputs.artifact-url }}" + - name: Push wheel to PyPI + uses: neuralmagic/nm-actions/actions/publish-whl@v1.0.0 + with: + username: ${{ secrets.PYPI_PUBLIC_USER }} + password: ${{ secrets.PYPI_PUBLIC_AUTH }} + whl: $(find dist -name '*.whl') + - name: Push tar.gz to PyPI + uses: neuralmagic/nm-actions/actions/publish-whl@v1.0.0 + with: + username: ${{ secrets.PYPI_PUBLIC_USER }} + password: ${{ secrets.PYPI_PUBLIC_AUTH }} + whl: $(find dist -name '*.tar.gz') + + update-main-version: + needs: build-and-publish + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.9" + - name: Install dependencies + run: pip install packaging + - name: Set Tag Version + id: set-tag-version + run: echo "tag_version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT + - name: Set setup.py version + id: set-setup-version + run: echo "setup_version=$(grep -oP 'LAST_RELEASE_VERSION = Version\("\K[^"]+' setup.py)" >> $GITHUB_OUTPUT + - name: Check if version needs to be updated + id: check-version + run: | + TAG_VERSION=${{ steps.set-tag-version.outputs.tag_version }} + SETUP_VERSION=${{ steps.set-setup-version.outputs.setup_version }} + if [ "$(python -c "from packaging.version import Version; print(Version('$TAG_VERSION') > Version('$SETUP_VERSION'))")" = "True" ]; then + echo "Version needs to be updated." + echo "update_needed=true" >> $GITHUB_OUTPUT + else + echo "No update needed." + echo "update_needed=false" >> $GITHUB_OUTPUT + fi + - name: Generate GitHub App token + id: app-token + if: steps.check-version.outputs.update_needed == 'true' + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.GH_NM_REDHAT_AUTOMATION_APP_ID }} + private-key: ${{ secrets.GH_NM_REDHAT_AUTOMATION_APP_PRIVATE_KEY }} + - name: Update LAST_RELEASE_VERSION in setup.py + if: steps.check-version.outputs.update_needed == 'true' + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: + TAG_VERSION=${{ steps.set-tag-version.outputs.tag_version }} + SETUP_VERSION=${{ steps.set-setup-version.outputs.setup_version }} + sed -i "s/LAST_RELEASE_VERSION = Version(\"[^\"]*\")/LAST_RELEASE_VERSION = Version(\"$TAG_VERSION\")/" setup.py + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add setup.py + git commit -m "Update LAST_RELEASE_VERSION from $SETUP_VERSION to $TAG_VERSION" + git push origin main + + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run unit tests + run: tox -e test-unit + + integration-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run integration tests + run: tox -e test-integration + + e2e-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install tox + - name: Run end-to-end tests + run: tox -e test-e2e diff --git a/.gitignore b/.gitignore index 3b9d91e..11e39b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# build version files +src/speculators/version.txt +src/speculators/version.py + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d30cb35..5d50982 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,6 +28,8 @@ repos: # dev dependencies pytest, pydantic_settings, + setuptools, + setuptools-git-versioning, # types types-click, diff --git a/MAINFEST.in b/MANIFEST.in similarity index 100% rename from MAINFEST.in rename to MANIFEST.in diff --git a/pyproject.toml b/pyproject.toml index 5917eaf..8349018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,7 @@ [build-system] -requires = ["setuptools >= 61.0", "wheel", "build"] +requires = ["setuptools >= 61.0", "setuptools-git-versioning>=2.0,<3"] build-backend = "setuptools.build_meta" - [tool.setuptools.packages.find] where = ["src"] include = ["*"] @@ -12,8 +11,8 @@ include = ["*"] # ************************************************ [project] +dynamic = ["version"] name = "speculators" -version = "0.1.0" description = "A unified library for creating, representing, and storing speculative decoding algorithms for LLM serving such as in vLLM." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.9" @@ -51,6 +50,11 @@ dependencies = [ [project.optional-dependencies] dev = [ + # build + "build>=1.0.0", + "setuptools>=61.0", + "setuptools-git-versioning>=2.0,<3", + # general and configurations "pre-commit~=3.5.0", "sphinx~=7.1.2", @@ -82,9 +86,9 @@ dev = [ ] [project.urls] -homepage = "https://github.com/neuralmagic/speculators" +homepage = "https://github.com/neuralmagic/speculators" source = "https://github.com/neuralmagic/speculators" -issues = "https://github.com/neuralmagic/speculators/issues" +issues = "https://github.com/neuralmagic/speculators/issues" # ************************************************ # ********** Code Quality Tools ********** @@ -107,7 +111,7 @@ exclude = ["venv", ".tox"] follow_imports = 'silent' [[tool.mypy.overrides]] -module = ["datasets.*", "transformers.*"] +module = ["datasets.*", "transformers.*", "setuptools.*", "setuptools_git_versioning.*"] ignore_missing_imports=true [tool.ruff] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9ab2a22 --- /dev/null +++ b/setup.py @@ -0,0 +1,126 @@ +import os +import re +from pathlib import Path +from typing import Optional, Union + +from packaging.version import Version +from setuptools import setup +from setuptools_git_versioning import count_since, get_branch, get_sha, get_tags + +LAST_RELEASE_VERSION = Version("0.0.1") +TAG_VERSION_PATTERN = re.compile(r"^v(\d+\.\d+\.\d+)$") + + +def get_last_version_diff() -> tuple[Version, Optional[str], Optional[int]]: + """ + Get the last version, last tag, and the number of commits since the last tag. + If no tags are found, return the last release version and None for the tag/commits. + + :returns: A tuple containing the last version, last tag, and number of commits since + the last tag. + """ + tagged_versions = [ + (Version(match.group(1)), tag) + for tag in get_tags(root=Path(__file__).parent) + if (match := TAG_VERSION_PATTERN.match(tag)) + ] + tagged_versions.sort(key=lambda tv: tv[0]) + last_version, last_tag = ( + tagged_versions[-1] if tagged_versions else (LAST_RELEASE_VERSION, None) + ) + commits_since_last = ( + count_since(last_tag + "^{commit}", root=Path(__file__).parent) + if last_tag + else None + ) + + return last_version, last_tag, commits_since_last + + +def get_next_version( + build_type: str, build_iteration: Optional[Union[str, int]] +) -> tuple[Version, Optional[str], int]: + """ + Get the next version based on the build type and iteration. + - build_type == release: take the last version and add a post if build iteration + - build_type == candidate: increment to next minor, add 'rc' with build iteration + - build_type == nightly: increment to next minor, add 'a' with build iteration + - build_type == alpha: increment to next minor, add 'a' with build iteration + - build_type == dev: increment to next minor, add 'dev' with build iteration + + :param build_type: The type of build (release, candidate, nightly, alpha, dev). + :param build_iteration: The build iteration number. If None, defaults to the number + of commits since the last tag or 0 if no commits since the last tag. + :returns: A tuple containing the next version, the last tag the version is based + off of (if any), and the final build iteration used. + """ + version, tag, commits_since_last = get_last_version_diff() + + if not build_iteration and build_iteration != 0: + build_iteration = commits_since_last or 0 + elif isinstance(build_iteration, str): + build_iteration = int(build_iteration) + + if build_type == "release": + if commits_since_last: + # add post since we have commits since last tag + version = Version(f"{version.base_version}.post{build_iteration}") + return version, tag, build_iteration + + # not in release pathway, so need to increment to target next release version + version = Version(f"{version.major}.{version.minor + 1}.0") + + if build_type == "candidate": + # add 'rc' since we are in candidate pathway + version = Version(f"{version}.rc{build_iteration}") + elif build_type in ["nightly", "alpha"]: + # add 'a' since we are in nightly or alpha pathway + version = Version(f"{version}.a{build_iteration}") + else: + # assume 'dev' if not in any of the above pathways + version = Version(f"{version}.dev{build_iteration}") + + return version, tag, build_iteration + + +def write_version_files() -> tuple[Path, Path]: + """ + Write the version information to version.txt and version.py files. + version.txt contains the version string. + version.py contains the version plus additional metadata. + + :returns: A tuple containing the paths to the version.txt and version.py files. + """ + build_type = os.getenv("SPECULATORS_BUILD_TYPE", "dev").lower() + version, tag, build_iteration = get_next_version( + build_type=build_type, + build_iteration=os.getenv("SPECULATORS_BUILD_ITERATION"), + ) + module_path = Path(__file__).parent / "src" / "speculators" + version_txt_path = module_path / "version.txt" + version_py_path = module_path / "version.py" + + with version_txt_path.open("w") as file: + file.write(str(version)) + + with version_py_path.open("w") as file: + file.writelines( + [ + f'version = "{version}"\n', + f'build_type = "{build_type}"\n', + f'build_iteration = "{build_iteration}"\n', + f'git_commit = "{get_sha()}"\n', + f'git_branch = "{get_branch()}"\n', + f'git_last_tag = "{tag}"\n', + ] + ) + + return version_txt_path, version_py_path + + +setup( + setuptools_git_versioning={ + "enabled": True, + "version_file": str(write_version_files()[0]), + } +) diff --git a/tox.ini b/tox.ini index 34da1fc..aba0f7a 100644 --- a/tox.ini +++ b/tox.ini @@ -66,9 +66,10 @@ description = Build the project deps = build setuptools - wheel - loguru - toml + setuptools-git-versioning +setenv = + SPECULATORS_BUILD_TYPE = {env:SPECULATORS_BUILD_TYPE:dev} + SPECULATORS_BUILD_ITERATION = {env:SPECULATORS_BUILD_ITERATION:} commands = python -m build