Skip to content

Commit 990b2db

Browse files
author
Patrick J. McNerthney
committed
Add PyPi build and publish
1 parent 3f77e4a commit 990b2db

File tree

8 files changed

+233
-57
lines changed

8 files changed

+233
-57
lines changed

.github/workflows/ci.yaml

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ on:
1414

1515
env:
1616
# Common versions
17-
PYTHON_VERSION: '3.12.11' # TODO: Used?
17+
PYTHON_VERSION: '3.13.7' # TODO: Used?
18+
HATCH_VERSION: '1.12.0'
1819
DOCKER_BUILDX_VERSION: 'v0.26.1'
1920

2021
# These environment variables are important to the Crossplane CLI install.sh
@@ -32,6 +33,9 @@ env:
3233
# XPKG: xpkg.upbound.io/${{ github.repository}}
3334
CROSSPLANE_REGORG: ghcr.io/${{ github.repository}}
3435

36+
# The PyPi project version to push. The default is v0.0.0+gitdate-gitsha.
37+
PYPI_VERSION: ${{ inputs.version }}
38+
3539
# The package version to push. The default is 0.0.0-gitsha.
3640
XPKG_VERSION: ${{ inputs.version }}
3741

@@ -86,11 +90,49 @@ jobs:
8690
#remove-link-from-badge: false
8791
#unique-id-for-comment: python3.8
8892

93+
build-pypi:
94+
runs-on: ubuntu-24.04
95+
steps:
96+
- name: Checkout
97+
uses: actions/checkout@v4
98+
99+
- name: Setup Python
100+
uses: actions/setup-python@v5
101+
with:
102+
python-version: ${{ env.PYTHON_VERSION }}
103+
104+
- name: Setup Hatch
105+
run: pipx install hatch==${{ env.HATCH_VERSION }}
106+
107+
# If a version wasn't explicitly passed as a workflow_dispatch input we
108+
# default to version v0.0.0+<git-commit-date>-<git-short-sha>, for example
109+
# v0.0.0+20231101115142-1091066df799. This is a simple implementation of
110+
# Go's pseudo-versions: https://go.dev/ref/mod#pseudo-versions.
111+
- name: Set Default PyPI Project Version
112+
if: env.PYPI_VERSION == ''
113+
run: echo "PYPI_VERSION=v0.0.0+$(date -d@$(git show -s --format=%ct) +%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)" >> $GITHUB_ENV
114+
115+
- name: Set PyPI Project Version
116+
run: hatch version ${{ env.PYPI_VERSION }}
117+
118+
- name: Build Sdist and Wheel
119+
run: hatch build
120+
121+
- name: Upload Sdist and Wheel to GitHub
122+
uses: actions/upload-artifact@v4
123+
with:
124+
name: dist
125+
path: "dist/*"
126+
if-no-files-found: error
127+
retention-days: 1
128+
89129
# We want to build most packages for the amd64 and arm64 architectures. To
90130
# speed this up we build single-platform packages in parallel. We then upload
91131
# those packages to GitHub as a build artifact. The push job downloads those
92132
# artifacts and pushes them as a single multi-platform package.
93-
build:
133+
build-xpkg:
134+
needs:
135+
- build-pypi
94136
runs-on: ubuntu-24.04
95137
strategy:
96138
fail-fast: true
@@ -113,6 +155,13 @@ jobs:
113155
- name: Checkout
114156
uses: actions/checkout@v4
115157

158+
- name: Download Wheel from GitHub
159+
uses: actions/download-artifact@v5
160+
with:
161+
name: dist
162+
pattern: '*.whl'
163+
path: dist
164+
116165
# We ask Docker to use GitHub Action's native caching support to speed up
117166
# the build, per https://docs.docker.com/build/cache/backends/gha/.
118167
- name: Build Runtime
@@ -142,16 +191,38 @@ jobs:
142191
if-no-files-found: error
143192
retention-days: 1
144193

194+
publish-pypi:
195+
# Don't publish unless we were run with an explicit version.
196+
if: ${{ inputs.version != '' }}
197+
needs:
198+
- build-xpkg # only publish if xpkg builds succeeds
199+
runs-on: ubuntu-24.04
200+
steps:
201+
- name: Download Sdist and Wheel from GitHub
202+
uses: actions/download-artifact@v5
203+
with:
204+
name: dist
205+
path: dist
206+
207+
- name: Setup Hatch
208+
run: pipx install hatch==${{ env.HATCH_VERSION }}
209+
210+
- name: Publish to PyPI
211+
env:
212+
HATCH_INDEX_USER: __token__
213+
HATCH_INDEX_AUTH: ${{ secrets.PYPI_API_TOKEN }}
214+
run: hatch publish --no-prompt
215+
145216
# This job downloads the single-platform packages built by the build job, and
146217
# pushes them as a multi-platform package. We only push the package it the
147218
# XPKG_ACCESS_ID and XPKG_TOKEN secrets were provided.
148-
push:
219+
push-xpkg:
220+
needs:
221+
- build-xpkg
149222
runs-on: ubuntu-24.04
150223
permissions:
151224
contents: read
152225
packages: write
153-
needs:
154-
- build
155226
steps:
156227
- name: Checkout
157228
uses: actions/checkout@v4

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,6 @@ marimo/_lsp/
211211
__marimo__/
212212

213213
# function-pythonic
214+
crossplane/pythonic/__version__.py
215+
pocs/
214216
pythonic-packages/

Dockerfile

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
FROM python:3.13-slim-trixie AS image
22

3-
WORKDIR /root/pythonic
4-
COPY pyproject.toml /root/pythonic
5-
COPY crossplane /root/pythonic/crossplane
3+
COPY dist/*.whl /root
64
WORKDIR /
75
RUN \
86
set -eux && \
9-
cd /root/pythonic && \
10-
pip install --root-user-action ignore --no-build-isolation setuptools==80.9.0 && \
11-
pip install --root-user-action ignore --no-build-isolation . && \
12-
pip uninstall --root-user-action ignore --yes setuptools && \
13-
cd .. && \
14-
rm -rf .cache pythonic && \
7+
pip install --root-user-action ignore --no-build-isolation /root/*.whl && \
8+
rm -rf /root/*.whl /root/.cache && \
159
groupadd --gid 2000 pythonic && \
1610
useradd --uid 2000 --gid pythonic --home-dir /opt/pythonic --create-home --shell /usr/sbin/nologin pythonic
1711

README.md

Lines changed: 112 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ metadata:
5959
spec:
6060
package: ghcr.io/fortra/function-pythonic:v0.0.7
6161
```
62-
6362
## Composed Resource Dependencies
6463
6564
function-pythonic automatically handles dependencies between composed resources.
@@ -253,34 +252,45 @@ selection. For now, use matchLabels and filter the results if required.
253252
RequiredResources acts like a Python list to provide access to the found required resources.
254253
Each resource in the list is the following RequiredResource class:
255254

256-
| Field | Description |
257-
| ----- | ----------- |
258-
| RequiredResource.name | The required resource name |
259-
| RequiredResource.observed | Low level direct access to the observed required resource |
260-
| RequiredResource.apiVersion | The required resource apiVersion |
261-
| RequiredResource.kind | The required resource kind |
262-
| RequiredResource.metadata | The required resource metadata |
263-
| RequiredResource.spec | The required resource spec |
264-
| RequiredResource.data | The required resource data |
265-
| RequiredResource.status | The required resource status |
266-
| RequiredResource.conditions | The required resource conditions |
255+
| Field | Type | Description |
256+
| ----- | ---- | ----------- |
257+
| RequiredResource.name | String | The required resource name |
258+
| RequiredResource.observed | Map | Low level direct access to the observed required resource |
259+
| RequiredResource.apiVersion | String | The required resource apiVersion |
260+
| RequiredResource.kind | String | The required resource kind |
261+
| RequiredResource.metadata | Map | The required resource metadata |
262+
| RequiredResource.spec | Map | The required resource spec |
263+
| RequiredResource.data | Map | The required resource data |
264+
| RequiredResource.status | Map | The required resource status |
265+
| RequiredResource.conditions | Map | The required resource conditions |
267266

268267
### Conditions
269268

270269
The `conditions` field is a map of the resource's status conditions array, with
271270
the map key being the condition type.
272271

273-
| Field | Description |
274-
| ----- | ----------- |
275-
| Condition.type | The condtion type |
276-
| Condition.status | RequiredResource.observed | Low level direct access to the observed required resource |
277-
| RequiredResource.apiVersion | The required resource apiVersion |
278-
| RequiredResource.kind | The required resource kind |
279-
| RequiredResource.metadata | The required resource metadata |
280-
| RequiredResource.spec | The required resource spec |
281-
| RequiredResource.data | The required resource data |
282-
| RequiredResource.status | The required resource status |
283-
| RequiredResource.conditions | The required resource conditions |
272+
| Field | Type | Description |
273+
| ----- | ---- | ----------- |
274+
| Condition.type | String | The condtion type, or name |
275+
| Condition.status | Boolean | The condition status |
276+
| Condition.reason | String | PascalCase, machine-readable reason for this condition |
277+
| Condition.message | String | Human-readable details about the condition |
278+
| Condition.lastTransitionTime | Timestamp | Last transition time, read only |
279+
| Condition.claim | Boolean | Also apply the condition the claim |
280+
281+
### Events
282+
283+
The `events` field is a list of events to apply to the Composite and
284+
optionally to the Claim.
285+
286+
| Field | Type | Description |
287+
| ----- | ---- | ----------- |
288+
| Event.info | Boolean | Normal informational event |
289+
| Event.warning | Boolean | Warning level event |
290+
| Event.fatal | Boolean | Fatal events also terminate composing the Composite |
291+
| Event.reason | String | PascalCase, machine-readable reason for this event |
292+
| Event.message | String | Human-readable details about the event |
293+
| Event.claim | Boolean | Also apply the event to the claim |
284294

285295
## Single use Composites
286296

@@ -301,6 +311,85 @@ spec:
301311
self.status.composite = 'Hello, World!'
302312
```
303313

314+
## Quick Start Development
315+
316+
The following example demonstrates how to locally render function-python
317+
compositions. First, install the `crossplane-function-pythonic` python
318+
package into the python environment:
319+
```shell
320+
$ pip install crossplane-function-pythonic
321+
```
322+
Then create the following files:
323+
#### xr.yaml
324+
```yaml
325+
apiVersion: pythonic.fortra.com/v1alpha1
326+
kind: Hello
327+
metadata:
328+
name: world
329+
spec:
330+
who: World
331+
```
332+
#### composition.yaml
333+
```yaml
334+
apiVersion: apiextensions.crossplane.io/v1
335+
kind: Composition
336+
metadata:
337+
name: hellos.pythonic.fortra.com
338+
spec:
339+
compositeTypeRef:
340+
apiVersion: pythonic.fortra.com/v1alpha1
341+
kind: Hello
342+
mode: Pipeline
343+
pipeline:
344+
- step: pythonic
345+
functionRef:
346+
name: function-pythonic
347+
input:
348+
apiVersion: pythonic.fn.fortra.com/v1alpha1
349+
kind: Composite
350+
composite: |
351+
class Composite(BaseComposite):
352+
def compose(self):
353+
self.status.greeting = f"Hello, {self.spec.who}!"
354+
```
355+
#### functions.yaml
356+
```yaml
357+
apiVersion: pkg.crossplane.io/v1beta1
358+
kind: Function
359+
metadata:
360+
name: function-pythonic
361+
annotations:
362+
render.crossplane.io/runtime: Development
363+
spec:
364+
package: ghcr.io/fortra/function-pythonic:v0.0.7
365+
```
366+
In one terminal session, run function-pythonic:
367+
```shell
368+
$ function-pythonic --insecure --debug
369+
[2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine
370+
```
371+
In other terminal session, render the Composite:
372+
```shell
373+
$ crossplane render xr.yaml composition.yaml functions.yaml
374+
---
375+
apiVersion: pythonic.fortra.com/v1alpha1
376+
kind: Hello
377+
metadata:
378+
name: world
379+
status:
380+
conditions:
381+
- lastTransitionTime: "2024-01-01T00:00:00Z"
382+
reason: Available
383+
status: "True"
384+
type: Ready
385+
- lastTransitionTime: "2024-01-01T00:00:00Z"
386+
message: All resources are composed
387+
reason: AllComposed
388+
status: "True"
389+
type: ResourcesComposed
390+
greeting: Hello, World!
391+
```
392+
304393
## ConfigMap Packages
305394

306395
ConfigMap based python packages are enable using the `--packages` and

crossplane/pythonic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
import base64
23

34
from .composite import BaseComposite

crossplane/pythonic/__version__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This is set at build time, using "hatch version"
2+
__version__ = "0.0.0"

crossplane/pythonic/function.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -220,30 +220,30 @@ async def run_function(self, request):
220220

221221
def trimFullName(self, name):
222222
name = name.split('.')
223-
ix = 0
224223
for values in (
225-
('request', 'response'),
226-
('observed', 'desired'),
227-
('resources', 'extra_resources'),
228-
None,
229-
('resource', 'items'),
224+
('request', 'observed', 'resources', None, 'resource'),
225+
('request', 'extra_resources', None, 'items', 'resource'),
226+
('response', 'desired', 'resources', None, 'resource'),
230227
):
231-
if values:
232-
if ix < len(name):
228+
if len(values) <= len(name):
229+
for ix, value in enumerate(values):
230+
if value and value != name[ix] and not name[ix].startswith(f"{value}["):
231+
break
232+
else:
233+
ix = 0
233234
for value in values:
234-
if name[ix] == value:
235-
del name[ix]
236-
break
237-
if name[ix].startswith(f"{value}["):
238-
if ix:
235+
if value:
236+
if value == name[ix]:
237+
del name[ix]
238+
elif ix:
239239
name[ix-1] += name[ix][len(value):]
240240
del name[ix]
241241
else:
242242
name[ix] = name[ix][len(value):]
243243
ix += 1
244-
break
245-
else:
246-
ix += 1
244+
else:
245+
ix += 1
246+
break
247247
return '.'.join(name)
248248

249249

0 commit comments

Comments
 (0)