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.
- spot_planner-0.1.0/.github/SETUP.md +130 -0
- spot_planner-0.1.0/.github/workflows/publish.yml +249 -0
- spot_planner-0.1.0/.gitignore +15 -0
- spot_planner-0.1.0/.python-version +1 -0
- spot_planner-0.1.0/Cargo.lock +740 -0
- spot_planner-0.1.0/Cargo.toml +14 -0
- spot_planner-0.1.0/PKG-INFO +146 -0
- spot_planner-0.1.0/README.md +137 -0
- spot_planner-0.1.0/pyproject.toml +22 -0
- spot_planner-0.1.0/scripts/release.sh +58 -0
- spot_planner-0.1.0/src/lib.rs +170 -0
- spot_planner-0.1.0/src/spot_planner/__init__.py +3 -0
- spot_planner-0.1.0/src/spot_planner/main.py +152 -0
- spot_planner-0.1.0/src/spot_planner/py.typed +0 -0
- spot_planner-0.1.0/tests/main_test.py +176 -0
- spot_planner-0.1.0/tests/test_rust_vs_python.py +511 -0
- spot_planner-0.1.0/uv.lock +79 -0
|
@@ -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 @@
|
|
|
1
|
+
3.13
|