crossplane-function-sdk-python 0.13.0__tar.gz → 0.15.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 (46) hide show
  1. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/.github/workflows/ci.yml +9 -9
  2. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/PKG-INFO +5 -4
  3. crossplane_function_sdk_python-0.15.0/crossplane/function/__init__.py +65 -0
  4. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/__version__.py +1 -1
  5. crossplane_function_sdk_python-0.15.0/crossplane/function/cli.py +127 -0
  6. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/response.py +6 -3
  7. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/runtime.py +9 -6
  8. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/pyproject.toml +3 -2
  9. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/test_response.py +15 -11
  10. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/.gitignore +0 -0
  14. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/CODEOWNERS +0 -0
  15. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/LICENSE +0 -0
  16. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/OWNERS.md +0 -0
  17. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/README.md +0 -0
  18. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/logging.py +0 -0
  19. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1/run_function.proto +0 -0
  20. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1/run_function_pb2.py +0 -0
  21. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1/run_function_pb2.pyi +0 -0
  22. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1/run_function_pb2_grpc.py +0 -0
  23. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1beta1/run_function.proto +0 -0
  24. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1beta1/run_function_pb2.py +0 -0
  25. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1beta1/run_function_pb2.pyi +0 -0
  26. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/proto/v1beta1/run_function_pb2_grpc.py +0 -0
  27. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/py.typed +0 -0
  28. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/request.py +0 -0
  29. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/crossplane/function/resource.py +0 -0
  30. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/renovate.json +0 -0
  31. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/test_request.py +0 -0
  32. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/test_resource.py +0 -0
  33. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/test_runtime.py +0 -0
  34. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/k8s/api/__init__.py +0 -0
  35. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/k8s/api/resource/__init__.py +0 -0
  36. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/k8s/api/resource/v1.py +0 -0
  37. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/__init__.py +0 -0
  38. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/__init__.py +0 -0
  39. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/v1.py +0 -0
  40. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/upbound/aws/s3/__init__.py +0 -0
  41. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/upbound/aws/s3/v1beta2.py +0 -0
  42. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/upbound/m/__init__.py +0 -0
  43. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/upbound/m/aws/__init__.py +0 -0
  44. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/upbound/m/aws/iam/__init__.py +0 -0
  45. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/upbound/m/aws/iam/accountalias/__init__.py +0 -0
  46. {crossplane_function_sdk_python-0.13.0 → crossplane_function_sdk_python-0.15.0}/tests/testdata/models/io/upbound/m/aws/iam/accountalias/v1beta1.py +0 -0
@@ -38,10 +38,10 @@ jobs:
38
38
  runs-on: ubuntu-24.04
39
39
  steps:
40
40
  - name: Checkout
41
- uses: actions/checkout@v6
41
+ uses: actions/checkout@v7
42
42
 
43
43
  - name: Setup Python
44
- uses: actions/setup-python@v6
44
+ uses: actions/setup-python@v7
45
45
  with:
46
46
  python-version: ${{ env.PYTHON_VERSION }}
47
47
 
@@ -55,10 +55,10 @@ jobs:
55
55
  runs-on: ubuntu-24.04
56
56
  steps:
57
57
  - name: Checkout
58
- uses: actions/checkout@v6
58
+ uses: actions/checkout@v7
59
59
 
60
60
  - name: Setup Python
61
- uses: actions/setup-python@v6
61
+ uses: actions/setup-python@v7
62
62
  with:
63
63
  python-version: ${{ env.PYTHON_VERSION }}
64
64
 
@@ -73,10 +73,10 @@ jobs:
73
73
  runs-on: ubuntu-24.04
74
74
  steps:
75
75
  - name: Checkout
76
- uses: actions/checkout@v6
76
+ uses: actions/checkout@v7
77
77
 
78
78
  - name: Setup Python
79
- uses: actions/setup-python@v6
79
+ uses: actions/setup-python@v7
80
80
  with:
81
81
  python-version: ${{ env.PYTHON_VERSION }}
82
82
 
@@ -120,7 +120,7 @@ jobs:
120
120
  path: "dist"
121
121
 
122
122
  - name: Publish to PyPI
123
- uses: pypa/gh-action-pypi-publish@v1.14.0
123
+ uses: pypa/gh-action-pypi-publish@v1.14.2
124
124
  with:
125
125
  # Note that this is currently being pushed to the 'crossplane' PyPI
126
126
  # user (not org). See @negz if you need access - PyPI requires 2FA to
@@ -136,10 +136,10 @@ jobs:
136
136
  runs-on: ubuntu-24.04
137
137
  steps:
138
138
  - name: Checkout
139
- uses: actions/checkout@v6
139
+ uses: actions/checkout@v7
140
140
 
141
141
  - name: Setup Python
142
- uses: actions/setup-python@v6
142
+ uses: actions/setup-python@v7
143
143
  with:
144
144
  python-version: ${{ env.PYTHON_VERSION }}
145
145
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: crossplane-function-sdk-python
3
- Version: 0.13.0
3
+ Version: 0.15.0
4
4
  Summary: The Python SDK for Crossplane composition functions
5
5
  Project-URL: Documentation, https://github.com/crossplane/function-sdk-python#readme
6
6
  Project-URL: Issues, https://github.com/crossplane/function-sdk-python/issues
@@ -13,9 +13,10 @@ Classifier: Programming Language :: Python
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Typing :: Typed
15
15
  Requires-Python: >=3.11
16
+ Requires-Dist: click==8.*
16
17
  Requires-Dist: grpcio-reflection==1.*
17
- Requires-Dist: grpcio==1.81.0
18
- Requires-Dist: protobuf==7.35.0
18
+ Requires-Dist: grpcio==1.84.0
19
+ Requires-Dist: protobuf==7.36.1
19
20
  Requires-Dist: pydantic==2.*
20
21
  Requires-Dist: structlog==26.*
21
22
  Description-Content-Type: text/markdown
@@ -0,0 +1,65 @@
1
+ # Copyright 2025 The Crossplane Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Python SDK for writing Crossplane composition functions.
16
+
17
+ This SDK provides the building blocks for implementing [Crossplane composition
18
+ functions](https://docs.crossplane.io/latest/concepts/composition-functions) in
19
+ Python. Composition functions are serverless components that extend Crossplane's
20
+ composition capability, allowing you to programmatically control how infrastructure
21
+ is composed and managed.
22
+
23
+ ## Quick Start
24
+
25
+ Install the SDK:
26
+
27
+ ```shell
28
+ pip install crossplane-function-sdk-python
29
+ ```
30
+
31
+ Create a composition function by subclassing `Runtime`:
32
+
33
+ ```python
34
+ from crossplane.function.runtime import Runtime
35
+ from crossplane.function.request import RunFunctionRequest
36
+ from crossplane.function.response import RunFunctionResponse
37
+
38
+
39
+ class MyFunction(Runtime):
40
+ def Run(self, request: RunFunctionRequest) -> RunFunctionResponse:
41
+ # Your composition logic here
42
+ return response.to(request)
43
+ ```
44
+
45
+ ## Modules
46
+
47
+ - **runtime** — Base class for implementing the function runtime and gRPC server.
48
+ - **request** — Types and utilities for parsing `RunFunctionRequest` messages.
49
+ - **response** — Types and utilities for building `RunFunctionResponse` messages.
50
+ - **resource** — Kubernetes resource types used in function pipelines.
51
+ - **logging** — Structured logging utilities for function output.
52
+
53
+ ## Protobuf Types
54
+
55
+ The `RunFunctionRequest` and `RunFunctionResponse` types are generated from
56
+ proto3 schema definitions. See the proto modules for API reference:
57
+
58
+ - [`proto.v1.run_function_pb2`][crossplane.function.proto.v1.run_function_pb2]
59
+ — Current API
60
+ - [`proto.v1beta1.run_function_pb2`][crossplane.function.proto.v1beta1.run_function_pb2]
61
+ — Legacy API
62
+
63
+ Proto-generated fields behave like standard Python types but follow
64
+ [protobuf Python conventions](https://protobuf.dev/reference/python/python-generated/).
65
+ """
@@ -15,4 +15,4 @@
15
15
  """The version of function-sdk-python."""
16
16
 
17
17
  # This is set at build time, using "hatch version"
18
- __version__ = "0.13.0"
18
+ __version__ = "0.15.0"
@@ -0,0 +1,127 @@
1
+ # Copyright 2026 The Crossplane Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Standard CLI options for Python composition functions.
16
+
17
+ Provides reusable options and a run helper so that every composition
18
+ function shares a standard set of flags, environment variables, and defaults.
19
+
20
+ Usage in a function's main.py::
21
+
22
+ import click
23
+ from crossplane.function import cli as sdkcli
24
+ from function import fn
25
+
26
+ @click.command()
27
+ @sdkcli.standard_options
28
+ def cli(**kwargs):
29
+ sdkcli.run(fn.FunctionRunner(), **kwargs)
30
+
31
+ To add custom options, stack them with the decorator::
32
+
33
+ @click.command()
34
+ @sdkcli.standard_options
35
+ @click.option("--cache-size", default=100, envvar="CACHE_SIZE")
36
+ def cli(cache_size, **kwargs):
37
+ runner = fn.FunctionRunner(cache_size=cache_size)
38
+ sdkcli.run(runner, **kwargs)
39
+ """
40
+
41
+ import functools
42
+ from collections.abc import Callable
43
+ from typing import TypeVar
44
+
45
+ import click
46
+
47
+ from crossplane.function import logging, runtime
48
+ from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
49
+
50
+ F = TypeVar("F", bound=Callable)
51
+
52
+ DEFAULT_ADDRESS = "0.0.0.0:9443"
53
+ DEFAULT_MAX_RECV_MESSAGE_SIZE = 4 # MB
54
+
55
+
56
+ def standard_options(func: F) -> F:
57
+ """Apply the standard Composition Function CLI options to a Click command."""
58
+
59
+ @click.option(
60
+ "--max-recv-message-size",
61
+ type=int,
62
+ default=DEFAULT_MAX_RECV_MESSAGE_SIZE,
63
+ show_default=True,
64
+ envvar="MAX_RECV_MESSAGE_SIZE",
65
+ help="Maximum size of received gRPC messages in MB.",
66
+ )
67
+ @click.option(
68
+ "--insecure",
69
+ is_flag=True,
70
+ envvar="INSECURE",
71
+ help="Run without mTLS credentials. "
72
+ "If you supply this flag --tls-server-certs-dir will be ignored.",
73
+ )
74
+ @click.option(
75
+ "--tls-server-certs-dir",
76
+ "--tls-certs-dir",
77
+ "tls_certs_dir",
78
+ envvar="TLS_SERVER_CERTS_DIR",
79
+ help="Serve using mTLS certificates.",
80
+ )
81
+ @click.option(
82
+ "--address",
83
+ default=DEFAULT_ADDRESS,
84
+ show_default=True,
85
+ envvar="ADDRESS",
86
+ help="Address at which to listen for gRPC connections.",
87
+ )
88
+ @click.option(
89
+ "--debug",
90
+ "-d",
91
+ is_flag=True,
92
+ envvar="DEBUG",
93
+ help="Emit debug logs.",
94
+ )
95
+ @functools.wraps(func)
96
+ def wrapper(*args, **kwargs):
97
+ return func(*args, **kwargs)
98
+
99
+ return wrapper
100
+
101
+
102
+ def run( # noqa: PLR0913
103
+ function_runner: grpcv1.FunctionRunnerServiceServicer,
104
+ *,
105
+ debug: bool,
106
+ address: str,
107
+ tls_certs_dir: str | None,
108
+ insecure: bool,
109
+ max_recv_message_size: int,
110
+ ) -> None:
111
+ """Start a composition function gRPC server with standard options."""
112
+ level = logging.Level.DEBUG if debug else logging.Level.INFO
113
+ logging.configure(level=level)
114
+
115
+ size_bytes = max_recv_message_size * 1024 * 1024
116
+ options = [
117
+ ("grpc.max_receive_message_length", size_bytes),
118
+ ("grpc.max_send_message_length", size_bytes),
119
+ ]
120
+
121
+ runtime.serve(
122
+ function_runner,
123
+ address,
124
+ creds=runtime.load_credentials(tls_certs_dir),
125
+ insecure=insecure,
126
+ options=options,
127
+ )
@@ -163,13 +163,16 @@ def require_resources( # noqa: PLR0913
163
163
  namespace: The namespace to search in (optional).
164
164
 
165
165
  Raises:
166
- ValueError: If both match_name and match_labels are provided, or neither.
166
+ ValueError: If both match_name and match_labels are provided.
167
167
 
168
168
  This tells Crossplane to fetch the specified resources and include them
169
169
  in the next call to the function in req.required_resources[name].
170
+
171
+ If neither match_name nor match_labels is provided, all resources of the
172
+ given api_version and kind are matched.
170
173
  """
171
- if (match_name is None) == (match_labels is None):
172
- msg = "Exactly one of match_name or match_labels must be provided"
174
+ if match_name is not None and match_labels is not None:
175
+ msg = "match_name and match_labels are mutually exclusive"
173
176
  raise ValueError(msg)
174
177
 
175
178
  selector = fnv1.ResourceSelector(
@@ -70,7 +70,7 @@ def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials:
70
70
 
71
71
 
72
72
  def serve(
73
- function: grpcv1.FunctionRunnerService,
73
+ function: grpcv1.FunctionRunnerServiceServicer,
74
74
  address: str,
75
75
  *,
76
76
  creds: grpc.ServerCredentials,
@@ -95,7 +95,10 @@ def serve(
95
95
  are supplied.
96
96
  """
97
97
  # Define the loop before the server so everything uses the same loop.
98
- loop = asyncio.get_event_loop()
98
+ try:
99
+ loop = asyncio.get_event_loop()
100
+ except RuntimeError:
101
+ loop = asyncio.new_event_loop()
99
102
 
100
103
  server = grpc.aio.server(options=options)
101
104
 
@@ -134,20 +137,20 @@ def serve(
134
137
  loop.close()
135
138
 
136
139
 
137
- class BetaFunctionRunner(grpcv1beta1.FunctionRunnerService):
140
+ class BetaFunctionRunner(grpcv1beta1.FunctionRunnerServiceServicer):
138
141
  """A BetaFunctionRunner handles beta gRPC RunFunctionRequests.
139
142
 
140
- It handles requests by passing them to a wrapped v1.FunctionRunnerService.
143
+ It handles requests by passing them to a wrapped v1.FunctionRunnerServiceServicer.
141
144
  Incoming v1beta1 requests are converted to v1 by round-tripping them through
142
145
  serialization. Outgoing requests are converted from v1 to v1beta1 the same
143
146
  way.
144
147
  """
145
148
 
146
- def __init__(self, wrapped: grpcv1.FunctionRunnerService):
149
+ def __init__(self, wrapped: grpcv1.FunctionRunnerServiceServicer):
147
150
  """Create a new BetaFunctionRunner."""
148
151
  self.wrapped = wrapped
149
152
 
150
- async def RunFunction( # noqa: N802 # gRPC requires this name.
153
+ async def RunFunction( # noqa: N802 # gRPC requires this name. # pyright: ignore[reportIncompatibleMethodOverride]
151
154
  self, req: fnv1beta1.RunFunctionRequest, context: grpc.aio.ServicerContext
152
155
  ) -> fnv1beta1.RunFunctionResponse:
153
156
  """Run the underlying function."""
@@ -18,9 +18,10 @@ classifiers = [
18
18
  ]
19
19
 
20
20
  dependencies = [
21
- "grpcio==1.81.0",
21
+ "click==8.*",
22
+ "grpcio==1.84.0",
22
23
  "grpcio-reflection==1.*",
23
- "protobuf==7.35.0", # Must be compatible with grpcio-tools.
24
+ "protobuf==7.36.1", # Must be compatible with grpcio-tools.
24
25
  "pydantic==2.*",
25
26
  "structlog==26.*",
26
27
  ]
@@ -233,6 +233,21 @@ class TestResponse(unittest.TestCase):
233
233
  match_name="worker-1",
234
234
  ),
235
235
  ),
236
+ TestCase(
237
+ reason="Should match all resources of a kind with no match field.",
238
+ rsp=fnv1.RunFunctionResponse(),
239
+ name="all-pods",
240
+ api_version="v1",
241
+ kind="Pod",
242
+ match_name=None,
243
+ match_labels=None,
244
+ namespace="default",
245
+ want_selector=fnv1.ResourceSelector(
246
+ api_version="v1",
247
+ kind="Pod",
248
+ namespace="default",
249
+ ),
250
+ ),
236
251
  ]
237
252
 
238
253
  for case in cases:
@@ -270,17 +285,6 @@ class TestResponse(unittest.TestCase):
270
285
  match_labels={"app": "test"},
271
286
  )
272
287
 
273
- # Should raise ValueError if neither match_name nor match_labels are provided
274
- with self.assertRaises(ValueError):
275
- response.require_resources(
276
- rsp,
277
- "test",
278
- "v1",
279
- "Pod",
280
- match_name=None,
281
- match_labels=None,
282
- )
283
-
284
288
  def test_require_schema(self) -> None:
285
289
  @dataclasses.dataclass
286
290
  class TestCase: