eo-processor 0.2.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.
Files changed (35) hide show
  1. eo_processor-0.2.0/.github/FUNDING.yml +15 -0
  2. eo_processor-0.2.0/.github/workflows/ci.yml +68 -0
  3. eo_processor-0.2.0/.github/workflows/jules-autofix.yml +130 -0
  4. eo_processor-0.2.0/.github/workflows/release.yml +236 -0
  5. eo_processor-0.2.0/.gitignore +73 -0
  6. eo_processor-0.2.0/CONTRIBUTING.md +220 -0
  7. eo_processor-0.2.0/Cargo.lock +387 -0
  8. eo_processor-0.2.0/Cargo.toml +26 -0
  9. eo_processor-0.2.0/LICENSE +21 -0
  10. eo_processor-0.2.0/Makefile +76 -0
  11. eo_processor-0.2.0/PKG-INFO +288 -0
  12. eo_processor-0.2.0/QUICKSTART.md +245 -0
  13. eo_processor-0.2.0/README.md +259 -0
  14. eo_processor-0.2.0/SECURITY.md +69 -0
  15. eo_processor-0.2.0/coverage.xml +31 -0
  16. eo_processor-0.2.0/examples/basic_usage.py +115 -0
  17. eo_processor-0.2.0/examples/map_blocks.py +203 -0
  18. eo_processor-0.2.0/examples/xarray_dask_usage.py +183 -0
  19. eo_processor-0.2.0/pyproject.toml +58 -0
  20. eo_processor-0.2.0/pytest.ini +6 -0
  21. eo_processor-0.2.0/python/eo_processor/__init__.py +93 -0
  22. eo_processor-0.2.0/python/eo_processor/__init__.pyi +19 -0
  23. eo_processor-0.2.0/scripts/generate_coverage_badge.py +77 -0
  24. eo_processor-0.2.0/scripts/jules_session_manager.py +89 -0
  25. eo_processor-0.2.0/scripts/version.py +199 -0
  26. eo_processor-0.2.0/src/indices.rs +547 -0
  27. eo_processor-0.2.0/src/lib.rs +25 -0
  28. eo_processor-0.2.0/src/spatial.rs +401 -0
  29. eo_processor-0.2.0/src/temporal.rs +0 -0
  30. eo_processor-0.2.0/src/tests.rs +48 -0
  31. eo_processor-0.2.0/tests/__init__.py +0 -0
  32. eo_processor-0.2.0/tests/test_indices.py +191 -0
  33. eo_processor-0.2.0/tests/test_spatial.py +67 -0
  34. eo_processor-0.2.0/tox.ini +74 -0
  35. eo_processor-0.2.0/uv.lock +1427 -0
@@ -0,0 +1,15 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4
+ patreon: bnjam
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: # Replace with a single Ko-fi username
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12
+ polar: # Replace with a single Polar username
13
+ buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14
+ thanks_dev: # Replace with a single thanks.dev username
15
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -0,0 +1,68 @@
1
+ name: CI
2
+
3
+ # --- TRIGGER: Runs on Pull Requests ---
4
+ on:
5
+ pull_request:
6
+ types: [opened, synchronize, reopened, edited]
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ env:
13
+ CARGO_TERM_COLOR: always
14
+
15
+ jobs:
16
+ test:
17
+ name: Tests via tox (Python ${{ matrix.python-version }})
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ contents: write # Needed for coverage badge/artifacts
21
+ strategy:
22
+ matrix:
23
+ python-version: ["3.11", "3.12"]
24
+ steps:
25
+ - name: Checkout repository
26
+ uses: actions/checkout@v4
27
+
28
+ # --- SETUP ---
29
+ - name: Install Rust toolchain
30
+ # Required for the 'clippy' environment and maturin builds
31
+ uses: dtolnay/rust-toolchain@stable
32
+
33
+ - name: Set up Python ${{ matrix.python-version }}
34
+ uses: actions/setup-python@v5
35
+ with:
36
+ python-version: ${{ matrix.python-version }}
37
+
38
+ - name: Install uv and tools
39
+ uses: astral-sh/setup-uv@v3
40
+
41
+ - name: Install tox and plugins
42
+ # Install tox and the tox-gh-actions plugin into the base environment
43
+ run: uv pip install --system tox tox-gh-actions
44
+
45
+ # --- LINT & CLIPPY STEPS (Run once on 3.11) ---
46
+ # Running static checks only on one version to save time
47
+ - name: ๐Ÿ”Ž Run Lint (tox -e lint)
48
+ if: matrix.python-version == '3.11'
49
+ run: tox -e lint
50
+
51
+ - name: ๐Ÿ”Ž Run Rust Checks (tox -e clippy)
52
+ if: matrix.python-version == '3.11'
53
+ run: tox -e clippy
54
+
55
+ # --- CORE TESTS STEP (Run on both 3.11 and 3.12) ---
56
+ - name: ๐Ÿงช Run Tests (tox -e py${{ matrix.python-version }})
57
+ id: tests_step
58
+ # Runs 'maturin develop' and 'pytest -q'
59
+ run: tox -e py${{ matrix.python-version }}
60
+
61
+ # --- COVERAGE & BADGE STEPS (Run once on 3.12) ---
62
+ - name: ๐Ÿ“Š Run Coverage Threshold Check
63
+ if: matrix.python-version == '3.12'
64
+ id: coverage_step
65
+ # This assumes the tox 'coverage' environment produces a coverage.xml file in the repository root.
66
+ run: |
67
+ set -euo pipefail
68
+ tox -e coverage
@@ -0,0 +1,130 @@
1
+ name: ๐Ÿค– Jules Autofix
2
+
3
+ # This workflow runs only when the main CI workflow completes and fails on a pull_request
4
+ on:
5
+ workflow_run:
6
+ workflows: [CI]
7
+ types: [completed]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ jules_autofix:
15
+ name: Run Jules Fix Session
16
+ # Use a faster, lighter runner since it's mostly Python scripting
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ pull-requests: write # Needed to comment on the PR
20
+ contents: read # Needed to checkout code
21
+ actions: read # Needed to list/download artifacts
22
+
23
+ # Only run if the CI failed AND it was triggered by a pull request
24
+ if: |
25
+ github.event.workflow_run.conclusion == 'failure' &&
26
+ github.event.workflow_run.event == 'pull_request'
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+ with:
32
+ # Use the commit SHA from the failed run to ensure context is correct
33
+ ref: ${{ github.event.workflow_run.head_sha }}
34
+
35
+ - name: Get PR Number and Author
36
+ id: get_pr
37
+ uses: actions/github-script@v6
38
+ with:
39
+ script: |
40
+ // Find the PR associated with the commit SHA from the failed workflow run
41
+ const { data: pullRequest } = await github.rest.pulls.list({
42
+ owner: context.repo.owner,
43
+ repo: context.repo.repo,
44
+ head: context.repo.owner + ':' + context.event.workflow_run.head_branch,
45
+ });
46
+ if (pullRequest.length === 0) {
47
+ core.info('Could not find corresponding pull request. Skipping.');
48
+ return;
49
+ }
50
+ core.setOutput('pr_number', pullRequest[0].number);
51
+ core.setOutput('pr_author', pullRequest[0].user.login);
52
+ core.setOutput('pr_repo', pullRequest[0].base.repo.full_name);
53
+
54
+ - name: Check if Author is "bnjam" (or other approved user)
55
+ # This conditional check ensures the script only runs for approved users, matching the original logic
56
+ if: ${{ steps.get_pr.outputs.pr_author != 'bnjam' }}
57
+ run: |
58
+ echo "Skipping Jules session. PR author is not 'bnjam'."
59
+ exit 0
60
+
61
+ - name: Set up Python
62
+ uses: actions/setup-python@v5
63
+ with:
64
+ python-version: "3.12"
65
+
66
+ # --- Download Artifacts from failed run ---
67
+ - name: ๐Ÿ“ฅ Find and Download Failure Context
68
+ uses: actions/github-script@v6
69
+ id: download_artifact
70
+ with:
71
+ script: |
72
+ const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
73
+ owner: context.repo.owner,
74
+ repo: context.repo.repo,
75
+ run_id: context.event.workflow_run.id,
76
+ });
77
+
78
+ // Find the artifact uploaded by the failing test job (e.g., failure-context-3.12)
79
+ const matchArtifact = artifacts.data.artifacts.find(artifact =>
80
+ artifact.name.startsWith('failure-context-')
81
+ );
82
+
83
+ if (matchArtifact) {
84
+ const download = await github.rest.actions.downloadArtifact({
85
+ owner: context.repo.owner,
86
+ repo: context.repo.repo,
87
+ artifact_id: matchArtifact.id,
88
+ archive_format: 'zip',
89
+ });
90
+
91
+ const fs = require('fs');
92
+ const unzip = require('unzip-stream');
93
+
94
+ // This downloads the zip, extracts it, and makes the error message available
95
+ fs.writeFileSync('failure-context.zip', Buffer.from(download.data));
96
+ fs.mkdirSync('failure-context');
97
+ const readStream = fs.createReadStream('failure-context.zip');
98
+ const extractStream = unzip.Extract({ path: 'failure-context' });
99
+
100
+ await new Promise((resolve, reject) => {
101
+ extractStream.on('close', resolve);
102
+ extractStream.on('error', reject);
103
+ readStream.pipe(extractStream);
104
+ });
105
+
106
+ console.log('Successfully downloaded and extracted failure context.');
107
+ } else {
108
+ core.setFailed('Could not find failure-context artifact. Aborting autofix attempt.');
109
+ }
110
+
111
+ - name: Install dependencies for API interaction
112
+ run: pip install requests # Need 'requests' to talk to the Jules API
113
+
114
+ - name: ๐Ÿ’ฌ Run Jules Session Manager
115
+ env:
116
+ JULES_API_KEY: ${{ secrets.JULES_API_KEY }}
117
+ REPO_NAME: ${{ steps.get_pr.outputs.pr_repo }}
118
+ PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
119
+ BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
120
+ ERROR_MESSAGE: ""
121
+ run: |
122
+ # Load the error message from the downloaded artifact
123
+ if [ -f failure-context/error_message.txt ]; then
124
+ export ERROR_MESSAGE=$(cat failure-context/error_message.txt)
125
+ else
126
+ export ERROR_MESSAGE="CI failed, but the specific error message artifact was missing."
127
+ fi
128
+
129
+ # Execute the Python script that interacts with the fixing system
130
+ python ./scripts/jules_session_manager.py
@@ -0,0 +1,236 @@
1
+ name: Build and Publish Wheels
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ env:
8
+ # The environment for PyPI publishing (used in the final publish step)
9
+ PYPI_ENVIRONMENT: pypi
10
+
11
+ permissions:
12
+ contents: write # Required for the version-bump job to commit and tag
13
+ id-token: write # Required for maturin to publish to PyPI securely via OIDC
14
+
15
+ jobs:
16
+ # ----------------------------------------------------
17
+ # JOB 1: VERSION BUMP, COMMIT, AND TAG
18
+ # ----------------------------------------------------
19
+ version-bump:
20
+ if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: write # Required to push commits and tags
24
+ outputs:
25
+ new_version: ${{ steps.bump.outputs.new_version }}
26
+ version_changed: ${{ steps.bump.outputs.version_changed }}
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ # Fetch depth 0 needed for full history access for version bump logic
32
+ fetch-depth: 0
33
+ token: ${{ secrets.GITHUB_TOKEN }}
34
+
35
+ - name: Set up Python
36
+ uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.12"
39
+
40
+ - name: Install uv
41
+ run: |
42
+ pip install uv
43
+
44
+ - name: Install Dependencies for version script
45
+ run: |
46
+ # Sync dev dependencies to ensure the version script's dependencies are met
47
+ uv pip install --system maturin ".[dev]"
48
+
49
+ - name: Determine version increment, Commit, and Tag
50
+ id: bump
51
+ # NOTE: You MUST ensure your Python version script (scripts/version.py)
52
+ # also updates the version in Cargo.toml for the Rust code,
53
+ # otherwise maturin will build with the old version number!
54
+ # Assuming your script now handles both: pyproject.toml and Cargo.toml
55
+ run: |
56
+ # Use github.event context to get PR information
57
+ PR_NUM=${{ github.event.pull_request.number }}
58
+ BRANCH_NAME=${{ github.event.pull_request.head.ref }}
59
+ LABELS=$(echo '${{ toJSON(github.event.pull_request.labels.*.name) }}' | tr -d '[]"' | sed 's/,/ /g')
60
+
61
+ echo "PR #$PR_NUM"
62
+ echo "Branch name: $BRANCH_NAME"
63
+ echo "PR labels: $LABELS"
64
+
65
+ INCREMENT_TYPE=""
66
+ if echo "$LABELS" | grep -q "bump:none"; then
67
+ INCREMENT_TYPE=""
68
+ elif echo "$LABELS" | grep -q "bump:major\|bump:release"; then
69
+ INCREMENT_TYPE="major"
70
+ elif echo "$LABELS" | grep -q "bump:major\|bump:major"; then
71
+ INCREMENT_TYPE="major"
72
+ elif echo "$LABELS" | grep -q "bump:minor\|bump:feature"; then
73
+ INCREMENT_TYPE="minor"
74
+ elif echo "$LABELS" | grep -q "bump:minor\|bump:minor"; then
75
+ INCREMENT_TYPE="minor"
76
+ elif echo "$LABELS" | grep -q "bump:patch\|bump:hotfix"; then
77
+ INCREMENT_TYPE="patch"
78
+ elif echo "$LABELS" | grep -q "bump:patch\|bump:patch"; then
79
+ INCREMENT_TYPE="patch"
80
+ fi
81
+
82
+ if [ -z "$INCREMENT_TYPE" ]; then
83
+ echo "No version bump label or recognized branch prefix found, skipping version bump"
84
+ echo "version_changed=false" >> $GITHUB_OUTPUT
85
+ exit 0
86
+ fi
87
+
88
+ echo "Version increment type: $INCREMENT_TYPE"
89
+ CURRENT_VERSION=$(python scripts/version.py current | cut -d' ' -f3)
90
+ echo "Current version: $CURRENT_VERSION"
91
+
92
+ # Increment version (This must update pyproject.toml AND Cargo.toml)
93
+ python scripts/version.py $INCREMENT_TYPE
94
+ NEW_VERSION=$(python scripts/version.py current | cut -d' ' -f3)
95
+
96
+ echo "New version: $NEW_VERSION"
97
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
98
+ echo "version_changed=true" >> $GITHUB_OUTPUT
99
+
100
+ # Configure git for commit
101
+ git config --local user.email "action@github.com"
102
+ git config --local user.name "GitHub Action"
103
+
104
+ # Commit version changes (must include pyproject.toml and Cargo.toml)
105
+ git add pyproject.toml Cargo.toml
106
+ # Add any other files updated by your version script (e.g., __init__.py)
107
+ git commit -m "chore: bump version to $NEW_VERSION (auto-increment from $BRANCH_NAME branch)"
108
+
109
+ # Pull latest changes from main to avoid push conflicts
110
+ git pull --rebase --autostash origin main || (git fetch origin main && git rebase origin/main)
111
+
112
+ # Push the version bump commit
113
+ git push
114
+
115
+ # Create and push tag
116
+ git tag "v$NEW_VERSION"
117
+ git push origin "v$NEW_VERSION"
118
+ env:
119
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
120
+
121
+ # ----------------------------------------------------
122
+ # JOB 2: BUILD AND PUBLISH TO PYPI
123
+ # ----------------------------------------------------
124
+ pypi-publish:
125
+ runs-on: ubuntu-latest
126
+ needs: [version-bump]
127
+ if: needs.version-bump.outputs.version_changed == 'true'
128
+
129
+ steps:
130
+ - name: Checkout repository
131
+ uses: actions/checkout@v4
132
+ with:
133
+ # Fetch the latest commit which contains the version bump and tag
134
+ ref: main
135
+
136
+ - name: Set up Python
137
+ uses: actions/setup-python@v5
138
+ with:
139
+ python-version: "3.12"
140
+
141
+ - name: Install Rust toolchain
142
+ uses: dtolnay/rust-toolchain@stable
143
+
144
+ - name: Install maturin
145
+ run: pip install maturin
146
+
147
+ - name: ๐Ÿ—๏ธ Build All Wheels and Source Distribution
148
+ # maturin builds for all major platforms (Linux/macOS/Windows) and Python versions
149
+ run: maturin build --release
150
+
151
+ - name: ๐Ÿš€ Publish to PyPI
152
+ env:
153
+ # Using the PYPI_ENVIRONMENT defined in the workflow env
154
+ MATURIN_PYPI_TOKEN: ${{ secrets.MATURIN_PYPI_TOKEN }}
155
+ # Uses maturin's built-in publishing command with OIDC (OpenID Connect)
156
+ run: maturin publish --skip-existing --repository pypi --username __token__ --password ${{ secrets.MATURIN_PYPI_TOKEN }}
157
+
158
+ # ----------------------------------------------------
159
+ # JOB 3: CREATE GITHUB RELEASE
160
+ # ----------------------------------------------------
161
+ github-release:
162
+ if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' && needs.version-bump.outputs.version_changed == 'true'
163
+ runs-on: ubuntu-latest
164
+ needs: [version-bump, pypi-publish] # Wait for both version bump and PyPI publish to ensure everything is ready
165
+ permissions:
166
+ contents: write
167
+ steps:
168
+ - name: Create Release
169
+ uses: ncipollo/release-action@v1
170
+ with:
171
+ tag: v${{ needs.version-bump.outputs.new_version }}
172
+ name: "Release v${{ needs.version-bump.outputs.new_version }}"
173
+ # A minimal body for the release, you could extend this
174
+ body: |
175
+ ## ๐ŸŽ‰ New Release: v${{ needs.version-bump.outputs.new_version }}
176
+
177
+ This release was automatically generated after a successful CI/CD pipeline.
178
+ token: ${{ secrets.GITHUB_TOKEN }}
179
+
180
+ # ----------------------------------------------------
181
+ # Coverage Badge Update Job
182
+ # ----------------------------------------------------
183
+ update-coverage-badge:
184
+ runs-on: ubuntu-latest
185
+ permissions:
186
+ contents: write
187
+ steps:
188
+ - name: Checkout repository
189
+ uses: actions/checkout@v4
190
+ - name: Set up Python
191
+ uses: actions/setup-python@v5
192
+ with:
193
+ python-version: "3.12"
194
+ - name: Install uv and tools
195
+ uses: astral-sh/setup-uv@v3
196
+ - name: Sync dependencies (dev + extras)
197
+ run: uv sync --all-extras --dev
198
+ # --- COVERAGE & BADGE STEPS (Run once on 3.12) ---
199
+ - name: ๐Ÿ“Š Run Coverage Reporting
200
+ id: coverage_step
201
+ # This assumes the 'coverage' env generates 'coverage.xml' in the repository root.
202
+ run: |
203
+ tox -e coverage
204
+ # Output coverage percentage for logging
205
+ COVERAGE_PERCENT=$(coverage report | tail -n 1 | awk '{print $4}' | sed 's/%//')
206
+ echo "Coverage Percentage: $COVERAGE_PERCENT%"
207
+ echo "coverage_percent=$COVERAGE_PERCENT" >> $GITHUB_OUTPUT
208
+
209
+ - name: ๐Ÿ–ผ๏ธ Generate Coverage Badge
210
+ # Runs your script using the installed uv environment
211
+ run: uv run python scripts/generate_coverage_badge.py coverage.xml coverage-badge.svg
212
+
213
+ # Optional: Upload the badge as an artifact so you can download it from the CI run
214
+ - name: Upload Coverage Badge Artifact
215
+ uses: actions/upload-artifact@v4
216
+ with:
217
+ name: coverage-badge
218
+ path: |
219
+ coverage.xml
220
+ coverage-badge.svg
221
+
222
+ # --- COMMIT & PUSH BADGE IF CHANGED (Run once on 3.12) ---
223
+ - name: ๐Ÿ“ค Commit and Push Coverage Badge if Changed
224
+ env:
225
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
226
+ run: |
227
+ if git diff --quiet coverage-badge.svg 2>/dev/null; then
228
+ echo 'No badge changes'
229
+ else
230
+ git config user.name 'github-actions'
231
+ git config user.email 'actions@github.com'
232
+ git add coverage-badge.svg
233
+ git commit -m 'chore(ci): update coverage badge'
234
+ git pull --rebase --autostash origin main || (git fetch origin main && git rebase origin/main)
235
+ git push
236
+ fi
@@ -0,0 +1,73 @@
1
+ # Generated by Cargo
2
+ # will have compiled files and executables
3
+ debug
4
+ target
5
+
6
+ # These are backup files generated by rustfmt
7
+ **/*.rs.bk
8
+
9
+ # MSVC Windows builds of rustc generate these, which store debugging information
10
+ *.pdb
11
+
12
+ # Generated by cargo mutants
13
+ # Contains mutation testing data
14
+ **/mutants.out*/
15
+
16
+ # RustRover
17
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
20
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
21
+ #.idea/
22
+
23
+ # Python
24
+ __pycache__/
25
+ *.py[cod]
26
+ *$py.class
27
+ *.so
28
+ .Python
29
+ build/
30
+ develop-eggs/
31
+ dist/
32
+ downloads/
33
+ eggs/
34
+ .eggs/
35
+ lib/
36
+ lib64/
37
+ parts/
38
+ sdist/
39
+ var/
40
+ wheels/
41
+ pip-wheel-metadata/
42
+ share/python-wheels/
43
+ *.egg-info/
44
+ .installed.cfg
45
+ *.egg
46
+ MANIFEST
47
+
48
+ # Virtual environments
49
+ venv/
50
+ env/
51
+ ENV/
52
+ .venv
53
+
54
+ # Testing
55
+ .pytest_cache/
56
+ .coverage
57
+ htmlcov/
58
+ .tox/
59
+
60
+ # IDEs
61
+ .vscode/
62
+ .idea/
63
+ *.swp
64
+ *.swo
65
+ *~
66
+
67
+ # OS
68
+ .DS_Store
69
+ Thumbs.db
70
+
71
+ # Build artifacts
72
+ *.pyc
73
+ *.pyo