spot-planner 0.1.0__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.
@@ -0,0 +1,130 @@
1
+ # GitHub Actions Setup Guide
2
+
3
+ This guide explains how to set up the GitHub Actions workflow for building and publishing `spot-planner` to PyPI.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **PyPI Account**: You need a PyPI account to publish packages
8
+ 2. **PyPI Project**: The project must be registered on PyPI
9
+ 3. **GitHub Repository**: The code must be in a GitHub repository
10
+
11
+ ## Setup Steps
12
+
13
+ ### 1. Configure PyPI OIDC Trusted Publishing
14
+
15
+ 1. Go to [PyPI](https://pypi.org) and log in
16
+ 2. Navigate to Account Settings → API tokens
17
+ 3. Click "Add API token"
18
+ 4. Select "Create a token for trusted publishing"
19
+ 5. Configure the trusted publisher:
20
+ - **PyPI project name**: `spot-planner`
21
+ - **Owner**: Your GitHub username or organization
22
+ - **Repository name**: `spot-planner`
23
+ - **Workflow filename**: `publish.yml`
24
+ - **Environment name**: `pypi` (optional)
25
+ 6. Click "Add token"
26
+
27
+ **Note**: The workflow includes the required `id-token: write` permission for OIDC authentication.
28
+
29
+ ### 2. Create PyPI Environment
30
+
31
+ 1. Go to your GitHub repository
32
+ 2. Navigate to Settings → Environments
33
+ 3. Click "New environment"
34
+ 4. Name: `pypi`
35
+ 5. Click "Configure environment"
36
+
37
+ **Optional Environment Protection:**
38
+
39
+ - **Required reviewers**: Add team members who must approve PyPI releases
40
+ - **Wait timer**: Add a delay before publishing (useful for rollback)
41
+ - **Deployment branches**: Restrict which branches can trigger publishing
42
+
43
+ ### 3. Test the Workflow
44
+
45
+ The workflow will automatically run on:
46
+
47
+ - **Every push to master branch**: Builds wheels and runs tests
48
+ - **Tagged commits**: Builds wheels, runs tests, AND publishes to PyPI
49
+
50
+ To test publishing:
51
+
52
+ ```bash
53
+ # Create and push a tag
54
+ git tag v0.1.0
55
+ git push origin v0.1.0
56
+
57
+ # Or push to master to trigger build-only
58
+ git push origin master
59
+ ```
60
+
61
+ ## Workflow Details
62
+
63
+ ### Jobs Overview
64
+
65
+ 1. **check-tag**: Determines if the current commit is tagged
66
+ 2. **build-wheels**: Builds native wheels for AMD64 and ARM64 Linux
67
+ 3. **publish**: Publishes to PyPI (only for tagged releases)
68
+ 4. **test-build**: Runs tests on every push to master
69
+
70
+ ### Supported Platforms
71
+
72
+ - **AMD64 Linux** (`x86_64-unknown-linux-gnu`): Standard x86_64 systems
73
+ - **ARM64 Linux** (`aarch64-unknown-linux-gnu`): Raspberry Pi 4/5, ARM servers
74
+
75
+ ### Version Management
76
+
77
+ The workflow automatically updates version numbers from git tags:
78
+
79
+ - Tag `v1.2.3` → Version `1.2.3`
80
+ - Tag `1.2.3` → Version `1.2.3`
81
+ - Updates both `pyproject.toml` and `Cargo.toml`
82
+
83
+ ### Artifacts
84
+
85
+ Each build creates:
86
+
87
+ - **Wheel files**: `spot_planner-{version}-cp3*-{platform}.whl`
88
+ - **Source distribution**: `spot-planner-{version}.tar.gz`
89
+
90
+ ## Troubleshooting
91
+
92
+ ### Common Issues
93
+
94
+ 1. **Build fails on ARM64**:
95
+
96
+ - Check that cross-compilation dependencies are installed
97
+ - Verify linker settings in the workflow
98
+
99
+ 2. **Publishing fails**:
100
+
101
+ - Verify `PYPI_API_TOKEN` secret is set correctly
102
+ - Check that the version doesn't already exist on PyPI
103
+ - Ensure the tag format is correct
104
+
105
+ 3. **Tests fail**:
106
+ - Check that all dependencies are installed
107
+ - Verify the Rust module builds correctly
108
+
109
+ ### Manual Publishing
110
+
111
+ If you need to publish manually:
112
+
113
+ ```bash
114
+ # Build wheels locally
115
+ maturin build --release --target x86_64-unknown-linux-gnu
116
+ maturin build --release --target aarch64-unknown-linux-gnu
117
+
118
+ # Build source distribution
119
+ uv build --sdist
120
+
121
+ # Upload to PyPI (requires API token for manual upload)
122
+ uv publish dist/*
123
+ ```
124
+
125
+ ## Security Notes
126
+
127
+ - **OIDC Authentication**: No long-lived tokens stored in GitHub secrets
128
+ - **Trusted Publishing**: PyPI verifies the GitHub Actions workflow and environment
129
+ - **Automatic Rotation**: OIDC tokens are short-lived and automatically rotated
130
+ - **Least Privilege**: Only the specific repository and workflow can publish
@@ -0,0 +1,249 @@
1
+ name: Build and Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ env:
13
+ CARGO_TERM_COLOR: always
14
+
15
+ jobs:
16
+ check-tag:
17
+ runs-on: ubuntu-latest
18
+ outputs:
19
+ is_tagged: ${{ steps.check.outputs.is_tagged }}
20
+ tag: ${{ steps.check.outputs.tag }}
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v4
24
+ with:
25
+ fetch-depth: 0 # Fetch full history for tag detection
26
+
27
+ - name: Check if this is a tagged release
28
+ id: check
29
+ run: |
30
+ if git describe --exact-match --tags HEAD 2>/dev/null; then
31
+ echo "is_tagged=true" >> $GITHUB_OUTPUT
32
+ echo "tag=$(git describe --exact-match --tags HEAD)" >> $GITHUB_OUTPUT
33
+ else
34
+ echo "is_tagged=false" >> $GITHUB_OUTPUT
35
+ fi
36
+
37
+ build-wheels:
38
+ needs: check-tag
39
+ runs-on: ubuntu-latest
40
+ strategy:
41
+ matrix:
42
+ target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu]
43
+ include:
44
+ - target: x86_64-unknown-linux-gnu
45
+ platform: linux-x86_64
46
+ - target: aarch64-unknown-linux-gnu
47
+ platform: linux-aarch64
48
+
49
+ steps:
50
+ - name: Checkout code
51
+ uses: actions/checkout@v4
52
+
53
+ - name: Install Rust
54
+ uses: dtolnay/rust-toolchain@stable
55
+ with:
56
+ targets: ${{ matrix.target }}
57
+
58
+ - name: Cache cargo registry
59
+ uses: actions/cache@v4
60
+ with:
61
+ path: |
62
+ ~/.cargo/registry
63
+ ~/.cargo/git
64
+ ~/.cargo/bin
65
+ key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
66
+ restore-keys: |
67
+ ${{ runner.os }}-cargo-${{ matrix.target }}-
68
+ ${{ runner.os }}-cargo-
69
+
70
+ - name: Cache maturin
71
+ uses: actions/cache@v4
72
+ with:
73
+ path: ~/.cargo/bin/maturin
74
+ key: ${{ runner.os }}-maturin-1.9.5
75
+ restore-keys: |
76
+ ${{ runner.os }}-maturin-
77
+
78
+ - name: Install cross-compilation dependencies (ARM)
79
+ if: matrix.target == 'aarch64-unknown-linux-gnu'
80
+ run: |
81
+ sudo apt-get update
82
+ sudo apt-get install -y gcc-aarch64-linux-gnu
83
+ rustup target add aarch64-unknown-linux-gnu
84
+
85
+ - name: Install maturin
86
+ run: |
87
+ # Check if maturin is already cached
88
+ if command -v maturin &> /dev/null; then
89
+ echo "maturin already installed, skipping installation"
90
+ maturin --version
91
+ else
92
+ echo "Installing maturin..."
93
+ # Use a specific version and add timeout to avoid cancellation
94
+ timeout 600 cargo install maturin --version 1.9.5 --locked --force
95
+ fi
96
+
97
+ - name: Update version from tag
98
+ if: needs.check-tag.outputs.is_tagged == 'true'
99
+ run: |
100
+ TAG_VERSION=${{ needs.check-tag.outputs.tag }}
101
+ # Remove 'v' prefix if present
102
+ TAG_VERSION=${TAG_VERSION#v}
103
+ echo "Updating version to $TAG_VERSION"
104
+
105
+ # Update pyproject.toml
106
+ sed -i "s/^version = \".*\"/version = \"$TAG_VERSION\"/" pyproject.toml
107
+
108
+ # Update Cargo.toml
109
+ sed -i "s/^version = \".*\"/version = \"$TAG_VERSION\"/" Cargo.toml
110
+
111
+ echo "Updated version to $TAG_VERSION"
112
+
113
+ - name: Install uv
114
+ uses: astral-sh/setup-uv@v3
115
+ with:
116
+ version: "latest"
117
+
118
+ - name: Build wheel
119
+ env:
120
+ TARGET: ${{ matrix.target }}
121
+ run: |
122
+ if [ "$TARGET" = "x86_64-unknown-linux-gnu" ]; then
123
+ maturin build --release --target $TARGET --out dist
124
+ else
125
+ # For ARM64, we need to set the linker and use uv's Python
126
+ export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
127
+ export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
128
+ export AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar
129
+ export RANLIB_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ranlib
130
+
131
+ # Create a virtual environment with uv and activate it
132
+ uv venv --python 3.13
133
+ source .venv/bin/activate
134
+
135
+ # Get the Python interpreter path from the venv
136
+ PYTHON_PATH=$(which python)
137
+ echo "Using Python at: $PYTHON_PATH"
138
+ python --version
139
+
140
+ # Create a symlink with the expected name for maturin
141
+ sudo ln -sf "$PYTHON_PATH" /usr/local/bin/python3.13
142
+
143
+ # Set up cargo config for cross-compilation
144
+ mkdir -p .cargo
145
+ echo '[target.aarch64-unknown-linux-gnu]' > .cargo/config.toml
146
+ echo 'linker = "aarch64-linux-gnu-gcc"' >> .cargo/config.toml
147
+ echo 'ar = "aarch64-linux-gnu-ar"' >> .cargo/config.toml
148
+
149
+ # Build with the interpreter name maturin expects for cross-compilation
150
+ maturin build --release --target $TARGET --out dist -i python3.13
151
+ fi
152
+
153
+ - name: Upload wheel artifacts
154
+ uses: actions/upload-artifact@v4
155
+ with:
156
+ name: wheel-${{ matrix.platform }}
157
+ path: dist/*.whl
158
+
159
+ publish:
160
+ needs: [check-tag, build-wheels]
161
+ runs-on: ubuntu-latest
162
+ if: github.ref == 'refs/heads/master' && needs.check-tag.outputs.is_tagged == 'true'
163
+ environment: pypi
164
+
165
+ steps:
166
+ - name: Checkout code
167
+ uses: actions/checkout@v4
168
+
169
+ - name: Download all wheel artifacts
170
+ uses: actions/download-artifact@v4
171
+ with:
172
+ path: dist
173
+
174
+ - name: Install uv
175
+ uses: astral-sh/setup-uv@v3
176
+ with:
177
+ version: "latest"
178
+
179
+ - name: Build source distribution
180
+ run: uv build --sdist
181
+
182
+ - name: Publish to PyPI
183
+ run: |
184
+ # Upload all wheels and source distribution using OIDC
185
+ uv publish dist/*
186
+
187
+ test-build:
188
+ runs-on: ubuntu-latest
189
+ if: github.ref == 'refs/heads/master'
190
+
191
+ steps:
192
+ - name: Checkout code
193
+ uses: actions/checkout@v4
194
+
195
+ - name: Install Rust
196
+ uses: dtolnay/rust-toolchain@stable
197
+
198
+ - name: Cache cargo registry
199
+ uses: actions/cache@v4
200
+ with:
201
+ path: |
202
+ ~/.cargo/registry
203
+ ~/.cargo/git
204
+ ~/.cargo/bin
205
+ key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
206
+ restore-keys: |
207
+ ${{ runner.os }}-cargo-test-
208
+ ${{ runner.os }}-cargo-
209
+
210
+ - name: Cache maturin
211
+ uses: actions/cache@v4
212
+ with:
213
+ path: ~/.cargo/bin/maturin
214
+ key: ${{ runner.os }}-maturin-1.9.5
215
+ restore-keys: |
216
+ ${{ runner.os }}-maturin-
217
+
218
+ - name: Install maturin
219
+ run: |
220
+ # Check if maturin is already cached
221
+ if command -v maturin &> /dev/null; then
222
+ echo "maturin already installed, skipping installation"
223
+ maturin --version
224
+ else
225
+ echo "Installing maturin..."
226
+ # Use a specific version and add timeout to avoid cancellation
227
+ timeout 600 cargo install maturin --version 1.9.5 --locked --force
228
+ fi
229
+
230
+ - name: Install uv
231
+ uses: astral-sh/setup-uv@v3
232
+ with:
233
+ version: "latest"
234
+
235
+ - name: Install Python dependencies
236
+ run: uv sync --dev
237
+
238
+ - name: Build in development mode
239
+ run: |
240
+ # Activate the virtual environment created by uv
241
+ source .venv/bin/activate
242
+ maturin develop --release
243
+
244
+ - name: Run tests
245
+ run: uv run pytest tests/ -v
246
+
247
+ - name: Test import
248
+ run: |
249
+ uv run python -c "from spot_planner import get_cheapest_periods; print('Import successful')"
@@ -0,0 +1,15 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ *.so
13
+
14
+ # Rust build artifacts
15
+ target/
@@ -0,0 +1 @@
1
+ 3.13