lib-shopware6-api-base 6.0.2__tar.gz → 6.0.4__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.
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/.devcontainer/devcontainer.json +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/.devcontainer/settings.json +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/.github/actions/extract-metadata/action.yml +24 -7
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/.github/dependabot.yml +0 -0
- lib_shopware6_api_base-6.0.2/.github/workflows/codeql-analysis.yml → lib_shopware6_api_base-6.0.4/.github/workflows/codeql.yml +1 -1
- lib_shopware6_api_base-6.0.4/.github/workflows/default_cicd_public.yml +436 -0
- lib_shopware6_api_base-6.0.4/.github/workflows/default_release_public.yml +95 -0
- lib_shopware6_api_base-6.0.4/.github/workflows/integration.yml +103 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/.gitignore +8 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/CHANGELOG.md +21 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/CODE_OF_CONDUCT.md +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/CONTRIBUTING.md +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/INSTALL.md +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/ISSUE_TEMPLATE.md +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/LICENSE +0 -0
- lib_shopware6_api_base-6.0.4/Makefile +453 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/PKG-INFO +36 -30
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/PULL_REQUEST_TEMPLATE.md +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/README.md +23 -17
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/example.env +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/notebooks/Quickstart.ipynb +1 -1
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/pyproject.toml +45 -22
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/.gitignore +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/__init__.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/__init__conf__.py +1 -1
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/__main__.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/_compat.py +1 -1
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/_http_common.py +10 -10
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/conf_shopware6_api_base_classes.py +2 -2
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/config.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/defaultconfig.d/10-logging.toml +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/defaultconfig.d/20-shopware.toml +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/defaultconfig.toml +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/exit_codes.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_admin_client.py +9 -7
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_api_base.py +33 -33
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_api_base_cli.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria.py +17 -17
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria_aggregation.py +8 -8
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria_filter.py +11 -11
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria_sorting.py +1 -1
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_api_base_helpers.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/lib_shopware6_storefront_client.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/logging_setup.py +5 -3
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/src/lib_shopware6_api_base/py.typed +0 -0
- lib_shopware6_api_base-6.0.4/src/lib_shopware6_api_base/typed_click.py +57 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/conf_test_docker.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/conftest.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_api_base.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_cli.py +3 -1
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_conf_classes.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_config.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_criteria.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_criteria_aggregation.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_criteria_filter.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_criteria_sorting.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_doctests.py +4 -1
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_helpers.py +0 -0
- {lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/tests/test_logging_config.py +0 -0
- lib_shopware6_api_base-6.0.2/.github/workflows/cicd_docker.yml +0 -385
- lib_shopware6_api_base-6.0.2/.github/workflows/default_release_public.yml +0 -69
- lib_shopware6_api_base-6.0.2/Makefile +0 -260
- lib_shopware6_api_base-6.0.2/src/lib_shopware6_api_base/typed_click.py +0 -34
{lib_shopware6_api_base-6.0.2 → lib_shopware6_api_base-6.0.4}/.devcontainer/devcontainer.json
RENAMED
|
File without changes
|
|
File without changes
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
name: Extract project metadata
|
|
2
2
|
description: >-
|
|
3
3
|
Parse pyproject.toml and expose project metadata as step outputs.
|
|
4
|
-
Requires Python
|
|
5
|
-
|
|
4
|
+
Requires Python to be available in the runner environment.
|
|
5
|
+
Auto-installs rtoml if not already present.
|
|
6
6
|
|
|
7
7
|
outputs:
|
|
8
8
|
PACKAGE_MODULE:
|
|
@@ -29,6 +29,11 @@ outputs:
|
|
|
29
29
|
BANDIT_SKIP_FLAG:
|
|
30
30
|
description: Bandit skip flag string (e.g. "-s B101,B601") or empty
|
|
31
31
|
value: ${{ steps.meta.outputs.BANDIT_SKIP_FLAG }}
|
|
32
|
+
PYTEST_MARKER_EXPR:
|
|
33
|
+
description: >-
|
|
34
|
+
Marker expression for the CI test run (e.g. "not local_only and not integration"),
|
|
35
|
+
built from tool.ci.pytest-exclude-markers
|
|
36
|
+
value: ${{ steps.meta.outputs.PYTEST_MARKER_EXPR }}
|
|
32
37
|
|
|
33
38
|
runs:
|
|
34
39
|
using: composite
|
|
@@ -37,14 +42,15 @@ runs:
|
|
|
37
42
|
id: meta
|
|
38
43
|
shell: python
|
|
39
44
|
run: |
|
|
40
|
-
import os
|
|
45
|
+
import os, subprocess, sys
|
|
41
46
|
try:
|
|
42
|
-
import
|
|
47
|
+
import rtoml
|
|
43
48
|
except ModuleNotFoundError:
|
|
44
|
-
|
|
49
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "rtoml", "-q"])
|
|
50
|
+
import rtoml
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
data = rtoml.load(Path('pyproject.toml'))
|
|
48
54
|
|
|
49
55
|
# --- [project] section ---
|
|
50
56
|
project = data['project']['name']
|
|
@@ -68,6 +74,15 @@ runs:
|
|
|
68
74
|
bandit_skips = data.get('tool', {}).get('bandit', {}).get('skips', [])
|
|
69
75
|
bandit_skip_flag = f"-s {','.join(bandit_skips)}" if bandit_skips else ""
|
|
70
76
|
|
|
77
|
+
# --- [tool.ci] section ---
|
|
78
|
+
# CI always skips local_only (those tests need a dev box). A project whose
|
|
79
|
+
# suite also carries markers CI cannot satisfy - integration tests needing a
|
|
80
|
+
# container that a separate workflow starts - names them here, rather than
|
|
81
|
+
# every such repo forking this workflow to edit one -m expression.
|
|
82
|
+
ci_config = data.get('tool', {}).get('ci', {})
|
|
83
|
+
exclude_markers = ['local_only', *ci_config.get('pytest-exclude-markers', [])]
|
|
84
|
+
pytest_marker_expr = ' and '.join(f'not {m}' for m in exclude_markers)
|
|
85
|
+
|
|
71
86
|
# Write to GITHUB_OUTPUT for composite action outputs
|
|
72
87
|
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as out:
|
|
73
88
|
out.write(f"PROJECT_NAME={project}\n")
|
|
@@ -78,6 +93,7 @@ runs:
|
|
|
78
93
|
out.write(f"COV_REPORT_FILE={cov_report_file}\n")
|
|
79
94
|
out.write(f"COV_FAIL_UNDER={cov_fail_under}\n")
|
|
80
95
|
out.write(f"BANDIT_SKIP_FLAG={bandit_skip_flag}\n")
|
|
96
|
+
out.write(f"PYTEST_MARKER_EXPR={pytest_marker_expr}\n")
|
|
81
97
|
|
|
82
98
|
# Also write to GITHUB_ENV so subsequent steps can use ${{ env.VAR }}
|
|
83
99
|
with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env:
|
|
@@ -89,3 +105,4 @@ runs:
|
|
|
89
105
|
env.write(f"COV_REPORT_FILE={cov_report_file}\n")
|
|
90
106
|
env.write(f"COV_FAIL_UNDER={cov_fail_under}\n")
|
|
91
107
|
env.write(f"BANDIT_SKIP_FLAG={bandit_skip_flag}\n")
|
|
108
|
+
env.write(f"PYTEST_MARKER_EXPR={pytest_marker_expr}\n")
|
|
File without changes
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, master ]
|
|
8
|
+
schedule:
|
|
9
|
+
# Run daily at 3:17 AM UTC (off-peak hours, odd minute to avoid congestion)
|
|
10
|
+
- cron: '17 3 * * *'
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
setup:
|
|
14
|
+
name: Extract Python versions and OS matrix from pyproject.toml
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
outputs:
|
|
17
|
+
python-versions: ${{ steps.metadata.outputs.python-versions }}
|
|
18
|
+
python-latest: ${{ steps.metadata.outputs.python-latest }}
|
|
19
|
+
os-matrix: ${{ steps.metadata.outputs.os-matrix }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
- uses: actions/setup-python@v7
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.x"
|
|
25
|
+
- name: Parse pyproject.toml for Python versions and OS matrix
|
|
26
|
+
id: metadata
|
|
27
|
+
shell: python
|
|
28
|
+
run: |
|
|
29
|
+
import json, os, tomllib
|
|
30
|
+
|
|
31
|
+
with open("pyproject.toml", "rb") as f:
|
|
32
|
+
data = tomllib.load(f)
|
|
33
|
+
|
|
34
|
+
# Python versions from classifiers
|
|
35
|
+
classifiers = data.get("project", {}).get("classifiers", [])
|
|
36
|
+
prefix = "Programming Language :: Python :: "
|
|
37
|
+
versions = [
|
|
38
|
+
c[len(prefix):]
|
|
39
|
+
for c in classifiers
|
|
40
|
+
if c.startswith(prefix) and "." in c[len(prefix):]
|
|
41
|
+
]
|
|
42
|
+
if not versions:
|
|
43
|
+
raise SystemExit("No Python X.Y versions found in classifiers")
|
|
44
|
+
|
|
45
|
+
# OS matrix from [tool.ci]
|
|
46
|
+
ci_config = data.get("tool", {}).get("ci", {})
|
|
47
|
+
os_list = ci_config.get("os", ["ubuntu-latest"])
|
|
48
|
+
|
|
49
|
+
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
|
|
50
|
+
fh.write(f"python-versions={json.dumps(versions)}\n")
|
|
51
|
+
fh.write(f"python-latest={versions[-1]}\n")
|
|
52
|
+
fh.write(f"os-matrix={json.dumps(os_list)}\n")
|
|
53
|
+
|
|
54
|
+
test:
|
|
55
|
+
name: Tests (Python ${{ matrix.python }}, ${{ matrix.os }})
|
|
56
|
+
needs: setup
|
|
57
|
+
runs-on: ${{ matrix.os }}
|
|
58
|
+
strategy:
|
|
59
|
+
fail-fast: false
|
|
60
|
+
matrix:
|
|
61
|
+
os: ${{ fromJSON(needs.setup.outputs.os-matrix) }}
|
|
62
|
+
python: ${{ fromJSON(needs.setup.outputs.python-versions) }}
|
|
63
|
+
env:
|
|
64
|
+
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"
|
|
65
|
+
PYTHONIOENCODING: "utf-8"
|
|
66
|
+
UV_LINK_MODE: "copy"
|
|
67
|
+
UV_BREAK_SYSTEM_PACKAGES: "1"
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v7
|
|
70
|
+
- uses: actions/setup-python@v7
|
|
71
|
+
with:
|
|
72
|
+
python-version: ${{ matrix.python }}
|
|
73
|
+
- name: Install uv
|
|
74
|
+
uses: astral-sh/setup-uv@v7
|
|
75
|
+
with:
|
|
76
|
+
enable-cache: true
|
|
77
|
+
prune-cache: false
|
|
78
|
+
|
|
79
|
+
- name: Install bash 4+ on macOS
|
|
80
|
+
if: runner.os == 'macOS'
|
|
81
|
+
run: |
|
|
82
|
+
brew install bash
|
|
83
|
+
echo "$(brew --prefix)/bin" >> "$GITHUB_PATH"
|
|
84
|
+
|
|
85
|
+
- name: Extract project metadata
|
|
86
|
+
id: metadata
|
|
87
|
+
uses: ./.github/actions/extract-metadata
|
|
88
|
+
|
|
89
|
+
- name: Install CI system dependencies (Ubuntu)
|
|
90
|
+
if: runner.os == 'Linux'
|
|
91
|
+
shell: python
|
|
92
|
+
run: |
|
|
93
|
+
import subprocess
|
|
94
|
+
try:
|
|
95
|
+
import tomllib
|
|
96
|
+
except ModuleNotFoundError:
|
|
97
|
+
import pip._vendor.tomli as tomllib
|
|
98
|
+
with open("pyproject.toml", "rb") as f:
|
|
99
|
+
data = tomllib.load(f)
|
|
100
|
+
deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-ubuntu", [])
|
|
101
|
+
if deps:
|
|
102
|
+
print(f"Installing system packages: {' '.join(deps)}")
|
|
103
|
+
subprocess.check_call(["sudo", "apt-get", "update"])
|
|
104
|
+
subprocess.check_call(["sudo", "apt-get", "install", "-y", *deps])
|
|
105
|
+
else:
|
|
106
|
+
print("No CI system dependencies configured for Ubuntu.")
|
|
107
|
+
|
|
108
|
+
- name: Install CI system dependencies (macOS)
|
|
109
|
+
if: runner.os == 'macOS'
|
|
110
|
+
shell: python
|
|
111
|
+
run: |
|
|
112
|
+
import subprocess
|
|
113
|
+
try:
|
|
114
|
+
import tomllib
|
|
115
|
+
except ModuleNotFoundError:
|
|
116
|
+
import pip._vendor.tomli as tomllib
|
|
117
|
+
with open("pyproject.toml", "rb") as f:
|
|
118
|
+
data = tomllib.load(f)
|
|
119
|
+
deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-macos", [])
|
|
120
|
+
if deps:
|
|
121
|
+
print(f"Installing system packages: {' '.join(deps)}")
|
|
122
|
+
subprocess.check_call(["brew", "install", *deps])
|
|
123
|
+
else:
|
|
124
|
+
print("No CI system dependencies configured for macOS.")
|
|
125
|
+
|
|
126
|
+
- name: Install dev deps
|
|
127
|
+
shell: bash
|
|
128
|
+
run: |
|
|
129
|
+
uv pip install -e .[dev] --system
|
|
130
|
+
|
|
131
|
+
- name: Install CI system dependencies (Windows/choco)
|
|
132
|
+
if: runner.os == 'Windows'
|
|
133
|
+
shell: python
|
|
134
|
+
run: |
|
|
135
|
+
import subprocess
|
|
136
|
+
try:
|
|
137
|
+
import tomllib
|
|
138
|
+
except ModuleNotFoundError:
|
|
139
|
+
import pip._vendor.tomli as tomllib
|
|
140
|
+
with open("pyproject.toml", "rb") as f:
|
|
141
|
+
data = tomllib.load(f)
|
|
142
|
+
deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-windows-choco", [])
|
|
143
|
+
if deps:
|
|
144
|
+
print(f"Installing choco packages: {' '.join(deps)}")
|
|
145
|
+
subprocess.check_call(["choco", "install", "-y", *deps])
|
|
146
|
+
else:
|
|
147
|
+
print("No CI choco dependencies configured for Windows.")
|
|
148
|
+
|
|
149
|
+
- name: Install CI pip dependencies (Windows)
|
|
150
|
+
if: runner.os == 'Windows'
|
|
151
|
+
shell: python
|
|
152
|
+
run: |
|
|
153
|
+
import subprocess
|
|
154
|
+
try:
|
|
155
|
+
import tomllib
|
|
156
|
+
except ModuleNotFoundError:
|
|
157
|
+
import pip._vendor.tomli as tomllib
|
|
158
|
+
with open("pyproject.toml", "rb") as f:
|
|
159
|
+
data = tomllib.load(f)
|
|
160
|
+
deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-windows-pip", [])
|
|
161
|
+
if deps:
|
|
162
|
+
print(f"Installing pip packages: {' '.join(deps)}")
|
|
163
|
+
subprocess.check_call(["uv", "pip", "install", *deps, "--system"])
|
|
164
|
+
else:
|
|
165
|
+
print("No CI pip dependencies configured for Windows.")
|
|
166
|
+
|
|
167
|
+
# Self-hosted runners persist ruff cache via RUFF_CACHE_DIR volume mount,
|
|
168
|
+
# so actions/cache is only needed on GitHub-hosted runners.
|
|
169
|
+
- name: Restore ruff cache
|
|
170
|
+
if: runner.environment == 'github-hosted'
|
|
171
|
+
id: ruff-cache
|
|
172
|
+
uses: actions/cache/restore@v6
|
|
173
|
+
with:
|
|
174
|
+
path: .ruff_cache
|
|
175
|
+
key: ruff-${{ runner.os }}-py${{ matrix.python }}-${{ github.sha }}
|
|
176
|
+
restore-keys: |
|
|
177
|
+
ruff-${{ runner.os }}-py${{ matrix.python }}-
|
|
178
|
+
ruff-${{ runner.os }}-
|
|
179
|
+
|
|
180
|
+
- name: Run ruff (lint + format)
|
|
181
|
+
id: ruff
|
|
182
|
+
continue-on-error: true
|
|
183
|
+
shell: bash
|
|
184
|
+
run: |
|
|
185
|
+
ruff check .
|
|
186
|
+
ruff format --check .
|
|
187
|
+
|
|
188
|
+
- name: Run pyright (type check)
|
|
189
|
+
id: pyright
|
|
190
|
+
continue-on-error: true
|
|
191
|
+
shell: bash
|
|
192
|
+
run: pyright
|
|
193
|
+
|
|
194
|
+
- name: Run pytest (tests + coverage)
|
|
195
|
+
id: pytest
|
|
196
|
+
continue-on-error: true
|
|
197
|
+
shell: bash
|
|
198
|
+
env:
|
|
199
|
+
TEST_VERBOSE: "1"
|
|
200
|
+
# Write coverage SQLite database to local disk — network mounts
|
|
201
|
+
# and stale journal files cause "database is locked" errors.
|
|
202
|
+
COVERAGE_FILE: "${{ runner.temp }}/.coverage"
|
|
203
|
+
run: >-
|
|
204
|
+
pytest -m "${{ env.PYTEST_MARKER_EXPR }}"
|
|
205
|
+
--cov=${{ env.SRC_PATH }}/${{ env.PACKAGE_MODULE }}
|
|
206
|
+
--cov-report=term-missing
|
|
207
|
+
--cov-report=xml:${{ env.COV_REPORT_FILE }}
|
|
208
|
+
--cov-fail-under=${{ env.COV_FAIL_UNDER }}
|
|
209
|
+
${{ env.PYTEST_VERBOSITY }}
|
|
210
|
+
|
|
211
|
+
- name: Upload coverage to Codecov
|
|
212
|
+
if: >-
|
|
213
|
+
always()
|
|
214
|
+
&& steps.pytest.outcome == 'success'
|
|
215
|
+
&& matrix.os == 'ubuntu-latest'
|
|
216
|
+
&& matrix.python == needs.setup.outputs.python-latest
|
|
217
|
+
uses: codecov/codecov-action@v7
|
|
218
|
+
with:
|
|
219
|
+
files: ${{ env.COV_REPORT_FILE }}
|
|
220
|
+
fail_ci_if_error: false
|
|
221
|
+
|
|
222
|
+
- name: Run import-linter (architecture)
|
|
223
|
+
id: importlinter
|
|
224
|
+
continue-on-error: true
|
|
225
|
+
shell: bash
|
|
226
|
+
run: lint-imports
|
|
227
|
+
|
|
228
|
+
- name: Run bandit (security scan)
|
|
229
|
+
id: bandit
|
|
230
|
+
continue-on-error: true
|
|
231
|
+
shell: bash
|
|
232
|
+
run: bandit -q -c pyproject.toml -r ${{ env.SRC_PATH }}/${{ env.PACKAGE_MODULE }} ${{ env.BANDIT_SKIP_FLAG }}
|
|
233
|
+
|
|
234
|
+
- name: Run pip-audit (vulnerability scan)
|
|
235
|
+
id: pipaudit
|
|
236
|
+
continue-on-error: true
|
|
237
|
+
shell: python
|
|
238
|
+
run: |
|
|
239
|
+
import subprocess, sys, tempfile
|
|
240
|
+
import rtoml
|
|
241
|
+
from pathlib import Path
|
|
242
|
+
|
|
243
|
+
# Audit the project's OWN resolved dependency tree, not the ambient
|
|
244
|
+
# environment. "Install dev deps" uses --system, so the interpreter
|
|
245
|
+
# being audited is the runner image's, and that image ships packages
|
|
246
|
+
# this project neither declares nor distributes (setuptools, pip,
|
|
247
|
+
# wheel). Auditing those reported CVEs no consumer of this project is
|
|
248
|
+
# exposed to, and a scan that is permanently yellow is a scan everyone
|
|
249
|
+
# learns to ignore. Resolving pyproject.toml here keeps the result
|
|
250
|
+
# identical on every OS/python and dependent only on what we declare.
|
|
251
|
+
data = rtoml.load(Path("pyproject.toml"))
|
|
252
|
+
|
|
253
|
+
# Not every project declares dev dependencies, and those that do may use
|
|
254
|
+
# either an extra or a PEP 735 group. Asking uv for one that does not
|
|
255
|
+
# exist is a hard error ("Requested extra not found"), which would leave
|
|
256
|
+
# the audit permanently broken here rather than merely noisy, so select
|
|
257
|
+
# the flag from what this project actually declares.
|
|
258
|
+
selector = []
|
|
259
|
+
if "dev" in data.get("project", {}).get("optional-dependencies", {}):
|
|
260
|
+
selector = ["--extra", "dev"]
|
|
261
|
+
elif "dev" in data.get("dependency-groups", {}):
|
|
262
|
+
selector = ["--group", "dev"]
|
|
263
|
+
|
|
264
|
+
tmp = Path(tempfile.mkdtemp())
|
|
265
|
+
req = tmp / "compiled-requirements.txt"
|
|
266
|
+
subprocess.check_call(
|
|
267
|
+
["uv", "pip", "compile", *selector, "pyproject.toml", "-o", str(req), "--quiet"]
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Drop direct URL references ("pkg @ git+https://...") before auditing.
|
|
271
|
+
# pip-audit cannot audit those (they are not on PyPI) but, given a
|
|
272
|
+
# requirements file, first tries to `git clone` each one to resolve a
|
|
273
|
+
# version - a slow, credential-dependent fetch. No public project here
|
|
274
|
+
# uses one today, yet "allow-direct-references" is enabled, so this
|
|
275
|
+
# guards the day one appears and keeps this step identical to the
|
|
276
|
+
# default_cicd_private template's.
|
|
277
|
+
audit_req = tmp / "audit-requirements.txt"
|
|
278
|
+
kept, dropped = [], []
|
|
279
|
+
for line in req.read_text(encoding="utf-8").splitlines():
|
|
280
|
+
if " @ " in line and not line.lstrip().startswith("#"):
|
|
281
|
+
dropped.append(line.split(" @ ", 1)[0].strip())
|
|
282
|
+
else:
|
|
283
|
+
kept.append(line)
|
|
284
|
+
audit_req.write_text("\n".join(kept) + "\n", encoding="utf-8")
|
|
285
|
+
if dropped:
|
|
286
|
+
# Never drop coverage silently: name what is not being audited here.
|
|
287
|
+
print(f"::notice::pip-audit: not auditing {len(dropped)} direct-URL dependency/ies "
|
|
288
|
+
f"(audited in their own repo): {', '.join(sorted(dropped))}")
|
|
289
|
+
|
|
290
|
+
req = audit_req
|
|
291
|
+
vulns = data.get("tool", {}).get("pip-audit", {}).get("ignore-vulns", [])
|
|
292
|
+
# --no-deps: the compiled file is already a complete pinned resolution,
|
|
293
|
+
# so pip-audit must audit it verbatim rather than re-resolving it.
|
|
294
|
+
cmd = ["pip-audit", "--no-deps", "-r", str(req)]
|
|
295
|
+
for v in vulns:
|
|
296
|
+
cmd.extend(["--ignore-vuln", v])
|
|
297
|
+
sys.exit(subprocess.call(cmd))
|
|
298
|
+
|
|
299
|
+
- name: Check all steps passed
|
|
300
|
+
shell: bash
|
|
301
|
+
run: |
|
|
302
|
+
if [[ "${{ steps.ruff.outcome }}" != "success" ]]; then
|
|
303
|
+
echo "::error::Ruff check failed"
|
|
304
|
+
exit 1
|
|
305
|
+
fi
|
|
306
|
+
if [[ "${{ steps.pyright.outcome }}" != "success" ]]; then
|
|
307
|
+
echo "::error::Pyright check failed"
|
|
308
|
+
exit 1
|
|
309
|
+
fi
|
|
310
|
+
if [[ "${{ steps.pytest.outcome }}" != "success" ]]; then
|
|
311
|
+
echo "::error::Pytest failed"
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
if [[ "${{ steps.importlinter.outcome }}" != "success" ]]; then
|
|
315
|
+
echo "::error::Import-linter check failed"
|
|
316
|
+
exit 1
|
|
317
|
+
fi
|
|
318
|
+
if [[ "${{ steps.bandit.outcome }}" != "success" ]]; then
|
|
319
|
+
echo "::error::Bandit security scan failed"
|
|
320
|
+
exit 1
|
|
321
|
+
fi
|
|
322
|
+
if [[ "${{ steps.pipaudit.outcome }}" != "success" ]]; then
|
|
323
|
+
echo "::warning::pip-audit vulnerability scan failed"
|
|
324
|
+
fi
|
|
325
|
+
echo "All checks passed!"
|
|
326
|
+
|
|
327
|
+
- name: Build wheel/sdist
|
|
328
|
+
shell: bash
|
|
329
|
+
run: uv build
|
|
330
|
+
|
|
331
|
+
- name: Verify wheel install in clean env
|
|
332
|
+
shell: bash
|
|
333
|
+
run: |
|
|
334
|
+
uv venv .venv_wheel
|
|
335
|
+
. .venv_wheel/bin/activate 2>/dev/null || . .venv_wheel/Scripts/activate 2>/dev/null
|
|
336
|
+
uv pip install dist/*.whl
|
|
337
|
+
"$CLI_BIN" --version 2>/dev/null || python -m "$PACKAGE_MODULE" --version
|
|
338
|
+
|
|
339
|
+
- name: Prune uv cache
|
|
340
|
+
continue-on-error: true
|
|
341
|
+
shell: bash
|
|
342
|
+
run: uv cache prune --ci
|
|
343
|
+
|
|
344
|
+
- name: Save ruff cache
|
|
345
|
+
if: always() && runner.environment == 'github-hosted' && steps.ruff-cache.outputs.cache-hit != 'true'
|
|
346
|
+
uses: actions/cache/save@v6
|
|
347
|
+
with:
|
|
348
|
+
path: .ruff_cache
|
|
349
|
+
key: ruff-${{ runner.os }}-py${{ matrix.python }}-${{ github.sha }}
|
|
350
|
+
|
|
351
|
+
pipx-uv:
|
|
352
|
+
name: pipx/uv verification (ubuntu)
|
|
353
|
+
runs-on: ubuntu-latest
|
|
354
|
+
steps:
|
|
355
|
+
- uses: actions/checkout@v7
|
|
356
|
+
- uses: actions/setup-python@v7
|
|
357
|
+
with:
|
|
358
|
+
python-version: "3.14"
|
|
359
|
+
|
|
360
|
+
- name: Install uv
|
|
361
|
+
uses: astral-sh/setup-uv@v7
|
|
362
|
+
with:
|
|
363
|
+
enable-cache: true
|
|
364
|
+
prune-cache: false
|
|
365
|
+
|
|
366
|
+
- name: Extract project metadata
|
|
367
|
+
id: metadata
|
|
368
|
+
uses: ./.github/actions/extract-metadata
|
|
369
|
+
|
|
370
|
+
- name: Build wheel
|
|
371
|
+
run: uv build
|
|
372
|
+
- name: pipx install from wheel
|
|
373
|
+
run: |
|
|
374
|
+
uv pip install --system pipx
|
|
375
|
+
pipx ensurepath
|
|
376
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
377
|
+
pipx install dist/*.whl
|
|
378
|
+
"$CLI_BIN" --version
|
|
379
|
+
|
|
380
|
+
- name: uv tool install
|
|
381
|
+
run: |
|
|
382
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
383
|
+
uv tool install --force .
|
|
384
|
+
"$CLI_BIN" --version
|
|
385
|
+
|
|
386
|
+
- name: Prune uv cache
|
|
387
|
+
continue-on-error: true
|
|
388
|
+
run: uv cache prune --ci
|
|
389
|
+
|
|
390
|
+
notebooks:
|
|
391
|
+
name: Execute notebooks (ubuntu, Python 3.14)
|
|
392
|
+
runs-on: ubuntu-latest
|
|
393
|
+
steps:
|
|
394
|
+
- uses: actions/checkout@v7
|
|
395
|
+
- uses: actions/setup-python@v7
|
|
396
|
+
with:
|
|
397
|
+
python-version: "3.14"
|
|
398
|
+
|
|
399
|
+
- name: Install uv
|
|
400
|
+
uses: astral-sh/setup-uv@v7
|
|
401
|
+
with:
|
|
402
|
+
enable-cache: true
|
|
403
|
+
prune-cache: false
|
|
404
|
+
|
|
405
|
+
- name: Install notebook runner deps
|
|
406
|
+
run: |
|
|
407
|
+
uv pip install --system nbclient nbformat ipykernel jupyter_client
|
|
408
|
+
python -m ipykernel install --user --name python3 --display-name "Python 3"
|
|
409
|
+
- name: Execute Quickstart notebook
|
|
410
|
+
env:
|
|
411
|
+
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
|
412
|
+
PIP_NO_INPUT: "1"
|
|
413
|
+
run: |
|
|
414
|
+
python - << 'PY'
|
|
415
|
+
from pathlib import Path
|
|
416
|
+
|
|
417
|
+
import nbformat
|
|
418
|
+
from nbclient import NotebookClient
|
|
419
|
+
|
|
420
|
+
nb_path = Path('notebooks/Quickstart.ipynb')
|
|
421
|
+
if not nb_path.exists():
|
|
422
|
+
raise SystemExit(f"Notebook not found: {nb_path}")
|
|
423
|
+
|
|
424
|
+
notebook = nbformat.read(nb_path, as_version=4)
|
|
425
|
+
client = NotebookClient(
|
|
426
|
+
notebook,
|
|
427
|
+
timeout=900,
|
|
428
|
+
kernel_name='python3',
|
|
429
|
+
allow_errors=False,
|
|
430
|
+
)
|
|
431
|
+
client.execute()
|
|
432
|
+
|
|
433
|
+
out_path = Path('notebooks/Quickstart-executed.ipynb')
|
|
434
|
+
nbformat.write(notebook, out_path)
|
|
435
|
+
print(f"Executed notebook written to: {out_path}")
|
|
436
|
+
PY
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# The tag push is the only release event bmk guarantees: it runs
|
|
4
|
+
# `git push <remote> <tag>` unconditionally, whereas `gh release create` sits
|
|
5
|
+
# behind a gh-availability check and runs with check=False, so a missing or
|
|
6
|
+
# failing gh would leave nothing to publish. Listening for `release: published`
|
|
7
|
+
# as well fired a second, redundant run that raced the first to the upload and
|
|
8
|
+
# only ever logged "Skipping ... already exists".
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
tags:
|
|
12
|
+
- 'v*'
|
|
13
|
+
workflow_dispatch: {}
|
|
14
|
+
|
|
15
|
+
# One publish at a time per tag, so a workflow_dispatch re-run cannot overlap
|
|
16
|
+
# the run the tag push started.
|
|
17
|
+
concurrency:
|
|
18
|
+
group: release-${{ github.ref_name }}
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
build:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
env:
|
|
27
|
+
UV_LINK_MODE: "copy"
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v7
|
|
30
|
+
- uses: actions/setup-python@v7
|
|
31
|
+
with:
|
|
32
|
+
python-version: '3.14'
|
|
33
|
+
|
|
34
|
+
- name: Install uv
|
|
35
|
+
uses: astral-sh/setup-uv@v7
|
|
36
|
+
with:
|
|
37
|
+
enable-cache: true
|
|
38
|
+
prune-cache: false
|
|
39
|
+
|
|
40
|
+
- name: Build artifacts
|
|
41
|
+
run: uv build
|
|
42
|
+
|
|
43
|
+
- name: Upload artifacts
|
|
44
|
+
uses: actions/upload-artifact@v7
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/*
|
|
48
|
+
|
|
49
|
+
- name: Prune uv cache
|
|
50
|
+
continue-on-error: true
|
|
51
|
+
run: uv cache prune --ci
|
|
52
|
+
|
|
53
|
+
publish:
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
environment: pypi
|
|
57
|
+
permissions:
|
|
58
|
+
contents: read
|
|
59
|
+
# id-token is required for OIDC / PyPI Trusted Publisher. It is harmless when
|
|
60
|
+
# a PYPI_API_TOKEN secret is configured, in which case the token path runs.
|
|
61
|
+
id-token: write
|
|
62
|
+
steps:
|
|
63
|
+
- name: Select PyPI auth method
|
|
64
|
+
id: auth
|
|
65
|
+
env:
|
|
66
|
+
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
67
|
+
run: |
|
|
68
|
+
if [ -n "$PYPI_API_TOKEN" ]; then
|
|
69
|
+
echo "PYPI_API_TOKEN is set; publishing with the API token."
|
|
70
|
+
echo "method=token" >> "$GITHUB_OUTPUT"
|
|
71
|
+
else
|
|
72
|
+
echo "No PYPI_API_TOKEN; publishing via OIDC Trusted Publisher."
|
|
73
|
+
echo "method=oidc" >> "$GITHUB_OUTPUT"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
- name: Download artifacts
|
|
77
|
+
uses: actions/download-artifact@v8
|
|
78
|
+
with:
|
|
79
|
+
name: dist
|
|
80
|
+
path: dist/
|
|
81
|
+
|
|
82
|
+
- name: Publish to PyPI (API token)
|
|
83
|
+
if: steps.auth.outputs.method == 'token'
|
|
84
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
85
|
+
with:
|
|
86
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
87
|
+
attestations: false
|
|
88
|
+
skip-existing: true
|
|
89
|
+
|
|
90
|
+
- name: Publish to PyPI (Trusted Publisher / OIDC)
|
|
91
|
+
if: steps.auth.outputs.method == 'oidc'
|
|
92
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
93
|
+
with:
|
|
94
|
+
attestations: true
|
|
95
|
+
skip-existing: true
|