Skip to content

Commit 35aa288

Browse files
author
Patrick J. McNerthney
committed
Add PyPi build and publish, fix --help
1 parent 3f77e4a commit 35aa288

File tree

9 files changed

+246
-67
lines changed

9 files changed

+246
-67
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: 115 additions & 25 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,46 @@ 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

270-
The `conditions` field is a map of the resource's status conditions array, with
271-
the map key being the condition type.
269+
The `BaseCompsite.conditions`, `Resource.conditions`, and `RequiredResource.conditions` fields
270+
are maps of that entity's status conditions array, with the map key being the condition type.
271+
The fields are read only for `Resource.conditions` and `RequiredResource.conditions`.
272272

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 |
273+
| Field | Type | Description |
274+
| ----- | ---- | ----------- |
275+
| Condition.type | String | The condtion type, or name |
276+
| Condition.status | Boolean | The condition status |
277+
| Condition.reason | String | PascalCase, machine-readable reason for this condition |
278+
| Condition.message | String | Human-readable details about the condition |
279+
| Condition.lastTransitionTime | Timestamp | Last transition time, read only |
280+
| Condition.claim | Boolean | Also apply the condition the claim |
281+
282+
### Events
283+
284+
The `BaseComposite.events` field is a list of events to apply to the Composite and
285+
optionally to the Claim.
286+
287+
| Field | Type | Description |
288+
| ----- | ---- | ----------- |
289+
| Event.info | Boolean | Normal informational event |
290+
| Event.warning | Boolean | Warning level event |
291+
| Event.fatal | Boolean | Fatal events also terminate composing the Composite |
292+
| Event.reason | String | PascalCase, machine-readable reason for this event |
293+
| Event.message | String | Human-readable details about the event |
294+
| Event.claim | Boolean | Also apply the event to the claim |
284295

285296
## Single use Composites
286297

@@ -301,6 +312,85 @@ spec:
301312
self.status.composite = 'Hello, World!'
302313
```
303314

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

306396
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)