tundri 1.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tundri might be problematic. Click here for more details.

Files changed (36) hide show
  1. tundri-1.3.3/.github/pull_request_template.md +16 -0
  2. tundri-1.3.3/.github/workflows/manual-release.yml +81 -0
  3. tundri-1.3.3/.github/workflows/on-release-merge.yml +84 -0
  4. tundri-1.3.3/.github/workflows/pr-tests.yml +53 -0
  5. tundri-1.3.3/.github/workflows/publish-on-release.yml +35 -0
  6. tundri-1.3.3/.github/workflows/publish-to-testpypi.yml +54 -0
  7. tundri-1.3.3/.gitignore +18 -0
  8. tundri-1.3.3/.python-version +1 -0
  9. tundri-1.3.3/PKG-INFO +147 -0
  10. tundri-1.3.3/README.md +120 -0
  11. tundri-1.3.3/docs/RELEASE_WORKFLOW.md +196 -0
  12. tundri-1.3.3/docs/images/logo.jpg +0 -0
  13. tundri-1.3.3/docs/images/run_example.png +0 -0
  14. tundri-1.3.3/examples/permifrost.yml +364 -0
  15. tundri-1.3.3/examples/permifrost_with_bob.yml +393 -0
  16. tundri-1.3.3/examples/permifrost_without_bob.yml +364 -0
  17. tundri-1.3.3/pyproject.toml +62 -0
  18. tundri-1.3.3/pytest.ini +3 -0
  19. tundri-1.3.3/tests/conftest.py +42 -0
  20. tundri-1.3.3/tests/data/correct_required_params_spec.yml +21 -0
  21. tundri-1.3.3/tests/data/incorrect_required_params_spec.yml +21 -0
  22. tundri-1.3.3/tests/data/uppercase_meta_params_spec.yml +22 -0
  23. tundri-1.3.3/tests/integration_tests/test_connection.py +34 -0
  24. tundri-1.3.3/tests/integration_tests/test_ddl.py +46 -0
  25. tundri-1.3.3/tests/test_core.py +46 -0
  26. tundri-1.3.3/tests/test_parser.py +84 -0
  27. tundri-1.3.3/tests/test_utils.py +47 -0
  28. tundri-1.3.3/tundri/__init__.py +3 -0
  29. tundri-1.3.3/tundri/cli.py +121 -0
  30. tundri-1.3.3/tundri/constants.py +47 -0
  31. tundri-1.3.3/tundri/core.py +307 -0
  32. tundri-1.3.3/tundri/inspector.py +174 -0
  33. tundri-1.3.3/tundri/objects.py +74 -0
  34. tundri-1.3.3/tundri/parser.py +112 -0
  35. tundri-1.3.3/tundri/utils.py +224 -0
  36. tundri-1.3.3/uv.lock +1785 -0
@@ -0,0 +1,16 @@
1
+ ## Description
2
+
3
+ <!--- In this section, concisely describe what your Pull Request does, and why it is important (both in a technical and business value sense) -->
4
+
5
+ ## Changes
6
+
7
+ <!--- Please document here, if relevant, what has changed, and in which specific models -->
8
+
9
+ ## Checklist
10
+
11
+ <!--- Please make sure all of the below are checked off before submitting your PR ;) -->
12
+
13
+ - [ ] My pull request represents one logical piece of work
14
+ - [ ] My commits are related to the pull request and look clean
15
+ - [ ] I tested my changes locally with `uv run pytest -vv`
16
+ - [ ] I formatted my code with `uv run black .`
@@ -0,0 +1,81 @@
1
+ name: Manual Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version_type:
7
+ description: "Type of version bump (major, minor, patch)"
8
+ required: true
9
+ default: "patch"
10
+ type: choice
11
+ options:
12
+ - major
13
+ - minor
14
+ - patch
15
+
16
+ jobs:
17
+ release:
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - name: Checkout code
22
+ uses: actions/checkout@v3
23
+
24
+ - name: Set up Python
25
+ uses: actions/setup-python@v4
26
+ with:
27
+ python-version: '3.12'
28
+
29
+ - name: Bump version
30
+ id: bump_version
31
+ run: |
32
+ CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed -E "s/version = \"([0-9]+\.[0-9]+\.[0-9]+)\"/\1/")
33
+ echo "Current version: $CURRENT_VERSION"
34
+
35
+ # Determine the version type to bump (major, minor, patch)
36
+ VERSION_TYPE=${{ github.event.inputs.version_type }}
37
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
38
+
39
+ if [ "$VERSION_TYPE" == "major" ]; then
40
+ MAJOR=$((MAJOR + 1))
41
+ MINOR=0
42
+ PATCH=0
43
+ elif [ "$VERSION_TYPE" == "minor" ]; then
44
+ MINOR=$((MINOR + 1))
45
+ PATCH=0
46
+ elif [ "$VERSION_TYPE" == "patch" ]; then
47
+ PATCH=$((PATCH + 1))
48
+ else
49
+ echo "Invalid version type: $VERSION_TYPE"
50
+ exit 1
51
+ fi
52
+
53
+ NEW_VERSION="$MAJOR.$MINOR.$PATCH"
54
+ echo "New version: $NEW_VERSION"
55
+
56
+ # Update pyproject.toml with the new version
57
+ sed -i "s/version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/" pyproject.toml
58
+
59
+ echo "new_version=$NEW_VERSION" >> $GITHUB_ENV
60
+
61
+ - name: Create release branch
62
+ id: create_branch
63
+ run: |
64
+ NEW_VERSION=${{ env.new_version }}
65
+ BRANCH_NAME="release/v$NEW_VERSION"
66
+ git checkout -b "$BRANCH_NAME"
67
+ git push origin "$BRANCH_NAME"
68
+ echo "branch_name=$BRANCH_NAME" >> $GITHUB_ENV
69
+
70
+ - name: Create pull request
71
+ uses: peter-evans/create-pull-request@v7
72
+ with:
73
+ token: ${{ secrets.GITHUB_TOKEN }}
74
+ branch: ${{ env.branch_name }}
75
+ base: main
76
+ title: "Release ${{ env.new_version }}"
77
+ body: |
78
+ This PR bumps the version to ${{ env.new_version }} and prepares the release.
79
+
80
+ # Tag and release creation will be handled by on-release-merge.yml workflow
81
+ # when this PR is merged
@@ -0,0 +1,84 @@
1
+ name: On Release Merge
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+ branches:
7
+ - main
8
+ paths:
9
+ - 'pyproject.toml'
10
+
11
+ jobs:
12
+ create-tag-and-release:
13
+ if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/')
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v3
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - name: Set up Python
23
+ uses: actions/setup-python@v4
24
+ with:
25
+ python-version: '3.12'
26
+
27
+ - name: Get version
28
+ id: get_version
29
+ run: |
30
+ VERSION=$(grep '^version = ' pyproject.toml | sed -E "s/version = \"([0-9]+\.[0-9]+\.[0-9]+)\"/\1/")
31
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
32
+
33
+ - name: Create tag
34
+ run: |
35
+ VERSION=${{ steps.get_version.outputs.version }}
36
+ TAG_NAME="v$VERSION"
37
+ git config user.name "github-actions"
38
+ git config user.email "github-actions@github.com"
39
+ git tag "$TAG_NAME"
40
+ git push origin "$TAG_NAME"
41
+
42
+ - name: Create release
43
+ uses: actions/create-release@v1
44
+ env:
45
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46
+ with:
47
+ tag_name: v${{ steps.get_version.outputs.version }}
48
+ release_name: v${{ steps.get_version.outputs.version }}
49
+ body: |
50
+ This release includes the following changes:
51
+ - Version bump to ${{ steps.get_version.outputs.version }}
52
+ draft: false
53
+ prerelease: false
54
+
55
+ publish-to-pypi:
56
+ if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/')
57
+ runs-on: ubuntu-latest
58
+ needs: create-tag-and-release
59
+
60
+ steps:
61
+ - name: Checkout code
62
+ uses: actions/checkout@v3
63
+
64
+ - name: Set up Python
65
+ uses: actions/setup-python@v4
66
+ with:
67
+ python-version: '3.12'
68
+
69
+ - name: Install build dependencies
70
+ run: |
71
+ python -m pip install --upgrade pip
72
+ pip install build twine
73
+
74
+ - name: Build package
75
+ run: python -m build
76
+
77
+ - name: Check package
78
+ run: twine check dist/*
79
+
80
+ - name: Publish to PyPI
81
+ env:
82
+ TWINE_USERNAME: __token__
83
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
84
+ run: twine upload dist/*
@@ -0,0 +1,53 @@
1
+ name: PR Unit Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+
12
+ env:
13
+ PERMISSION_BOT_USER: ${{ secrets.SNOWFLAKE_USER }}
14
+ PERMISSION_BOT_KEY_PATH: "./.ssh/private_key.p8"
15
+ PERMISSION_BOT_KEY_PASSPHRASE: ${{ secrets.SNOWFLAKE_KEY_PASSPHRASE }}
16
+ PERMISSION_BOT_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
17
+ PERMISSION_BOT_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }}
18
+ PERMISSION_BOT_ROLE: ${{ secrets.SNOWFLAKE_ROLE }}
19
+ PERMISSION_BOT_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }}
20
+
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v3
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v4
27
+ with:
28
+ python-version: '3.12'
29
+
30
+ - name: Install uv
31
+ uses: astral-sh/setup-uv@v1
32
+ with:
33
+ version: "latest"
34
+
35
+ # tundri currently only supports connecting to Snowflake by providing
36
+ # the path to the user's private key, so we need to dump the key somewhere in
37
+ # the Ubuntu environment. While connecting via password is also possible, we are
38
+ # opting for key-pair to be future-proof
39
+ #
40
+ # TODO: extend get_snowflake_cursor() function to allow
41
+ # passing raw keys, so we can avoid exposing the private key
42
+ - name: Create folder for private key
43
+ run: mkdir -p ./.ssh
44
+
45
+ - name: Write private key to file
46
+ run: |
47
+ echo "${{ secrets.SNOWFLAKE_PRIVATE_KEY }}" > $PERMISSION_BOT_KEY_PATH
48
+
49
+ - name: Install dependencies
50
+ run: uv sync
51
+
52
+ - name: Run tests
53
+ run: uv run pytest -vv
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI on Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish-to-pypi:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - name: Checkout code
13
+ uses: actions/checkout@v3
14
+
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v4
17
+ with:
18
+ python-version: '3.12'
19
+
20
+ - name: Install build dependencies
21
+ run: |
22
+ python -m pip install --upgrade pip
23
+ pip install build twine
24
+
25
+ - name: Build package
26
+ run: python -m build
27
+
28
+ - name: Check package
29
+ run: twine check dist/*
30
+
31
+ - name: Publish to PyPI
32
+ env:
33
+ TWINE_USERNAME: __token__
34
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
35
+ run: twine upload dist/*
@@ -0,0 +1,54 @@
1
+ name: Publish to TestPyPI
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'pyproject.toml'
9
+ - 'tundri/**'
10
+
11
+ jobs:
12
+ publish-to-testpypi:
13
+ runs-on: ubuntu-latest
14
+ if: github.event.pull_request.merged == false
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v3
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v4
22
+ with:
23
+ python-version: '3.12'
24
+
25
+ - name: Install build dependencies
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ pip install build twine
29
+
30
+ - name: Build package
31
+ run: python -m build
32
+
33
+ - name: Check package
34
+ run: twine check dist/*
35
+
36
+ - name: Publish to TestPyPI
37
+ env:
38
+ TWINE_USERNAME: __token__
39
+ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
40
+ run: twine upload --repository testpypi dist/*
41
+
42
+ - name: Comment PR with TestPyPI link
43
+ uses: actions/github-script@v6
44
+ with:
45
+ script: |
46
+ const version = require('fs').readFileSync('pyproject.toml', 'utf8')
47
+ .match(/version = "([^"]+)"/)[1];
48
+ const testpypiUrl = `https://test.pypi.org/project/tundri/${version}/`;
49
+ github.rest.issues.createComment({
50
+ issue_number: context.issue.number,
51
+ owner: context.repo.owner,
52
+ repo: context.repo.repo,
53
+ body: `🚀 Package published to TestPyPI!\n\nYou can test the installation with:\n\`\`\`bash\npip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ tundri==${version}\n\`\`\`\n\nView package: ${testpypiUrl}`
54
+ });
@@ -0,0 +1,18 @@
1
+ # Developer-specific notes
2
+ TODO.md
3
+ launch.json
4
+
5
+ # Environment variables
6
+ .env
7
+
8
+ # Python-generated files
9
+ __pycache__/
10
+ *.py[oc]
11
+ build/
12
+ dist/
13
+ wheels/
14
+ *.egg-info
15
+
16
+ # Virtual environments
17
+ .venv
18
+
@@ -0,0 +1 @@
1
+ 3.11.9
tundri-1.3.3/PKG-INFO ADDED
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: tundri
3
+ Version: 1.3.3
4
+ Summary: Drop, create and alter Snowflake objects and set permissions with Permifrost
5
+ Project-URL: Homepage, https://github.com/Gemma-Analytics/tundri
6
+ Project-URL: Repository, https://github.com/Gemma-Analytics/tundri
7
+ Project-URL: Issues, https://github.com/Gemma-Analytics/tundri/issues
8
+ Author-email: Gemma Analytics <bijan.soltani@gemmaanalytics.com>
9
+ License: MIT
10
+ Keywords: database,ddl,permifrost,snowflake
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ Requires-Dist: gemma-permifrost
22
+ Requires-Dist: python-dotenv
23
+ Requires-Dist: pyyaml
24
+ Requires-Dist: rich
25
+ Requires-Dist: snowflake-connector-python
26
+ Description-Content-Type: text/markdown
27
+
28
+ <div align="center">
29
+ <img src="docs/images/logo.jpg" alt="tundri Logo" width="200">
30
+ </div>
31
+
32
+ **tundri** is a Python package to declaratively create, drop, and alter Snowflake objects and manage their permissions with [Permifrost](https://gitlab.com/gitlab-data/permifrost).
33
+
34
+ ## Motivation
35
+
36
+ Permifrost is great at managing permissions, but it doesn't create or alter objects. As [GitLab's data team handbook](https://handbook.gitlab.com/handbook/enterprise-data/platform/permifrost/) states:
37
+ > Object creation and deletion is not managed by permifrost
38
+
39
+ With only Permifrost, one would have to manually create the objects and then run Permifrost to set the permissions. This is error prone and time consuming. That is where tundri comes in.
40
+
41
+ ### In a nutshell
42
+ **tundri** reads the [Permifrost spec file](https://gitlab.com/gitlab-data/permifrost#spec_file) and compares with the current state of the Snowflake account. It then creates, drops, and alters the objects to match. It leverages Permifrost's YAML `meta` tags to set attributes like `default_role` for users and `warehouse_size` for warehouses. Once the objects are created, tundri runs Permifrost to set the permissions.
43
+
44
+ ## Getting started
45
+
46
+ ### Prerequisites
47
+
48
+ - Credentials to a Snowflake user account with the `securityadmin` role
49
+ - A Permifrost spec file
50
+
51
+ ### Install
52
+
53
+ ```bash
54
+ pip install tundri
55
+ ```
56
+
57
+ ### Configure
58
+
59
+ #### Permifrost
60
+ Add a valid [Permifrost spec file](https://gitlab.com/gitlab-data/permifrost#spec_file) to your repository. You can use the files in the `examples` folder as reference.
61
+
62
+ #### Snowflake
63
+ Set up your Snowflake connection details in the environment variables listed below.
64
+
65
+ > [!TIP]
66
+ > You can use a `.env` file to store your credentials. Place it in the same folder as the Permifrost spec file.
67
+
68
+ ```bash
69
+ PERMISSION_BOT_ACCOUNT=abc134.west-europe.azure # Your account identifier
70
+ PERMISSION_BOT_USER=PERMIFROST
71
+ PERMISSION_BOT_PASSWORD=...
72
+ PERMISSION_BOT_ROLE=SECURITYADMIN # Permifrost requires it to be `SECURITYADMIN`
73
+ PERMISSION_BOT_DATABASE=PERMIFROST
74
+ PERMISSION_BOT_WAREHOUSE=ADMIN
75
+ ```
76
+
77
+ ### Usage
78
+ The `run` subcommand is going to drop/create objects and run Permifrost.
79
+
80
+ #### Dry run
81
+ ```bash
82
+ tundri run --permifrost_spec_path examples/permifrost.yml --dry
83
+ ```
84
+
85
+ #### Normal run
86
+ ```bash
87
+ tundri run --permifrost_spec_path examples/permifrost.yml
88
+ ```
89
+
90
+ #### Getting help
91
+ ```bash
92
+ tundri --help
93
+ ```
94
+
95
+ ## Development
96
+ ### Local setup
97
+ Install the development dependencies
98
+
99
+ ```bash
100
+ uv sync
101
+ ```
102
+
103
+ ### Run tests
104
+ Run the tests
105
+ ```bash
106
+ uv run pytest -v
107
+ ```
108
+
109
+ ### Formatting
110
+ Run the command below to format the code
111
+ ```bash
112
+ uv run black .
113
+ ```
114
+
115
+ ### Testing locally
116
+ Dry run with the example spec file
117
+ ```bash
118
+ uv run tundri run --dry -p examples/permifrost.yml
119
+ ```
120
+
121
+ ## Contributing
122
+
123
+ ### Release process
124
+
125
+ The release process is automated using GitHub Actions. Here's how it works:
126
+
127
+ 1. **Adding new features or bug fixes**
128
+ - PR tests run automatically to verify the changes on each PR
129
+ - Multiple PRs can be merged to main until a release-ready state is reached
130
+
131
+ 1. **Initiating a Release**
132
+ - A maintainer triggers the manual release workflow
133
+ - They specify the version bump type (`major`, `minor`, or `patch`)
134
+ - This creates a release branch and PR with updated version
135
+
136
+ 1. **Release Creation**
137
+ - When the release PR is merged to main:
138
+ - A Git tag is created (e.g., `v1.2.3`)
139
+ - A GitHub release is created
140
+ - The package is published to PyPI
141
+
142
+ The process requires the following GitHub secrets to be configured:
143
+ - `PYPI_API_TOKEN`: For production PyPI publishing
144
+ - `TEST_PYPI_API_TOKEN`: For TestPyPI publishing
145
+ - `SNOWFLAKE_*`: Snowflake credentials for running tests
146
+
147
+ For full details on the release workflow, see [RELEASE_WORKFLOW.md](docs/RELEASE_WORKFLOW.md).
tundri-1.3.3/README.md ADDED
@@ -0,0 +1,120 @@
1
+ <div align="center">
2
+ <img src="docs/images/logo.jpg" alt="tundri Logo" width="200">
3
+ </div>
4
+
5
+ **tundri** is a Python package to declaratively create, drop, and alter Snowflake objects and manage their permissions with [Permifrost](https://gitlab.com/gitlab-data/permifrost).
6
+
7
+ ## Motivation
8
+
9
+ Permifrost is great at managing permissions, but it doesn't create or alter objects. As [GitLab's data team handbook](https://handbook.gitlab.com/handbook/enterprise-data/platform/permifrost/) states:
10
+ > Object creation and deletion is not managed by permifrost
11
+
12
+ With only Permifrost, one would have to manually create the objects and then run Permifrost to set the permissions. This is error prone and time consuming. That is where tundri comes in.
13
+
14
+ ### In a nutshell
15
+ **tundri** reads the [Permifrost spec file](https://gitlab.com/gitlab-data/permifrost#spec_file) and compares with the current state of the Snowflake account. It then creates, drops, and alters the objects to match. It leverages Permifrost's YAML `meta` tags to set attributes like `default_role` for users and `warehouse_size` for warehouses. Once the objects are created, tundri runs Permifrost to set the permissions.
16
+
17
+ ## Getting started
18
+
19
+ ### Prerequisites
20
+
21
+ - Credentials to a Snowflake user account with the `securityadmin` role
22
+ - A Permifrost spec file
23
+
24
+ ### Install
25
+
26
+ ```bash
27
+ pip install tundri
28
+ ```
29
+
30
+ ### Configure
31
+
32
+ #### Permifrost
33
+ Add a valid [Permifrost spec file](https://gitlab.com/gitlab-data/permifrost#spec_file) to your repository. You can use the files in the `examples` folder as reference.
34
+
35
+ #### Snowflake
36
+ Set up your Snowflake connection details in the environment variables listed below.
37
+
38
+ > [!TIP]
39
+ > You can use a `.env` file to store your credentials. Place it in the same folder as the Permifrost spec file.
40
+
41
+ ```bash
42
+ PERMISSION_BOT_ACCOUNT=abc134.west-europe.azure # Your account identifier
43
+ PERMISSION_BOT_USER=PERMIFROST
44
+ PERMISSION_BOT_PASSWORD=...
45
+ PERMISSION_BOT_ROLE=SECURITYADMIN # Permifrost requires it to be `SECURITYADMIN`
46
+ PERMISSION_BOT_DATABASE=PERMIFROST
47
+ PERMISSION_BOT_WAREHOUSE=ADMIN
48
+ ```
49
+
50
+ ### Usage
51
+ The `run` subcommand is going to drop/create objects and run Permifrost.
52
+
53
+ #### Dry run
54
+ ```bash
55
+ tundri run --permifrost_spec_path examples/permifrost.yml --dry
56
+ ```
57
+
58
+ #### Normal run
59
+ ```bash
60
+ tundri run --permifrost_spec_path examples/permifrost.yml
61
+ ```
62
+
63
+ #### Getting help
64
+ ```bash
65
+ tundri --help
66
+ ```
67
+
68
+ ## Development
69
+ ### Local setup
70
+ Install the development dependencies
71
+
72
+ ```bash
73
+ uv sync
74
+ ```
75
+
76
+ ### Run tests
77
+ Run the tests
78
+ ```bash
79
+ uv run pytest -v
80
+ ```
81
+
82
+ ### Formatting
83
+ Run the command below to format the code
84
+ ```bash
85
+ uv run black .
86
+ ```
87
+
88
+ ### Testing locally
89
+ Dry run with the example spec file
90
+ ```bash
91
+ uv run tundri run --dry -p examples/permifrost.yml
92
+ ```
93
+
94
+ ## Contributing
95
+
96
+ ### Release process
97
+
98
+ The release process is automated using GitHub Actions. Here's how it works:
99
+
100
+ 1. **Adding new features or bug fixes**
101
+ - PR tests run automatically to verify the changes on each PR
102
+ - Multiple PRs can be merged to main until a release-ready state is reached
103
+
104
+ 1. **Initiating a Release**
105
+ - A maintainer triggers the manual release workflow
106
+ - They specify the version bump type (`major`, `minor`, or `patch`)
107
+ - This creates a release branch and PR with updated version
108
+
109
+ 1. **Release Creation**
110
+ - When the release PR is merged to main:
111
+ - A Git tag is created (e.g., `v1.2.3`)
112
+ - A GitHub release is created
113
+ - The package is published to PyPI
114
+
115
+ The process requires the following GitHub secrets to be configured:
116
+ - `PYPI_API_TOKEN`: For production PyPI publishing
117
+ - `TEST_PYPI_API_TOKEN`: For TestPyPI publishing
118
+ - `SNOWFLAKE_*`: Snowflake credentials for running tests
119
+
120
+ For full details on the release workflow, see [RELEASE_WORKFLOW.md](docs/RELEASE_WORKFLOW.md).