From 0125e0ff2005678c0e088b3f0c53ccc095cbe540 Mon Sep 17 00:00:00 2001 From: Jan Jagusch Date: Mon, 29 Dec 2025 13:37:31 +0100 Subject: [PATCH 1/6] Add failing integration test for dominodatalab dep_updates issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test case reproduces the issue where the dep_updates migrator fails for v1 recipes when grayskull generates a recipe with `skip: match(python, ...)` but the feedstock's variant config only has `python_min` (not `python`). The test adds: - dominodatalab feedstock as a git submodule (pinned to v1.4.7) - Test case that validates version update from 1.4.7 to 2.0.0 - New `assert_pr_body_not_contains` helper method to check PR body - Assertion that the PR body doesn't contain the dep_updates error message The test currently fails, demonstrating the bug where rattler-build skips all variants when the `python` variable is not in the variant config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitmodules | 3 ++ .../lib/_definitions/__init__.py | 3 +- .../lib/_definitions/base_classes.py | 25 +++++++++ .../_definitions/dominodatalab/__init__.py | 52 +++++++++++++++++++ .../dominodatalab/resources/feedstock | 1 + .../lib/_integration_test_helper.py | 21 ++++++++ 6 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests_integration/lib/_definitions/dominodatalab/__init__.py create mode 160000 tests_integration/lib/_definitions/dominodatalab/resources/feedstock diff --git a/.gitmodules b/.gitmodules index ef7d2bd08..968381ebb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "tests_integration/lib/_definitions/zizmor/feedstock"] path = tests_integration/lib/_definitions/zizmor/resources/feedstock url = https://github.com/conda-forge/zizmor-feedstock.git +[submodule "tests_integration/lib/_definitions/dominodatalab/resources/feedstock"] + path = tests_integration/lib/_definitions/dominodatalab/resources/feedstock + url = https://github.com/conda-forge/dominodatalab-feedstock.git diff --git a/tests_integration/lib/_definitions/__init__.py b/tests_integration/lib/_definitions/__init__.py index e28255398..46125e138 100644 --- a/tests_integration/lib/_definitions/__init__.py +++ b/tests_integration/lib/_definitions/__init__.py @@ -1,8 +1,9 @@ -from . import conda_forge_pinning, fastapi, polars, pydantic, zizmor +from . import conda_forge_pinning, dominodatalab, fastapi, polars, pydantic, zizmor from .base_classes import AbstractIntegrationTestHelper, GitHubAccount, TestCase TEST_CASE_MAPPING: dict[str, list[TestCase]] = { "conda-forge-pinning": conda_forge_pinning.ALL_TEST_CASES, + "dominodatalab": dominodatalab.ALL_TEST_CASES, "fastapi": fastapi.ALL_TEST_CASES, "polars": polars.ALL_TEST_CASES, "pydantic": pydantic.ALL_TEST_CASES, diff --git a/tests_integration/lib/_definitions/base_classes.py b/tests_integration/lib/_definitions/base_classes.py index 22953a948..627a760e9 100644 --- a/tests_integration/lib/_definitions/base_classes.py +++ b/tests_integration/lib/_definitions/base_classes.py @@ -189,6 +189,31 @@ def assert_new_run_requirements_equal_v1( """ pass + def assert_pr_body_not_contains( + self, + feedstock: str, + new_version: str, + not_included: list[str], + ): + """ + Assert that the version update PR body does NOT contain certain strings. + + Parameters + ---------- + feedstock + The feedstock we expect the PR for, without the -feedstock suffix. + new_version + The new version that is expected. + not_included + A list of strings that must NOT be present in the PR body. + + Raises + ------ + AssertionError + If any of the strings are found in the PR body. + """ + pass + class TestCase(ABC): """ diff --git a/tests_integration/lib/_definitions/dominodatalab/__init__.py b/tests_integration/lib/_definitions/dominodatalab/__init__.py new file mode 100644 index 000000000..8c4c54b46 --- /dev/null +++ b/tests_integration/lib/_definitions/dominodatalab/__init__.py @@ -0,0 +1,52 @@ +from pathlib import Path + +from fastapi import APIRouter + +from ..base_classes import AbstractIntegrationTestHelper, TestCase + + +class VersionUpdate(TestCase): + """Test case for dominodatalab version update from 1.4.7 to 2.0.0. + + This test case is designed to reproduce the issue where the bot fails + to update dominodatalab with the error: + "Failed to render recipe YAML! No output recipes found!" + """ + + def get_router(self) -> APIRouter: + router = APIRouter() + + @router.get("/pypi.org/pypi/dominodatalab/json") + def handle_pypi_json_api(): + return { + # rest omitted + "info": {"name": "dominodatalab", "version": "2.0.0"} + } + + return router + + def prepare(self, helper: AbstractIntegrationTestHelper): + feedstock_dir = Path(__file__).parent / "resources" / "feedstock" + helper.overwrite_feedstock_contents("dominodatalab", feedstock_dir) + + def validate(self, helper: AbstractIntegrationTestHelper): + helper.assert_version_pr_present_v1( + "dominodatalab", + new_version="2.0.0", + new_hash="05d0f44a89bf0562413018f638839e31bdc108d6ed67869d5ccaceacf41ee237", + old_version="1.4.7", + old_hash="d016b285eba676147b2e4b0c7cc235b71b5a2009e7421281f7246a6ec619342c", + ) + # Assert that the dep_updates migrator didn't fail silently + # If grayskull fails to generate a valid recipe for dependency analysis, + # the PR body will contain this error message + helper.assert_pr_body_not_contains( + "dominodatalab", + new_version="2.0.0", + not_included=[ + "We couldn't run dependency analysis due to an internal error in the bot, depfinder, or grayskull." + ], + ) + + +ALL_TEST_CASES: list[TestCase] = [VersionUpdate()] diff --git a/tests_integration/lib/_definitions/dominodatalab/resources/feedstock b/tests_integration/lib/_definitions/dominodatalab/resources/feedstock new file mode 160000 index 000000000..51a0052f5 --- /dev/null +++ b/tests_integration/lib/_definitions/dominodatalab/resources/feedstock @@ -0,0 +1 @@ +Subproject commit 51a0052f5072a4d77fbe71dbae6094eff74b9999 diff --git a/tests_integration/lib/_integration_test_helper.py b/tests_integration/lib/_integration_test_helper.py index 569dec5a8..dde31556c 100644 --- a/tests_integration/lib/_integration_test_helper.py +++ b/tests_integration/lib/_integration_test_helper.py @@ -263,3 +263,24 @@ def assert_new_run_requirements_equal_v1( pr = self._get_matching_version_pr(feedstock=feedstock, new_version=new_version) recipe = self._get_pr_content_recipe_v1(pr) assert recipe["requirements"]["run"] == run_requirements + + def assert_pr_body_not_contains( + self, + feedstock: str, + new_version: str, + not_included: list[str], + ): + pr = self._get_matching_version_pr(feedstock=feedstock, new_version=new_version) + pr_body = pr.body or "" + + for not_included_str in not_included: + assert not_included_str not in pr_body, ( + f"'{not_included_str}' should NOT be in PR body but was found.\n" + f"PR body:\n{pr_body}" + ) + + LOGGER.info( + "PR body for %s v%s validated successfully (excluded strings not found).", + feedstock, + new_version, + ) From f07f160a38a9c53715fa935ed0c362b0e2240619 Mon Sep 17 00:00:00 2001 From: Jan Jagusch Date: Mon, 29 Dec 2025 13:43:29 +0100 Subject: [PATCH 2/6] Add unit test for grayskull v1 recipe python_min mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test reproduces the issue where get_grayskull_comparison fails for v1 recipes when grayskull generates `skip: match(python, "<3.10")` but the variant config only has `python_min` (not `python`). The test is marked as xfail since it currently fails - rattler-build skips all variants when the `python` variable is not set in the variant config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/test_update_deps.py | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_update_deps.py b/tests/test_update_deps.py index c172ba552..79c89365c 100644 --- a/tests/test_update_deps.py +++ b/tests/test_update_deps.py @@ -1146,3 +1146,59 @@ def test_jsii_package_name_resolution(): resolved_name = _modify_package_name_from_github(feedstock_package_name, src) assert resolved_name == "jsii" + + +@pytest.mark.xfail( + reason="Grayskull generates skip: match(python, ...) but variant config only has python_min" +) +def test_get_grayskull_comparison_v1_python_min_mismatch(): + """Test that get_grayskull_comparison works for v1 recipes using python_min. + + This test reproduces the issue where grayskull generates a recipe with + `skip: match(python, "<3.10")` but the feedstock's variant config only has + `python_min` (not `python`). When rattler-build tries to render the recipe, + it skips all variants because the `python` variable is not set. + + See: https://github.com/conda-forge/dominodatalab-feedstock/pull/21 + """ + attrs = { + "meta_yaml": { + "schema_version": 1, + "package": { + "name": "dominodatalab", + "version": "1.4.7", + }, + "source": { + "url": "https://pypi.org/packages/source/d/dominodatalab/dominodatalab-1.4.7.tar.gz", + }, + "build": {"noarch": "python"}, + }, + "feedstock_name": "dominodatalab", + "version_pr_info": {"version": "2.0.0"}, + "total_requirements": { + "build": set(), + "host": {"pip", "python"}, + "run": { + "python", + "packaging", + "requests >=2.4.2", + "beautifulsoup4 >=4.11,<4.12", + "polling2 >=0.5.0,<0.6", + "urllib3 >=1.26.12,<1.27", + "typing-extensions >=4.5.0", + "frozendict >=2.3.4,<2.4", + "python-dateutil >=2.8.2,<2.9", + "retry ==0.9.2", + }, + "test": set(), + }, + } + + # This should not raise an exception, but currently it does because + # grayskull generates `skip: match(python, "<3.10")` and the variant + # config only has `python_min`, causing rattler-build to skip all variants. + dep_comparison, recipe = get_grayskull_comparison(attrs=attrs) + + # If we get here, the comparison should have valid results + assert "run" in dep_comparison + assert recipe != "" From ebda86a1208e11c329970413b940f01063c50955 Mon Sep 17 00:00:00 2001 From: Jan Jagusch Date: Mon, 29 Dec 2025 14:11:59 +0100 Subject: [PATCH 3/6] Remove xfail marker from unit test to ensure CI fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The xfail marker was causing pytest to consider the failing test as "passing" (expected failure). Removing it ensures the test actually fails in CI until the bug is fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/test_update_deps.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_update_deps.py b/tests/test_update_deps.py index 79c89365c..460ca7d86 100644 --- a/tests/test_update_deps.py +++ b/tests/test_update_deps.py @@ -1148,9 +1148,6 @@ def test_jsii_package_name_resolution(): assert resolved_name == "jsii" -@pytest.mark.xfail( - reason="Grayskull generates skip: match(python, ...) but variant config only has python_min" -) def test_get_grayskull_comparison_v1_python_min_mismatch(): """Test that get_grayskull_comparison works for v1 recipes using python_min. From 0eff808af608082ff00ee878657527d88b53f07b Mon Sep 17 00:00:00 2001 From: Jan Jagusch Date: Mon, 29 Dec 2025 14:38:29 +0100 Subject: [PATCH 4/6] Fix grayskull v1 recipe rendering for python_min recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `match(python, ...)` with `match(python_min, ...)` in grayskull-generated v1 recipes. This fixes the dep_updates migrator for feedstocks using CFEP-25's python_min variable. Grayskull generates skip conditions like `skip: match(python, "<3.10")` but v1 recipe variant configs only define `python_min`, not `python`. This caused rattler-build to skip all variants with: "Failed to render recipe YAML! No output recipes found!" See: https://github.com/conda/grayskull/issues/574 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- conda_forge_tick/update_deps.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/conda_forge_tick/update_deps.py b/conda_forge_tick/update_deps.py index 8f91462bb..ce0d74a84 100644 --- a/conda_forge_tick/update_deps.py +++ b/conda_forge_tick/update_deps.py @@ -458,7 +458,14 @@ def _make_grayskull_recipe_v1( is_arch=not package_is_noarch, ) _validate_grayskull_recipe_v1(recipe=recipe) - return _generate_grayskull_recipe_v1(recipe=recipe, configuration=config) + recipe_str = _generate_grayskull_recipe_v1(recipe=recipe, configuration=config) + + # Grayskull generates `match(python, ...)` for noarch recipes, but v1 recipes + # use `python_min` in their variant configs (CFEP-25). Replace to make the + # recipe renderable. See: https://github.com/conda/grayskull/issues/574 + recipe_str = recipe_str.replace("match(python,", "match(python_min,") + + return recipe_str def get_grayskull_comparison(attrs, version_key="version"): From 09ca7cc92e962e500984752cb075cd262ecf0328 Mon Sep 17 00:00:00 2001 From: Jan Jagusch Date: Mon, 29 Dec 2025 17:07:27 +0100 Subject: [PATCH 5/6] fix: complete integration test mock infrastructure for dominodatalab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add mock endpoint for version-specific PyPI API (/pypi/dominodatalab/2.0.0/json) - Include 'urls' field in main PyPI endpoint for grayskull sdist discovery - Add spdx.org/* to transparent URLs (grayskull license discovery) - Store full PyPI metadata response for reproducible testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../_definitions/dominodatalab/__init__.py | 19 ++- .../pypi_version_json_response.json | 133 ++++++++++++++++++ tests_integration/lib/_shared.py | 1 + 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tests_integration/lib/_definitions/dominodatalab/pypi_version_json_response.json diff --git a/tests_integration/lib/_definitions/dominodatalab/__init__.py b/tests_integration/lib/_definitions/dominodatalab/__init__.py index 8c4c54b46..fb8b408d4 100644 --- a/tests_integration/lib/_definitions/dominodatalab/__init__.py +++ b/tests_integration/lib/_definitions/dominodatalab/__init__.py @@ -1,3 +1,4 @@ +import json from pathlib import Path from fastapi import APIRouter @@ -18,11 +19,25 @@ def get_router(self) -> APIRouter: @router.get("/pypi.org/pypi/dominodatalab/json") def handle_pypi_json_api(): + # Must include 'urls' with sdist info for grayskull to find the package return { - # rest omitted - "info": {"name": "dominodatalab", "version": "2.0.0"} + "info": {"name": "dominodatalab", "version": "2.0.0"}, + "urls": [ + { + "packagetype": "sdist", + "url": "https://files.pythonhosted.org/packages/d8/6d/1e321187451c1cc1670e615497474f9c54f04ad5f4ff7e831ea2dc3eeb23/dominodatalab-2.0.0.tar.gz", + } + ], } + @router.get("/pypi.org/pypi/dominodatalab/2.0.0/json") + def handle_pypi_version_json_api(): + return json.loads( + Path(__file__) + .parent.joinpath("pypi_version_json_response.json") + .read_text() + ) + return router def prepare(self, helper: AbstractIntegrationTestHelper): diff --git a/tests_integration/lib/_definitions/dominodatalab/pypi_version_json_response.json b/tests_integration/lib/_definitions/dominodatalab/pypi_version_json_response.json new file mode 100644 index 000000000..ea0f43c47 --- /dev/null +++ b/tests_integration/lib/_definitions/dominodatalab/pypi_version_json_response.json @@ -0,0 +1,133 @@ +{ + "info": { + "author": "Domino Data Lab", + "author_email": "support@dominodatalab.com", + "bugtrack_url": null, + "classifiers": [], + "description": "This library provides bindings for the Domino APIs. It ships with the Domino Standard Environment (DSE).\n\nSee this documentation for details about the APIs:\n\n- [Latest public Domino\n APIs](https://docs.dominodatalab.com/en/latest/api_guide/8c929e/domino-public-apis/)\n\n- [Legacy APIs](https://dominodatalab.github.io/api-docs/)\n\nThe latest released version of `python-domino` is `2.0.0`.\n\n# Version compatibility matrix\n\nThe `python-domino` library is compatible with different versions of\nDomino:\n\n| Domino Versions | Python-Domino |\n|-----------------|:-----------------------------------------------------------------------------------------------------:|\n| 3.6.x or lower | [0.3.5](https://github.com/dominodatalab/python-domino/archive/0.3.5.zip) |\n| 4.1.0 or higher | [1.0.0](https://github.com/dominodatalab/python-domino/archive/refs/tags/1.0.0.zip) or Higher |\n| 5.3.0 or higher | [1.2.0](https://github.com/dominodatalab/python-domino/archive/refs/tags/Release-1.2.0.zip) or Higher |\n| 5.5.0 or higher | [1.2.2](https://github.com/dominodatalab/python-domino/archive/refs/tags/Release-1.2.2.zip) or Higher |\n| 5.10.0 or higher | [1.3.1](https://github.com/dominodatalab/python-domino/archive/refs/tags/Release-1.3.1.zip) or Higher |\n| 5.11.0 or higher | [1.4.1](https://github.com/dominodatalab/python-domino/archive/refs/tags/Release-1.4.1.zip) or Higher |\n| 6.0.0 or higher | [1.4.8](https://github.com/dominodatalab/python-domino/archive/refs/tags/Release-1.4.8.zip) or Higher |\n| 6.2.0 or higher | [2.0.0](https://github.com/dominodatalab/python-domino/archive/refs/tags/Release-2.0.0.zip) or Higher |\n\n# Development\n\nThe current `python-domino` is based on Python 3.10, which is therefore recommended for development. `Pipenv` is also recommended to manage the dependencies.\n\nTo use the Python binding in a Domino workbook session, include `dominodatalab` in your project's requirements.txt file.\nThis makes the Python binding available for each new workbook session (or batch run) started within the project.\n\nTo install dependencies from `setup.py` for development:\n\n pipenv --python 3.10 install -e \".[dev]\"\n\nUse the same process for Airflow, agents, and data:\n\n pipenv --python 3.10 install -e \".[data]\" \".[airflow]\" \".[agents]\"\n\n# Set up the connection\n\nYou can set up the connection by creating a new instance of `Domino`:\n\n _class_ Domino(project, api_key=None, host=None, domino_token_file=None, auth_token=None)\n\n- *project:* A project identifier (in the form of ownerusername/projectname).\n \n- *api_proxy:* (Optional) Location of the Domino API reverse proxy as host:port.\n\n If set, this proxy is used to intercept any Domino API requests and insert an authentication token.\n _This is the preferred method of authentication_. \n Alternatively, set the `DOMINO_API_PROXY` environment variable.\n In Domino 5.4.0 or later, this variable is set inside a Domino run container.\n\n NOTE: This mechanism does not work when connecting to an HTTPS endpoint; it is meant to be used inside Domino runs.\n\n- *api_key:* (Optional) An API key to authenticate with. \n\n If not provided, the library expects to find one in the `DOMINO_USER_API_KEY` environment variable.\n If you are using the Python package in code that is already running in Domino, the `DOMINO_API_USER_KEY` variable is set automatically to be the key for the user who started the run.\n\n- *host:* (Optional) A host URL. \n\n If not provided, the library expects to find one in the `DOMINO_API_HOST` environment variable.\n\n- *domino_token_file:* (Optional) Path to the Domino token file\n containing the auth token. \n\n If not provided, the library expects to find one in the `DOMINO_TOKEN_FILE` environment variable.\n If you are using Python package in code that is already running in Domino, the `DOMINO_TOKEN_FILE` is set automatically to be the token file for the user who started the run.\n\n- *auth_token:* (Optional) Authentication token.\n\n## Authentication\n\nDomino looks for the authentication method in the following order and uses the first one it finds:\n\n1. `api_proxy`\n2. `auth_token`\n3. `domino_token_file`\n4. `api_key`\n5. `DOMINO_API_PROXY`\n6. `DOMINO_TOKEN_FILE`\n7. `DOMINO_USER_API_KEY`\n\nThe API proxy is the preferred method of authentication.\nSee \n[Use the API Proxy to Authenticate Calls to the Domino API](https://docs.dominodatalab.com/en/latest/user_guide/40b91f/domino-api/).\n\n## Additional environment variables\n\n- `DOMINO_LOG_LEVEL`\n \n The default log level is `INFO`.\n You can change the log level by setting `DOMINO_LOG_LEVEL`, for example to `DEBUG`.\n\n- `DOMINO_VERIFY_CERTIFICATE`\n \n For testing purposes and issues with SSL certificates, set `DOMINO_VERIFY_CERTIFICATE` to `false`. \n Be sure to unset this variable when not in use.\n\n- `DOMINO_MAX_RETRIES`\n \n Default Retry is set to 4 \n Determines the number of attempts for the request session in case of a ConnectionError\n Get more info on request max timeout/error durations based on Retry and backoff factors [here](https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html#module-urllib3.util.retry)\n\n- `MLFLOW_TRACKING_URI`\n\n Must be set for the `domino.agents` package to work properly. This will be set automatically in executions, e.g. Jobs, Workspaces, Scheduled Jobs, etc.\n For testing, the value must be set on the command line to \"http://localhost:5000\".\n\n# Working with docs\n\n## building locally\n\n- Install the app in dev mode and all dependencies and pandoc: `brew install pandoc ghc cabal-install haskell-stack` and `cabal update && cabal install --lib pandoc-types --package-env .`\n- install dependencies in this order: `pipenv --python 3.10 install -e \".[dev]\" && pipenv --python 3.10 install -e \".[docs]\" && pipenv --python 3.10 install -e \".[data]\" \".[airflow]\" \".[agents]\"`\n- Build: `./docs/build.sh`\n- View: open docs/build/html/index.html\n- Manually pick the changes you want and add to `README.adoc` and `README.md`. Update the styling of the docs as needed.\n\n## transform agents docs for README\n\n- Run:\n`./docs/transform_agents_docs_for_readme.sh`\n- copy/paste the outputs for asciidoc and markdown into respective README files\n\n# Methods\n\n# Budgets and Billing Tags\n\nSee\n[`example_budget_manager.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/example_budget_manager.py)\nfor example code.\n\n### budget_defaults_list()\n\nGet a list of the available default budgets with the assigned (if any) limits\nRequires Admin permission\n\n### budget_defaults_update(budget_label, budget_limit)\n\nUpdate default budgets by BudgetLabel\nRequires Admin roles\n\n- *budget_label:* (required) label of budget to be updated ex: `BillingTag`, `Organization`\n\n- *budget_limit:* (required) new budget quota to assign to default label\n\n### budget_overrides_list()\n\nGet a list of the available budgets overrides with the assigned limits.\nRequires Admin permission\n\n### budget_override_create(budget_label, budget_id, budget_limit)\n\nCreate Budget overrides based on BudgetLabels, ie BillingTags, Organization, or Projects\nthe object id is used as budget ids\nRequires Admin roles\n\n- *budget_label:* label of budget to be updated\n\n- *budget_id:* id of project or organization to be used as new budget override id.\n\n- *budget_limit:* budget quota to assign to override\n\n### budget_override_update(budget_label, budget_id, budget_limit)\n\nUpdate Budget overrides based on BudgetLabel and budget id\nRequires Admin roles\n\n- *budget_label:* label of budget to be updated\n\n- *budget_id:* id of budget override to be updated.\n\n- *budget_limit:* new budget quota to assign to override\n\n### budget_override_delete(budget_id)\n\nDelete an existing budget override\nRequires Admin roles\n\n- *budget_id:* id of budget override to be deleted.\n\n### budget_alerts_settings()\n\nGet the current budget alerts settings\nRequires Admin permission\n\n### budget_alerts_settings_update(alerts_enabled, notify_org_owner)\n\nUpdate the current budget alerts settings to enable/disable budget notifications\nand whether to notify org owners on projects notifications\nRequires Admin permission\n\n- *alerts_enabled:* whether to enable or disable notifications.\n\n- *notify_org_owner:* whether to notify organizations owners on projects reaching threshold.\n\n### budget_alerts_targets_update(targets)\n\nUpdate the current budget alerts settings with additional email targets per budget label\nRequires Admin permission\n\n- *targets:* dictionary of budget labels and list of email addresses\n\n### billing_tags_list_active()\n\nGet a list of active billing tags\nRequires Admin permission\n\n### billing_tags_create(tags_list)\n\nCreate a list of active billing tags\nRequires Admin permission\n\n- *tags_list:* list of billing tags names to be created\n\n### active_billing_tag_by_name(name)\n\nGet detailed info on active or archived billing tag\nRequires Admin permission\n\n- *name:* name of existing billing tag\n\n### billing_tag_archive(name)\n\nArchive an active billing tag\nRequires Admin permission\n\n- *name:* name of existing billing tag to archive\n\n### billing_tag_settings()\n\nGet the current billing tag settings\nRequires Admin permission\n\n### billing_tag_settings_mode()\n\nGet the current billing tag settings mode\nRequires Admin permission\n\n### billing_tag_settings_mode_update(mode)\n\nUpdate the current billing tag settings mode\nRequires Admin permission\n\n- *mode:* new mode to set the billing tag settings (see BillingTagSettingMode)\n\n### project_billing_tag(project_id)\n\nGet a billing tag assigned to a particular project by project id\nRequires Admin permission\n\n- *project_id:* id of the project to find assigned billing tag\n\n### project_billing_tag_update(billing_tag, project_id)\n\nUpdate project's billing tag with new billing tag.\nRequires Admin permission\n\n- *billing_tag:* billing tag to assign to a project\n\n- *project_id:* id of the project to assign a billing tag\n\n### project_billing_tag_reset(project_id)\n\nRemove a billing tag from a specified project\nRequires Admin permission\n\n- *project_id:* id of the project to reset billing tag field\n\n### projects_by_billing_tag( billing_tag, offset, page_size, name_filter, sort_by, sort_order, missing_tag_only=False)\n\nRemove a billing tag from a specified project\nRequires Admin permission\n\n- *billing_tag:* billing tag string to filter projects by\n\n- *offset:* The index of the start of the page, where checkpointProjectId is index 0.\nIf the offset is negative the project it points to will be the end of the page.\n- *page_size:* The number of records to return per page.\n\n- *name_filter:* matches projects by name substring\n\n- *sort_by:* (Optional) field to sort the projects on\n\n- *sort_order:* (Optional) Whether to sort in asc or desc order\n\n- *missing_tag_only:* (Optional) determine whether to only return projects with missing tag\n\n### project_billing_tag_bulk_update(projects_tag)\n\nUpdate project's billing tags in bulk\nRequires Admin permission\n\n- *projects_tag:* dictionary of project_id and billing_tags\n\n\n## Projects\n\nSee\n[`example_projects_usage.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/example_projects_usage.py)\nfor example code.\n\n### project_create_v4(project_name, owner_id, owner_username, description, collaborators, tags, billing_tag, visibility=PUBLIC)\n\nNewer version of projects creation using the v4 endpoints which allows more optional fields.\n\n- *project_name:* (required) The name of the project.\n\n- *owner_id:* (Optional) user id of the owner of the new project to be created (must be admin to create projects for other users)\n owner_id or owner_username can be used, both are not needed (Defaults to current owner_username)\n\n- *owner_username:* (Optional) username of the owner of the new project to be created (must be admin to create projects for other users)\n owner_id or owner_username can be used, both are not needed (Defaults to current owner_username)\n\n- *description:* (Optional) description of the project\n\n- *collaborators:* (Optional) list of collaborators to be added to the project\n\n- *tags:* (Optional) list of tags to add to project\n\n- *billing_tag:* (Optional unless billingTag settings mode is Required) active billing tag to be added to projects for governance\n\n- *visibility:* (Optional) (Defaults to Public) project visibility \n\n### project_create(project_name, owner_username=None)\n\nCreate a new project with given project name.\n\n- *project_name:* The name of the project.\n\n- *owner_username:* (Optional) The owner username for the project.\n This parameter is useful when you need to create a project under an\n organization.\n\n### collaborators_get()\n\nGet the list of collaborators on a project.\n\n### collaborators_add(username_or_email, message=\"\")\n\nAdd collaborators to a project.\n\n- *username_or_email:* Name or email of the Domino user to add as\n collaborator to the current project.\n\n- *message:* Optional - Message related to the user\u2019s role or purpose\n to the project.\n\n## Project tags\n\nProject tags are an easy way to add freeform metadata to a project. Tags\nhelp colleagues and consumers organize and find the Domino projects that\ninterest them. Tags can be used to describe the subject explored by a\nproject, the packages and libraries it uses, or the source of the data\nwithin.\n\nSee\n[`example_projects_usage.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/example_projects_usage.py)\nfor example code.\n\n### tags_list(\\*project_id)\n\nList a project\u2019s tags.\n\n- *project_id:* The project identifier.\n\n### tag_details(tag_id)\n\nGet details about a tag.\n\n- *tag_id:* The tag identifier.\n\n### tags_add(tags, \\*project_id)\n\nCreate a tag, if it does not exist, and add it to a project.\n\n- *tags (list):* One or more tag names.\n\n- *project_id:* (Defaults to current project ID) The project\n identifier.\n\n### tag_get_id(tag_name, \\*project_id)\n\nGet the tag ID using the tag string name.\n\n- *tag_name (string):* The tag name.\n\n- *project_id:* (Defaults to current project id) The project ID.\n\n### tags_remove(tag_name, project_id=None)\n\nRemove a tag from a project.\n\n- *tag_name (string):* The tag name.\n\n- *project_id:* (Defaults to current project id) The project ID.\n\n## Executions\n\nSee these code example files:\n\n- [`start_run_and_check_status.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/start_run_and_check_status.py)\n\n- [`export_runs.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/export_runs.py)\n\n### runs_list()\n\nList the executions on the selected project.\n\n### runs_start(command, isDirect, commitId, title, tier, publishApiEndpoint)\n\nStart a new execution on the selected project.\n\n- *command:* The command to execution as an array of strings where\n members of the array represent arguments of the command. For\n example: `[\"main.py\", \"hi mom\"]`\n\n- *isDirect:* (Optional) Whether this command should be passed\n directly to a shell.\n\n- *commitId:* (Optional) The `commitId` to launch from. If not\n provided, the project launches from the latest commit.\n\n- *title:* (Optional) A title for the execution.\n\n- *tier:* (Optional) The hardware tier to use for the execution. This\n is the human-readable name of the hardware tier, such as \"Free\",\n \"Small\", or \"Medium\". If not provided, the project\u2019s default tier is\n used.\n\n- *publishApiEndpoint:* (Optional) Whether to publish an API endpoint\n from the resulting output.\n\n### runs_start_blocking(command, isDirect, commitId, title, tier, publishApiEndpoint, poll_freq=5, max_poll_time=6000)\n\nStart a new execution on the selected project and make a blocking\nrequest that waits until job is finished.\n\n- *command:* The command to execution as an array of strings where\n members of the array represent arguments of the command. For\n example: `[\"main.py\", \"hi mom\"]`\n\n- *isDirect:* (Optional) Whether this command should be passed\n directly to a shell.\n\n- *commitId:* (Optional) The `commitId` to launch from. If not\n provided, the project launches from the latest commit.\n\n- *title:* (Optional) A title for the execution.\n\n- *tier:* (Optional) The hardware tier to use for the execution. Will\n use project\u2019s default tier if not provided. If not provided, the\n project\u2019s default tier is used.\n\n- *publishApiEndpoint:* (Optional) Whether to publish an API endpoint\n from the resulting output.\n\n- *poll_freq:* (Optional) Number of seconds between polling of the\n Domino server for status of the task that is running.\n\n- *max_poll_time:* (Optional) Maximum number of seconds to wait for\n a task to complete. If this threshold is exceeded, an exception is\n raised.\n\n- *retry_count:* (Optional) Maximum number of polling retries (in\n case of transient HTTP errors). If this threshold is exceeded, an\n exception is raised.\n\n### run_stop(runId, saveChanges=True):\n\nStop an existing execution in the selected project.\n\n- *runId:* String that identifies the execution.\n\n- *saveChanges:* (Defaults to True) If false, execution results are\n discarded.\n\n### runs_stdout(runId)\n\nGet `stdout` emitted by a particular execution.\n\n- *runId:* string that identifies the execution\n\n## Files and blobs\n\nSee these code example files:\n\n- [`upload_file.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/upload_file.py)\n\n- [`upload_and_run_file_and_download_results.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/upload_and_run_file_and_download_results.py)\n\n### files_list(commitId, path)\n\nList the files in a folder in the Domino project.\n\n- *commitId:* The `commitId` to list files from.\n\n- *path:* (Defaults to \"/\") The path to list from.\n\n### files_upload(path, file)\n\nUpload a Python file object into the specified path inside the project.\nSee `examples/upload_file.py` for an example. All parameters are\nrequired.\n\n- *path:* The path to save the file to. For example, `/README.md`\n writes to the root directory of the project while\n `/data/numbers.csv` saves the file to a sub folder named `data`. If\n the specified folder does not yet exist, it is created.\n\n- *file:* A Python file object. For example:\n `f = open(\"authors.txt\",\"rb\")`\n\n### blobs_get(key)\n\n*_Deprecated_* Retrieve a file from the Domino server by blob key. Use blobs_get_v2(path, commit_id, project_id) instead.\n\n- *key:* The key of the file to fetch from the blob server.\n\n### blobs_get_v2(path, commit_id, project_id)\n\nRetrieve a file from the Domino server in a project from its path and commit id.\n\n- *path:* The path to the file in the Domino project.\n- *commit_id:* ID of the commit to retrieve the file from.\n- *project_id:* ID of the project to retrieve the file from.\n\n## Apps\n\n### app_publish(unpublishRunningApps=True, hardwareTierId=None)\n\nPublish an app within a project, or republish an existing app.\n\n- *unpublishRunningApps:* (Defaults to True) Check for an active app\n instance in the current project and unpublish it before\n re/publishing.\n\n- *hardwareTierId:* (Optional) Launch the app on the specified\n hardware tier.\n\n### app_unpublish()\n\nStop the running app in the project.\n\n## Jobs\n\n### job_start(command, commit_id=None, hardware_tier_name=None, environment_id=None, on_demand_spark_cluster_properties=None, compute_cluster_properties=None, external_volume_mounts=None, title=None):\n\nStart a new job (execution) in the project.\n\n- *command (string):* Command to execute in Job. For example:\n `domino.job_start(command=\"main.py arg1 arg2\")`\n\n- *commit_id (string):* (Optional) The `commitId` to launch from. If\n not provided, the job launches from the latest commit.\n\n- *hardware_tier_name (string):* (Optional) The hardware tier NAME\n to launch job in. If not provided, the project\u2019s default tier is\n used.\n\n- *environment_id (string):* (Optional) The environment ID with which\n to launch the job. If not provided, the project\u2019s default\n environment is used.\n\n- *on_demand_spark_cluster_properties (dict):* (Optional) On\n demand spark cluster properties. The following properties can be\n provided in the Spark cluster:\n\n {\n \"computeEnvironmentId\": \"\"\n \"executorCount\": \"\"\n (optional defaults to 1)\n \"executorHardwareTierId\": \"\"\n (optional defaults to last used historically if available)\n \"masterHardwareTierId\": \"\"\n (optional defaults to 0; 1GB is 1000MB Here)\n }\n\n- *param compute_cluster_properties (dict):* (Optional) The\n compute-cluster properties definition contains parameters for\n launching any Domino supported compute cluster for a job. Use this\n to launch a job that uses a compute-cluster instead of the\n deprecated `on_demand_spark_cluster_properties` field. If\n `on_demand_spark_cluster_properties` and\n `compute_cluster_properties` are both present,\n `on_demand_spark_cluster_properties` is ignored.\n `compute_cluster_properties` contains the following fields:\n\n {\n \"clusterType\": ,\n \"computeEnvironmentId\": ,\n \"computeEnvironmentRevisionSpec\": \"} (optional)>,\n \"masterHardwareTierId\": ,\n \"workerCount\": ,\n \"workerHardwareTierId\": ,\n \"workerStorage\": <{ \"value\": , \"unit\": },\n The disk storage size for the cluster's worker nodes (optional)>\n \"maxWorkerCount\": \n }\n\n- *external_volume_mounts (List\\[string\\]):* (Optional) External\n volume mount IDs to mount to execution. If not provided, the job\n launches with no external volumes mounted.\n\n- *title (string): (Optional) Title for Job.\n\n### job_stop(job_id, commit_results=True):\n\nStop the Job (execution) in the project.\n\n- *job_id (string):* Job identifier.\n\n- *commit_results (boolean):* (Defaults to `true`) If `false`, the\n job results are not committed.\n\n### jobs_list(self, project_id: str, order_by: str = \"number\", sort_by: str = \"desc\", page_size: Optional[int] = None, page_no: int = 1, show_archived: str = \"false\", status: str = \"all\", tag: Optional[str] = None):\n\nLists job history for a given project_id\n\n- *project_id (string):* The project to query\n\n- *order_by (string):* Field on which sort has to be applied\u2013 e.g. \"title\" (default \"number\")\n\n- *sort_by (string):* Sort \"desc\" (default) or \"asc\"\n\n- *page_size (integer):* The number of jobs to return (default: 3)\n\n- *page_no (integer):* Page number to fetch (default: 1).\n\n- *show_archived (string):* Show archived jobs in results (default: false)\n\n- *status (string):* Status of jobs to fetch\u2013 e.g. \"completed\" (default: \"all\")\n\n- *tag (string):* Optional tag filter\n\n### job_status(job_id):\n\nGet the status of a job.\n\n- *job_id (string):* Job identifier.\n\n### job_restart(job_id, should_use_original_input_commit=True):\n\nRestart a previous job\n\n- *job_id (string):* ID of the original job. This can be obtained with `jobs_list()`.\n\n- *should_use_original_input_commit (bool):* Should the new job run use the original code, or the current version?\n\n### job_start_blocking(poll_freq=5, max_poll_time=6000, \\*\\*kwargs):\n\nStart a job and poll until the job is finished. Additionally, this\nmethod supports all the parameters in the `job_start` method.\n\n- *poll_freq:* Poll frequency interval in seconds.\n\n- *max_poll_time:* Max poll time in seconds.\n\n## Datasets\n\nA Domino dataset is a collection of files that are available in user\nexecutions as a filesystem directory. A dataset always reflects the most\nrecent version of the data. You can modify the contents of a dataset\nthrough the Domino UI or through workload executions.\n\nSee [Domino\nDatasets](https://docs.dominodatalab.com/en/latest/user_guide/0a8d11/datasets-overview/)\nfor more details, and\n[`example_dataset.py`](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/example_dataset.py)\nfor example code.\n\n### datasets_list(project_id=None)\n\nProvide a JSON list of all the available datasets.\n\n- *project_id (string):* (Defaults to None) The project identifier.\n Each project can hold up to 5 datasets.\n\n### datasets_ids(project_id)\n\nList the IDs the datasets for a particular project.\n\n- *project_id:* The project identifier.\n\n### datasets_names(project_id)\n\nList the names the datasets for a particular project.\n\n- *project_id:* The project identifier.\n\n### datasets_details(dataset_id)\n\nProvide details about a dataset.\n\n- *dataset_id:* The dataset identifier.\n\n### datasets_create(dataset_name, dataset_description)\n\nCreate a new dataset.\n\n- *dataset_name:* Name of the new dataset. NOTE: The name must be\n unique.\n\n- *dataset_description:* Description of the dataset.\n\n### datasets_update_details(dataset_id, dataset_name=None, dataset_description=None)\n\nUpdate a dataset\u2019s name or description.\n\n- *dataset_id:* The dataset identifier.\n\n- *dataset_name:* (Optional) New name of the dataset.\n\n- *dataset_description:* (Optional) New description of the dataset.\n\n### datasets_remove(dataset_ids)\n\nDelete a set of datasets.\n\n- *dataset_ids (list\\[string\\]):* List of IDs of the datasets to\n delete. NOTE: Datasets are first marked for deletion, then deleted\n after a grace period (15 minutes, configurable). A Domino admin may\n also need to complete this process before the name can be reused.\n\n### datasets_upload_files(dataset_id, local_path_to_file_or_directory, file_upload_setting, max_workers, target_chunk_size, target_relative_path)\n\nUploads a file or entire directory to a dataset.\n\n- *dataset_id:* The dataset identifier.\n- *local_path_to_file_or_directory:* The path to the file or directory in local machine.\n- *file_upload_setting:* The setting to resolve naming conflict, must be one of `Overwrite`, `Rename`, `Ignore` (default).\n- *max_workers:* The max amount of threads (default: 10).\n- *target_chunk_size:* The max chunk size for multipart upload (default: 8MB).\n- *target_relative_path:* The path on the dataset to upload the file or directory to. Note that the path must exist or the upload will fail.\n \n# Agents\n\n\n## domino.agents.environment_variables\n\nDOMINO_AGENT_CONFIG_PATH: \nFor configuring the location of the agent_config.yaml file. If not set,\ndefaults to \u2018./agent_config.yaml\u2019.\n\ntype: \nstr\n\nDOMINO_AGENT_IS_PROD: \nIndicates if the Agent is running in production mode. Set to \u2018true\u2019 to\noptimize for production.\n\ntype: \nstr\n\nDOMINO_APP_ID: \nIndicates the ID of the Agent application. Must be set in production\nmode.\n\ntype: \nstr\n\nMLFLOW_TRACKING_URI: \nUsed to configure Mlflow functionality. It is required in order for\nlibrary to work and will be set automatically when running in Domino.\n\ntype: \nstr\n\n## domino.agents.logging\n\nFunctions\n\n| | |\n|----|----|\n| `log_evaluation`(trace_id,\u00a0name,\u00a0value) | This logs evaluation data and metadata to a parent trace. |\n\nClasses\n\n| | |\n|----|----|\n| `DominoRun`(\\[experiment_name,\u00a0run_id,\u00a0...\\]) | DominoRun is a context manager that starts an Mlflow run and attaches the user's Agent configuration to it, create a Logged Model with the Agent configuration, and computes summary metrics for evaluation traces made during the run. |\n\n### *class* domino.agents.logging.DominoRun(*experiment_name: str \\| None = None*, *run_id: str \\| None = None*, *agent_config_path: str \\| None = None*, *custom_summary_metrics: list\\[str, Literal\\['mean', 'median', 'stdev', 'max', 'min'\\]\\] \\| None = None*) \nBases: `object`\n\nDominoRun is a context manager that starts an Mlflow run and attaches\nthe user\u2019s Agent configuration to it, create a Logged Model with the\nAgent configuration, and computes summary metrics for evaluation traces\nmade during the run. Average metrics are computed by default, but the\nuser can provide a custom list of evaluation metric aggregators. This is\nintended to be used in development mode for Agent evaluation. Context\nmanager docs: https://docs.python.org/3/library/contextlib.html\n\nParallelism: DominoRun is not thread-safe. Runs in different threads\nwill work correctly. This is due to Mlflow\u2019s architecture. Parallelizing\noperations within a single DominoRun context however, is supported.\n\nExample\n\nimport mlflow\n\nmlflow.set_experiment(\u201cmy_experiment\u201d)\n\nwith DominoRun(): \ntrain_model()\n\nParameters: \n- **experiment_name** \u2013 the name of the mlflow experiment to log the run\n to.\n\n- **run_id** \u2013 optional, the ID of the mlflow run to continue logging\n to. If not provided a new run will start.\n\n- **agent_config_path** \u2013 the optional path to the Agent configuration\n file. If not provided, defaults to the DOMINO_AGENT_CONFIG_PATH\n environment variable.\n\n- **custom_summary_metrics** \u2013 an optional list of tuples that define\n what summary statistic to use with what evaluation metric. Valid\n summary statistics are: \u201cmean\u201d, \u201cmedian\u201d, \u201cstdev\u201d, \u201cmax\u201d, \u201cmin\u201d e.g.\n \\[(\u201challucination_rate\u201d, \u201cmax\u201d)\\]\n\nReturns: DominoRun context manager\n\n \n\n### domino.agents.logging.log_evaluation(*trace_id: str*, *name: str*, *value: float \\| str*) \nThis logs evaluation data and metadata to a parent trace. This is used\nto log the evaluation of a span after it was created. This is useful for\nanalyzing past performance of an Agent component.\n\nParameters: \n- **trace_id** \u2013 the ID of the trace to evaluate\n\n- **name** \u2013 a label for the evaluation result. This is used to identify\n the evaluation result\n\n- **value** \u2013 the evaluation result to log. This must be a float or\n string\n\nModules\n\n| | |\n|-------------|-----|\n| `dominorun` | |\n| `logging` | |\n\n## domino.agents.tracing\n\nFunctions\n\n| | |\n|----|----|\n| `add_tracing`(name\\[,\u00a0autolog_frameworks,\u00a0...\\]) | This is a decorator that starts an mlflow span for the function it decorates. |\n| `init_tracing`(\\[autolog_frameworks\\]) | Initialize Mlflow autologging for various frameworks and sets the active experiment to enable tracing in production. |\n| `search_agent_traces`(agent_id\\[,\u00a0...\\]) | This allows searching for traces that have a certain name and returns a paginated response of trace summaries that include the spans that were requested. |\n| `search_traces`(run_id\\[,\u00a0trace_name,\u00a0...\\]) | This allows searching for traces that have a certain name and returns a paginated response of trace summaries that included the spans that were requested. |\n\nClasses\n\n| | |\n|----|----|\n| `EvaluationResult`(name,\u00a0value) | An evaluation result for a trace. |\n| `SearchTracesResponse`(data,\u00a0page_token) | The response from searching for traces. |\n| `SpanSummary`(id,\u00a0name,\u00a0trace_id,\u00a0inputs,\u00a0outputs) | A span in a trace. |\n| `TraceSummary`(name,\u00a0id,\u00a0spans,\u00a0evaluation_results) | A summary of a trace. |\n\n### *class* domino.agents.tracing.EvaluationResult(*name: str*, *value: float \\| str*) \nBases: `object`\n\nAn evaluation result for a trace.\n\nname*: str* \nThe name of the evaluation\n\nvalue*: float \\| str* \nThe value of the evaluation\n\n \n\n### *class* domino.agents.tracing.SearchTracesResponse(*data: list\\[TraceSummary\\]*, *page_token: str \\| None*) \nBases: `object`\n\nThe response from searching for traces.\n\ndata*: list\\[TraceSummary\\]* \nThe list of trace summaries\n\npage_token*: str \\| None* \nThe token for the next page of results\n\n \n\n### *class* domino.agents.tracing.SpanSummary(*id: str*, *name: str*, *trace_id: str*, *inputs: Any*, *outputs: Any*) \nBases: `object`\n\nA span in a trace.\n\nid*: str* \nthe mlflow ID of the span\n\ninputs*: Any* \nThe inputs to the function that created the span\n\nname*: str* \nThe name of the span\n\noutputs*: Any* \nThe outputs of the function that created the span\n\ntrace_id*: str* \nThe parent trace ID\n\n \n\n### *class* domino.agents.tracing.TraceSummary(*name: str*, *id: str*, *spans: list\\[SpanSummary\\]*, *evaluation_results: list\\[EvaluationResult\\]*) \nBases: `object`\n\nA summary of a trace.\n\nevaluation_results*: list\\[EvaluationResult\\]* \nThe evaluation results for this trace\n\nid*: str* \nThe mlflow ID of the trace\n\nname*: str* \nThe name of the trace\n\nspans*: list\\[SpanSummary\\]* \nThe child spans of this trace\n\n \n\n### domino.agents.tracing.add_tracing(*name: str*, *autolog_frameworks: list\\[str\\] \\| None = \\[\\]*, *evaluator: Callable\\[\\[mlflow.entities.Span\\], dict\\[str, int \\| float \\| str\\]\\] \\| None = None*, *trace_evaluator: Callable\\[\\[mlflow.entities.Trace\\], dict\\[str, int \\| float \\| str\\]\\] \\| None = None*, *eagerly_evaluate_streamed_results: bool = True*, *allow_tracing_evaluator: bool = False*) \nThis is a decorator that starts an mlflow span for the function it\ndecorates. If there is an existing trace a span will be appended to it.\nIf there is no existing trace, a new trace will be created.\n\nIt also enables the user to run evaluators when the code is run in\ndevelopment mode. Evaluators can be run on the span and/or trace\ngenerated for the wrapped function call. The trace evaluator will run if\nthe parent trace was started and finished by the related decorator call.\nThe trace will contain all child span information. The span evaluator\nwill always run. The evaluation results from both evaluators will be\ncombined and saved to the trace.\n\nThis decorator must be used directly on the function to be traced\nwithout any intervening decorators, because it must have access to the\narguments.\n\n@add_tracing( \nname=\u201dassistant_chat_bot\u201d, evaluator=evaluate_helpfulness,\n\n) def ask_chat_bot(user_input: str) -\\> dict:\n\n> \u2026\n\nParameters: \n- **name** \u2013 the name of the span to add to existing trace or create if\n no trace exists yet.\n\n- **autolog_frameworks** \u2013 an optional list of mlflow supported\n frameworks to autolog\n\n- **evaluator** \u2013 an optional function that takes the span created for\n the wrapped function and returns a dictionary of evaluation results.\n The evaluation results will be saved to the trace\n\n- **trace_evaluator** \u2013 an optional function that takes the trace for\n this call stack and returns a dictionary of evaluation results. This\n evaluator will be triggered if the trace was started and finished by\n the add tracing decorator. The evaluation results will be saved to the\n trace\n\n- **eagerly_evaluate_streamed_results** \u2013 optional boolean, defaults to\n true, this determines if all yielded values should be aggregated and\n set as outputs to a single span. This makes evaluation easier, but\n will impact performance if you expect a large number of streamed\n values. If set to false, each yielded value will generate a new span\n on the trace, which can be evaluated post-hoc. Inline evaluators won\u2019t\n be executed. Each span will have a group_id set in their attributes to\n indicate that they are part of the same function call. Each span will\n have an index to indicate what order they arrived in.\n\n- **allow_tracing_evaluator** \u2013 optional boolean, defaults to false.\n This determines if inline evaluators will be traced by mlflow autolog.\n\nReturns: \nA decorator that wraps the function to be traced.\n\n \n\n### domino.agents.tracing.init_tracing(*autolog_frameworks: list\\[str\\] \\| None = None*) \nInitialize Mlflow autologging for various frameworks and sets the active\nexperiment to enable tracing in production. This may be used to\ninitialize logging and tracing for the Agent in dev and prod modes.\n\nIn prod mode, environment variables DOMINO_AGENT_IS_PROD, DOMINO_APP_ID\nmust be set. Call init_tracing before your app starts up to start\nlogging traces to Domino.\n\nParameters: \n**autolog_frameworks** \u2013 list of frameworks to autolog\n\n \n\n### domino.agents.tracing.search_agent_traces(*agent_id: str*, *agent_version: str \\| None = None*, *trace_name: str \\| None = None*, *start_time: datetime \\| None = None*, *end_time: datetime \\| None = None*, *page_token: str \\| None = None*, *max_results: int \\| None = None*) \u2192 SearchTracesResponse \nThis allows searching for traces that have a certain name and returns a\npaginated response of trace summaries that include the spans that were\nrequested.\n\nParameters: \n- **agent_id** \u2013 string, the ID of the Agent to filter by\n\n- **agent_version** \u2013 string, the version of the Agent to filter by, if\n not provided will search through all versions\n\n- **trace_name** \u2013 the name of the traces to search for\n\n- **start_time** \u2013 python datetime\n\n- **end_time** \u2013 python datetime, defaults to now\n\n- **page_token** \u2013 page token for pagination. You can use this to\n request the next page of results and may find a page_token in the\n response of the previous search_traces call.\n\n- **max_results** \u2013 defaults to 100\n\nReturns: \na token based pagination response that contains a list of trace summaries \ndata: list of TraceSummary page_token: the next page\u2019s token\n\nReturn type: \nSearchTracesResponse\n\n \n\n### domino.agents.tracing.search_traces(*run_id: str*, *trace_name: str \\| None = None*, *start_time: datetime \\| None = None*, *end_time: datetime \\| None = None*, *page_token: str \\| None = None*, *max_results: int \\| None = None*) \u2192 SearchTracesResponse \nThis allows searching for traces that have a certain name and returns a\npaginated response of trace summaries that included the spans that were\nrequested.\n\nParameters: \n- **run_id** \u2013 string, the ID of the development mode evaluation run to\n search for traces.\n\n- **trace_name** \u2013 the name of the traces to search for\n\n- **start_time** \u2013 python datetime\n\n- **end_time** \u2013 python datetime, defaults to now\n\n- **page_token** \u2013 page token for pagination. You can use this to\n request the next page of results and may find a page_token in the\n response of the previous search_traces call.\n\n- **max_results** \u2013 defaults to 100\n\nReturns: \na token based pagination response that contains a list of trace summaries \ndata: list of TraceSummary page_token: the next page\u2019s token\n\nReturn type: \nSearchTracesReponse\n\nModules\n\n| | |\n|---------------|-----|\n| `inittracing` | |\n| `tracing` | |\n\n# Airflow\n\nThe `python-domino` client comes bundled with an\n[Operator](https://airflow.apache.org/docs/apache-airflow/stable/concepts/operators.html)\nfor use with [Apache Airflow](https://airflow.apache.org/) as an extra.\n\nWhen installing the client from PyPI, add the `airflow` flag to extras:\n\n pip install \"dominodatalab[airflow]\"\n\nSimilarly, when installing the client from GitHub, use the following\ncommand:\n\n pip install -e git+https://github.com/dominodatalab/python-domino.git@1.0.6#egg=\"dominodatalab[airflow]\"\n\nSee also\n[example_airflow_dag.py](https://github.com/dominodatalab/python-domino/blob/Release-2.0.0/examples/example_airflow_dag.py)\nfor example code.\n\n## DominoOperator\n\n from domino.airflow import DominoOperator\n\nAllows a user to schedule Domino executions via Airflow. Follows the\nsame function signature as `domino.runs_start` with two extra arguments:\n\n- `startup_delay: Optional[int] = 10` | Add a startup delay to your job, useful if you want to delay execution until after other work finishes.\n- `include_setup_log: Optional[bool] = True` | Determine whether or not to publish the setup log of the job as the log prefix before `stdout`.\n\n## DominoSparkOperator\n\n from domino.airflow import DominoSparkOperator\n\nAllows a user to schedule Domino executions via the v4 API, which\nsupports `onDemandSparkClusters`. Follows the same function signature as\n`domino.job_start`, with the addition of `startup_delay` from above.\n\n# Example\n\n from domino import Domino\n\n # By and large your commands will run against a single project,\n # so you must specify the full project name\n domino = Domino(\"chris/canon\")\n\n # List all runs in the project, most-recently queued first\n all_runs = domino.runs_list()['data']\n\n latest_100_runs = all_runs[0:100]\n\n print(latest_100_runs)\n\n # all runs have a commitId (the snapshot of the project when the\n # run starts) and, if the run completed, an \"outputCommitId\"\n # (the snapshot of the project after the run completed)\n most_recent_run = all_runs[0]\n\n commitId = most_recent_run['outputCommitId']\n\n # list all the files in the output commit ID -- only showing the\n # entries under the results directory. If not provided, will\n # list all files in the project. Or you can say path=\u201c/\u201c to\n # list all files\n files = domino.files_list(commitId, path='results/')['data']\n\n for file in files:\n print file['path'], '->', file['url']\n\n print(files)\n\n # Get the content (i.e. blob) for the file you're interested in.\n # blobs_get returns a connection rather than the content, because\n # the content can get quite large and it's up to you how you want\n # to handle it\n print(domino.blobs_get(files[0]['key']).read())\n\n # Start a run of file main.py using the latest copy of that file\n domino.runs_start([\"main.py\", \"arg1\", \"arg2\"])\n\n # Start a \"direct\" command\n domino.runs_start([\"echo 'Hello, World!'\"], isDirect=True)\n\n # Start a run of a specific commit\n domino.runs_start([\"main.py\"], commitId=\"aabbccddee\")\n\n# Manual installation\n\nBecause `python-domino` ships with the DSE, normally you do not need to install it. \nThis section provides instructions for installing it in another environment or updating it to a newer version.\n\nStarting from version `1.0.6`, `python-domino` is available on PyPI as `dominodatalab`:\n\n pip install dominodatalab\n\nIf you are adding install instructions for `python-domino` to your [Domino Environment](https://support.dominodatalab.com/hc/en-us/articles/115000392643-Compute-Environment-Management) Dockerfile Instructions field, you must add `RUN` to the beginning:\n\n RUN pip install dominodatalab\n\nTo install a specific version of the library from PyPI, such as `1.0.6`:\n\n pip install dominodatalab==1.0.6\n\nTo install a specific version of the library from GitHub, such as\n`1.0.6`:\n\n pip install https://github.com/dominodatalab/python-domino/archive/1.0.6.zip\n\n# License\n\nThis library is made available under the Apache 2.0 License. This is an\nopen-source project of [Domino Data Lab](https://www.dominodatalab.com).\n", + "description_content_type": "text/markdown", + "docs_url": null, + "download_url": "https://github.com/dominodatalab/python-domino/archive/Release-2.0.0.zip", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "dynamic": null, + "home_page": "https://github.com/dominodatalab/python-domino", + "keywords": "Domino Data Lab, API", + "license": "Apache Software License (Apache 2.0)", + "license_expression": null, + "license_files": [ + "LICENSE.txt" + ], + "maintainer": null, + "maintainer_email": null, + "name": "dominodatalab", + "package_url": "https://pypi.org/project/dominodatalab/", + "platform": null, + "project_url": "https://pypi.org/project/dominodatalab/", + "project_urls": { + "Download": "https://github.com/dominodatalab/python-domino/archive/Release-2.0.0.zip", + "Homepage": "https://github.com/dominodatalab/python-domino" + }, + "provides_extra": [ + "airflow", + "data", + "agents", + "dev", + "docs" + ], + "release_url": "https://pypi.org/project/dominodatalab/2.0.0/", + "requires_dist": [ + "packaging==23.2", + "requests>=2.4.2", + "beautifulsoup4~=4.11", + "polling2~=0.5.0", + "urllib3<3,>=1.26.19", + "typing-extensions~=4.13.0", + "frozendict~=2.3", + "python-dateutil~=2.8.2", + "retry==0.9.2", + "apache-airflow==2.2.4; extra == \"airflow\"", + "dominodatalab-data>=0.1.0; extra == \"data\"", + "semver>=3.0.4; extra == \"agents\"", + "pandas>=2.3.1; extra == \"agents\"", + "numpy>=2.0.2; extra == \"agents\"", + "mlflow-skinny<3.3.0,>=3.2.0; extra == \"agents\"", + "mlflow-tracing<3.3.0,>=3.2.0; extra == \"agents\"", + "pytest-order>=1.3.0; extra == \"dev\"", + "pytest-asyncio>=0.23.8; extra == \"dev\"", + "scikit-learn>=1.6.1; extra == \"dev\"", + "openai>=2.7.2; extra == \"dev\"", + "ai-mock>=0.3.1; extra == \"dev\"", + "black==22.3.0; extra == \"dev\"", + "flake8==4.0.1; extra == \"dev\"", + "Jinja2==2.11.3; extra == \"dev\"", + "nbconvert==6.3.0; extra == \"dev\"", + "packaging==23.2; extra == \"dev\"", + "polling2==0.5.0; extra == \"dev\"", + "pre-commit==2.19.0; extra == \"dev\"", + "pyspark==3.3.0; extra == \"dev\"", + "pytest==7.4.3; extra == \"dev\"", + "requests_mock==1.9.3; extra == \"dev\"", + "tox==3.25.1; extra == \"dev\"", + "frozendict==2.4.6; extra == \"dev\"", + "docker>=7.1.0; extra == \"dev\"", + "pytest-mock>=3.14.1; extra == \"dev\"", + "sphinx>=7.4.0; extra == \"docs\"", + "markupsafe==2.0.1; extra == \"docs\"" + ], + "requires_python": ">=3.10.0", + "summary": "Python bindings for the Domino API", + "version": "2.0.0", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 32639435, + "urls": [ + { + "comment_text": null, + "digests": { + "blake2b_256": "5eeb37ed5f1a4c835eaf2aef407438bb6d751b42c28ba1486bdfbaa6346bbbae", + "md5": "58620aaaa5f8a149ee83ba0f29ca97d4", + "sha256": "44610719c61c963e8fb3815bc416e9e9ab8e4cdb4577a83488b068aa2f5ed831" + }, + "downloads": -1, + "filename": "dominodatalab-2.0.0-py3-none-any.whl", + "has_sig": false, + "md5_digest": "58620aaaa5f8a149ee83ba0f29ca97d4", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.10.0", + "size": 182955, + "upload_time": "2025-11-26T18:28:13", + "upload_time_iso_8601": "2025-11-26T18:28:13.396920Z", + "url": "https://files.pythonhosted.org/packages/5e/eb/37ed5f1a4c835eaf2aef407438bb6d751b42c28ba1486bdfbaa6346bbbae/dominodatalab-2.0.0-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": null, + "digests": { + "blake2b_256": "d86d1e321187451c1cc1670e615497474f9c54f04ad5f4ff7e831ea2dc3eeb23", + "md5": "3fc2d40e54dac9c36f077d38862f9f38", + "sha256": "05d0f44a89bf0562413018f638839e31bdc108d6ed67869d5ccaceacf41ee237" + }, + "downloads": -1, + "filename": "dominodatalab-2.0.0.tar.gz", + "has_sig": false, + "md5_digest": "3fc2d40e54dac9c36f077d38862f9f38", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.10.0", + "size": 153801, + "upload_time": "2025-11-26T18:28:14", + "upload_time_iso_8601": "2025-11-26T18:28:14.745369Z", + "url": "https://files.pythonhosted.org/packages/d8/6d/1e321187451c1cc1670e615497474f9c54f04ad5f4ff7e831ea2dc3eeb23/dominodatalab-2.0.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ], + "vulnerabilities": [] +} diff --git a/tests_integration/lib/_shared.py b/tests_integration/lib/_shared.py index 206b1c207..02b60fde7 100644 --- a/tests_integration/lib/_shared.py +++ b/tests_integration/lib/_shared.py @@ -76,6 +76,7 @@ def get_transparent_urls() -> set[str]: "https://pypi.io/packages/source/*", "https://pypi.org/packages/source/*", "https://files.pythonhosted.org/packages/*", + "https://spdx.org/*", # Needed for grayskull license discovery "https://api.anaconda.org/package/conda-forge/conda-forge-pinning", "https://api.anaconda.org/download/conda-forge/conda-forge-pinning/*", "https://binstar-cio-packages-prod.s3.amazonaws.com/*", From 5d9b38678458732d100b757f0c28269d92891966 Mon Sep 17 00:00:00 2001 From: Jan Jagusch Date: Mon, 29 Dec 2025 17:22:27 +0100 Subject: [PATCH 6/6] docs: add CLAUDE.md for Claude Code guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add documentation to help Claude Code understand the codebase structure, common commands, and integration test setup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..fda6aa869 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,226 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is **autotick-bot** (conda-forge-tick), the automated maintenance bot for the conda-forge ecosystem. It creates PRs to update packages, run migrations, and maintain the conda-forge dependency graph across thousands of feedstocks. + +## Common Commands + +### Development Setup +```bash +# Using environment.yml (recommended for development) +conda env create -f environment.yml + +# Or using the production lockfile +wget https://raw.githubusercontent.com/regro/cf-scripts/main/conda-lock.yml +conda-lock install conda-lock.yml + +# Install in editable mode +pip install -e . +``` + +### Running Tests +```bash +# Run all tests (requires docker for container tests) +pytest -v + +# Run tests in parallel +pytest -v -n 3 + +# Run a single test +pytest -v tests/test_file.py::test_function + +# Skip MongoDB tests +pytest -v -m "not mongodb" + +# To enable container-based tests, first build the test image: +docker build -t conda-forge-tick:test . +``` + +### CLI Usage +```bash +# General help +conda-forge-tick --help + +# Debug mode (enables debug logging, disables multiprocessing) +conda-forge-tick --debug + +# Online mode (fetches graph data from GitHub, useful for local testing) +conda-forge-tick --online + +# Disable containers (for debugging, but note security implications) +conda-forge-tick --no-containers + +# Example: update upstream versions for a single package +conda-forge-tick --debug --online update-upstream-versions numpy +``` + +### Linting +```bash +# Pre-commit handles linting (ruff, mypy, typos) +pre-commit run --all-files +``` + +## Architecture + +### Core Components + +**CLI Entry Points** (`conda_forge_tick/cli.py`, `conda_forge_tick/container_cli.py`): +- `conda-forge-tick`: Main CLI for bot operations +- `conda-forge-tick-container`: CLI for containerized operations + +**Key Modules**: +- `auto_tick.py`: Main bot job - creates PRs for migrations and version updates +- `make_graph.py`: Builds the conda-forge dependency graph +- `make_migrators.py`: Initializes migration objects +- `update_upstream_versions.py`: Fetches latest versions from upstream sources +- `update_prs.py`: Updates PR statuses from GitHub +- `feedstock_parser.py`: Parses feedstock metadata + +### Migrators (`conda_forge_tick/migrators/`) + +Base class: `Migration` in `core.py`. Migrators handle automated changes: +- `version.py`: Version updates (special - uses `CondaMetaYAML` parser) +- `migration_yaml.py`: CFEP-09 YAML migrations from conda-forge-pinning +- `arch.py`, `cross_compile.py`: Architecture migrations +- Custom migrators for specific ecosystem changes (libboost, numpy2, etc.) + +### Data Model + +The bot uses `cf-graph-countyfair` repository as its database. Key structures: +- `graph.json`: NetworkX dependency graph +- `node_attrs/`: Package metadata (one JSON per package, sharded paths) +- `versions/`: Upstream version information +- `pr_json/`: PR status tracking +- `pr_info/`, `version_pr_info/`: Migration/version PR metadata + +Pydantic models in `conda_forge_tick/models/` document the schema. + +### LazyJson System + +Data is loaded lazily via `LazyJson` class. Backends configured via `CF_TICK_GRAPH_DATA_BACKENDS`: +- `file`: Local filesystem (default, requires cf-graph-countyfair clone) +- `github`: Read-only from GitHub raw URLs (good for debugging) +- `mongodb`: MongoDB database + +### Recipe Parsing + +`CondaMetaYAML` in `recipe_parser/` handles Jinja2-templated YAML recipes: +- Preserves comments (important for conda selectors) +- Handles duplicate keys with different selectors via `__###conda-selector###__` tokens +- Extracts Jinja2 variables for version migration + +## Environment Variables + +See `conda_forge_tick/settings.py` for full list. Key ones: +- `CF_TICK_GRAPH_DATA_BACKENDS`: Colon-separated backend list +- `CF_TICK_GRAPH_DATA_USE_FILE_CACHE`: Enable/disable local caching +- `MONGODB_CONNECTION_STRING`: MongoDB connection string +- `BOT_TOKEN`: GitHub token for bot operations +- `CF_FEEDSTOCK_OPS_IN_CONTAINER`: Set to "true" when running in container + +## Bot Jobs Structure + +The bot runs as multiple parallel cron jobs via GitHub Actions: +- `bot-bot.yml`: Main job making PRs +- `bot-feedstocks.yml`: Updates feedstock list +- `bot-versions.yml`: Fetches upstream versions +- `bot-prs.yml`: Updates PR statuses +- `bot-make-graph.yml`: Builds dependency graph +- `bot-make-migrators.yml`: Creates migration objects +- `bot-pypi-mapping.yml`: PyPI to conda-forge mapping + +## Integration Tests + +Located in `tests_integration/`. Tests the full bot pipeline against real GitHub repositories using staging accounts. + +### Test Environment Architecture + +The integration tests require three GitHub entities that mimic production: +- **Conda-forge org** (`GITHUB_ACCOUNT_CONDA_FORGE_ORG`): Contains test feedstocks +- **Bot user** (`GITHUB_ACCOUNT_BOT_USER`): Creates forks and PRs +- **Regro org** (`GITHUB_ACCOUNT_REGRO_ORG`): Contains a test `cf-graph-countyfair` repository + +Default staging accounts are `conda-forge-bot-staging`, `regro-cf-autotick-bot-staging`, and `regro-staging`. You can use your own accounts by setting environment variables. + +### Setup + +1. **Initialize git submodules** (test feedstock resources are stored as submodules): +```bash +git submodule update --init --recursive +``` + +2. **Create a `.env` file** with required environment variables: +```bash +export BOT_TOKEN='' +export TEST_SETUP_TOKEN='' # typically same as BOT_TOKEN +export GITHUB_ACCOUNT_CONDA_FORGE_ORG='your-conda-forge-staging-org' +export GITHUB_ACCOUNT_BOT_USER='your-bot-user' +export GITHUB_ACCOUNT_REGRO_ORG='your-regro-staging-org' +export PROXY_DEBUG_LOGGING='true' # optional, for debugging +``` + +GitHub token requires scopes: `repo`, `workflow`, `delete_repo`. + +3. **Set up mitmproxy certificates** (required for HTTP proxy that intercepts requests): +```bash +cd tests_integration +./mitmproxy_setup_wizard.sh +``` + +On macOS: Add the generated certificate to Keychain Access and set "Always Trust". +On Linux: Copy to `/usr/local/share/ca-certificates/` and run `update-ca-certificates`. + +4. **Build the Docker test image** (required for container-based tests): +```bash +docker build -t conda-forge-tick:test . +``` + +### Running Integration Tests + +**Important**: Integration tests take a long time to execute (5+ minutes per test). To avoid repeated runs: +- Persist stdout/stderr to a file and grep for errors +- Run tests in the background while working on other tasks + +```bash +# Source your environment variables +source .env + +# Run from repository root, skipping container tests (default) +# Recommended: redirect output to file for later analysis +pytest -s -v --dist=no tests_integration -k "False" > /tmp/integration_test.log 2>&1 & +tail -f /tmp/integration_test.log # follow output in another terminal + +# Or run interactively if needed +pytest -s -v --dist=no tests_integration -k "False" + +# Run only container tests (requires Docker image built with test tag) +pytest -s -v --dist=no tests_integration -k "True" + +# Run a specific test scenario +pytest -s -v --dist=no tests_integration -k "test_scenario[0]" +``` + +### Test Case Structure + +Test cases are defined in `tests_integration/lib/_definitions//__init__.py`. Each test case: +1. `get_router()`: Defines mock HTTP responses via FastAPI router +2. `prepare(helper)`: Sets up test state (e.g., overwrites feedstock contents) +3. `validate(helper)`: Asserts expected outcomes (e.g., PR was created with correct changes) + +Current test feedstocks: `pydantic`, `polars`, `fastapi`, `zizmor`, `conda-forge-pinning`. + +### How Tests Execute + +Tests run the full bot pipeline in sequence: +1. `gather-all-feedstocks` +2. `make-graph --update-nodes-and-edges` +3. `make-graph` +4. `update-upstream-versions` +5. `make-migrators` +6. `auto-tick` +7. (repeat migrators and auto-tick for state propagation) + +Each step deploys to the staging `cf-graph-countyfair` repo.