crossplane-function-sdk-python 0.6.0__tar.gz → 0.7.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 (34) hide show
  1. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/.github/workflows/ci.yml +1 -1
  2. crossplane_function_sdk_python-0.7.0/CODEOWNERS +22 -0
  3. crossplane_function_sdk_python-0.7.0/OWNERS.md +18 -0
  4. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/PKG-INFO +4 -3
  5. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/__version__.py +1 -1
  6. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1/run_function_pb2.py +3 -3
  7. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1/run_function_pb2_grpc.py +1 -1
  8. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1beta1/run_function_pb2.py +3 -3
  9. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1beta1/run_function_pb2_grpc.py +1 -1
  10. crossplane_function_sdk_python-0.7.0/crossplane/function/py.typed +0 -0
  11. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/resource.py +5 -9
  12. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/runtime.py +9 -1
  13. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/pyproject.toml +11 -7
  14. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/test_resource.py +82 -0
  15. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/.gitignore +0 -0
  19. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/LICENSE +0 -0
  20. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/README.md +0 -0
  21. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/logging.py +0 -0
  22. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1/run_function.proto +0 -0
  23. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1/run_function_pb2.pyi +0 -0
  24. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1beta1/run_function.proto +0 -0
  25. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/proto/v1beta1/run_function_pb2.pyi +0 -0
  26. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/crossplane/function/response.py +0 -0
  27. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/renovate.json +0 -0
  28. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/test_response.py +0 -0
  29. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/test_runtime.py +0 -0
  30. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/__init__.py +0 -0
  31. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/__init__.py +0 -0
  32. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/v1.py +0 -0
  33. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/testdata/models/io/upbound/aws/s3/__init__.py +0 -0
  34. {crossplane_function_sdk_python-0.6.0 → crossplane_function_sdk_python-0.7.0}/tests/testdata/models/io/upbound/aws/s3/v1beta2.py +0 -0
@@ -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.12.3
123
+ uses: pypa/gh-action-pypi-publish@v1.12.4
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
@@ -0,0 +1,22 @@
1
+
2
+ # SPDX-FileCopyrightText: 2025 The Crossplane Authors <https://crossplane.io>
3
+ #
4
+ # SPDX-License-Identifier: CC0-1.0
5
+
6
+ # This file controls automatic PR reviewer assignment. See the following docs:
7
+ #
8
+ # * https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
9
+ # * https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team
10
+ #
11
+ # The goal of this file is for most PRs to automatically and fairly have one
12
+ # maintainer set as PR reviewers. All maintainers have permission to approve
13
+ # and merge PRs. All PRs must be approved by at least one maintainer before being merged.
14
+ #
15
+ # Where possible, prefer explicitly specifying a maintainer who is a subject
16
+ # matter expert for a particular part of the codebase rather than using fallback
17
+ # owners. Fallback owners are listed at the bottom of this file.
18
+ #
19
+ # See also OWNERS.md for governance details
20
+
21
+ # Fallback owners
22
+ * @negz @bobh66
@@ -0,0 +1,18 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 The Crossplane Authors <https://crossplane.io>
3
+
4
+ SPDX-License-Identifier: CC-BY-4.0
5
+ -->
6
+
7
+ # OWNERS
8
+
9
+ This page lists all maintainers for **this** repository. Each repository in the
10
+ [Crossplane Contrib organization](https://github.com/crossplane-contrib/) will list their
11
+ repository maintainers in their own `OWNERS.md` file.
12
+
13
+ ## Maintainers
14
+ * Nic Cope <negz@upbound.com> ([negz](https://github.com/negz))
15
+ * Bob Haddleton <bob.haddleton@nokia.com> ([bobh66](https://github.com/bobh66))
16
+
17
+
18
+ See [CODEOWNERS](./CODEOWNERS) for automatic PR assignment.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossplane-function-sdk-python
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -11,12 +11,13 @@ License-File: LICENSE
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Programming Language :: Python
13
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Typing :: Typed
14
15
  Requires-Python: >=3.11
15
16
  Requires-Dist: grpcio-reflection==1.*
16
- Requires-Dist: grpcio==1.*
17
+ Requires-Dist: grpcio==1.71.0
17
18
  Requires-Dist: protobuf==5.29.3
18
19
  Requires-Dist: pydantic==2.*
19
- Requires-Dist: structlog==24.*
20
+ Requires-Dist: structlog==25.*
20
21
  Description-Content-Type: text/markdown
21
22
 
22
23
  # function-sdk-python
@@ -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.6.0"
18
+ __version__ = "0.7.0"
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: crossplane/function/proto/v1/run_function.proto
5
- # Protobuf Python Version: 5.27.2
5
+ # Protobuf Python Version: 5.29.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
14
  5,
15
- 27,
16
- 2,
15
+ 29,
16
+ 0,
17
17
  '',
18
18
  'crossplane/function/proto/v1/run_function.proto'
19
19
  )
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from crossplane.function.proto.v1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.66.0'
8
+ GRPC_GENERATED_VERSION = '1.71.0'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: crossplane/function/proto/v1beta1/run_function.proto
5
- # Protobuf Python Version: 5.27.2
5
+ # Protobuf Python Version: 5.29.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
14
  5,
15
- 27,
16
- 2,
15
+ 29,
16
+ 0,
17
17
  '',
18
18
  'crossplane/function/proto/v1beta1/run_function.proto'
19
19
  )
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from crossplane.function.proto.v1beta1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.66.0'
8
+ GRPC_GENERATED_VERSION = '1.71.0'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -18,6 +18,7 @@ import dataclasses
18
18
  import datetime
19
19
 
20
20
  import pydantic
21
+ from google.protobuf import json_format
21
22
  from google.protobuf import struct_pb2 as structpb
22
23
 
23
24
  import crossplane.function.proto.v1.run_function_pb2 as fnv1
@@ -44,8 +45,8 @@ def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel
44
45
  # apiVersion is set to its default value 's3.aws.upbound.io/v1beta2'
45
46
  # (and not explicitly provided during initialization), it will be
46
47
  # excluded from the serialized output.
47
- data['apiVersion'] = source.apiVersion
48
- data['kind'] = source.kind
48
+ data["apiVersion"] = source.apiVersion
49
+ data["kind"] = source.kind
49
50
  r.resource.update(data)
50
51
  case structpb.Struct():
51
52
  # TODO(negz): Use struct_to_dict and update to match other semantics?
@@ -65,9 +66,7 @@ def dict_to_struct(d: dict) -> structpb.Struct:
65
66
  function makes it possible to work with a Python dict, then convert it to a
66
67
  struct in a RunFunctionResponse.
67
68
  """
68
- s = structpb.Struct()
69
- s.update(d)
70
- return s
69
+ return json_format.ParseDict(d, structpb.Struct())
71
70
 
72
71
 
73
72
  def struct_to_dict(s: structpb.Struct) -> dict:
@@ -77,10 +76,7 @@ def struct_to_dict(s: structpb.Struct) -> dict:
77
76
  protobuf struct. This function makes it possible to convert resources to a
78
77
  dictionary.
79
78
  """
80
- return {
81
- k: (struct_to_dict(v) if isinstance(v, structpb.Struct) else v)
82
- for k, v in s.items()
83
- }
79
+ return json_format.MessageToDict(s, preserving_proto_field_name=True)
84
80
 
85
81
 
86
82
  @dataclasses.dataclass
@@ -16,6 +16,7 @@
16
16
 
17
17
  import asyncio
18
18
  import os
19
+ import signal
19
20
 
20
21
  import grpc
21
22
  from grpc_reflection.v1alpha import reflection
@@ -31,6 +32,8 @@ SERVICE_NAMES = (
31
32
  fnv1beta1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name,
32
33
  )
33
34
 
35
+ SHUTDOWN_GRACE_PERIOD_SECONDS = 5
36
+
34
37
 
35
38
  def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials:
36
39
  """Load TLS credentials for a composition function gRPC server.
@@ -90,6 +93,11 @@ def serve(
90
93
 
91
94
  server = grpc.aio.server()
92
95
 
96
+ loop.add_signal_handler(
97
+ signal.SIGTERM,
98
+ lambda: asyncio.ensure_future(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS)),
99
+ )
100
+
93
101
  grpcv1.add_FunctionRunnerServiceServicer_to_server(function, server)
94
102
  grpcv1beta1.add_FunctionRunnerServiceServicer_to_server(
95
103
  BetaFunctionRunner(wrapped=function), server
@@ -116,7 +124,7 @@ def serve(
116
124
  try:
117
125
  loop.run_until_complete(start())
118
126
  finally:
119
- loop.run_until_complete(server.stop(grace=5))
127
+ loop.run_until_complete(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS))
120
128
  loop.close()
121
129
 
122
130
 
@@ -14,14 +14,15 @@ classifiers = [
14
14
  "Development Status :: 4 - Beta",
15
15
  "Programming Language :: Python",
16
16
  "Programming Language :: Python :: 3.11",
17
+ "Typing :: Typed",
17
18
  ]
18
19
 
19
20
  dependencies = [
20
- "grpcio==1.*",
21
+ "grpcio==1.71.0",
21
22
  "grpcio-reflection==1.*",
22
- "protobuf==5.29.3",
23
+ "protobuf==5.29.3", # Must be compatible with grpcio-tools.
23
24
  "pydantic==2.*",
24
- "structlog==24.*",
25
+ "structlog==25.*",
25
26
  ]
26
27
 
27
28
  dynamic = ["version"]
@@ -38,13 +39,16 @@ validate-bump = false # Allow going from 0.0.0.dev0+x to 0
38
39
  [tool.hatch.envs.default]
39
40
  type = "virtual"
40
41
  path = ".venv-default"
41
- dependencies = ["ipython==8.31.0"]
42
+ dependencies = ["ipython==9.1.0"]
42
43
 
43
44
  [tool.hatch.envs.generate]
44
45
  type = "virtual"
45
46
  detached = true
46
47
  path = ".venv-generate"
47
- dependencies = ["grpcio-tools==1.69.0"]
48
+ dependencies = [
49
+ "grpcio-tools==1.71.0",
50
+ "protobuf==5.29.3",
51
+ ]
48
52
 
49
53
  [tool.hatch.envs.generate.scripts]
50
54
  protoc = "python -m grpc_tools.protoc --proto_path=. --python_out=. --pyi_out=. --grpc_python_out=. crossplane/function/proto/v1beta1/run_function.proto crossplane/function/proto/v1/run_function.proto"
@@ -62,8 +66,8 @@ packages = ["crossplane"]
62
66
 
63
67
  # This special environment is used by hatch fmt.
64
68
  [tool.hatch.envs.hatch-static-analysis]
65
- dependencies = ["ruff==0.9.0"]
66
- config-path = "none" # Disable Hatch's default Ruff config.
69
+ dependencies = ["ruff==0.11.6"]
70
+ config-path = "none" # Disable Hatch's default Ruff config.
67
71
 
68
72
  [tool.ruff]
69
73
  target-version = "py311"
@@ -248,6 +248,66 @@ class TestResource(unittest.TestCase):
248
248
  dataclasses.asdict(case.want), dataclasses.asdict(got), "-want, +got"
249
249
  )
250
250
 
251
+ def test_dict_to_struct(self) -> None:
252
+ @dataclasses.dataclass
253
+ class TestCase:
254
+ reason: str
255
+ d: dict
256
+ want: structpb.Struct
257
+
258
+ cases = [
259
+ TestCase(
260
+ reason="Convert an empty dictionary to a struct.",
261
+ d={},
262
+ want=structpb.Struct(),
263
+ ),
264
+ TestCase(
265
+ reason="Convert a dictionary with a single field to a struct.",
266
+ d={"foo": "bar"},
267
+ want=structpb.Struct(
268
+ fields={"foo": structpb.Value(string_value="bar")}
269
+ ),
270
+ ),
271
+ TestCase(
272
+ reason="Convert a nested dictionary to a struct.",
273
+ d={"foo": {"bar": "baz"}},
274
+ want=structpb.Struct(
275
+ fields={
276
+ "foo": structpb.Value(
277
+ struct_value=structpb.Struct(
278
+ fields={"bar": structpb.Value(string_value="baz")}
279
+ )
280
+ )
281
+ }
282
+ ),
283
+ ),
284
+ TestCase(
285
+ reason="Convert a nested dictionary containing lists to a struct.",
286
+ d={"foo": {"bar": ["baz", "qux"]}},
287
+ want=structpb.Struct(
288
+ fields={
289
+ "foo": structpb.Value(
290
+ struct_value=structpb.Struct(
291
+ fields={
292
+ "bar": structpb.Value(
293
+ list_value=structpb.ListValue(
294
+ values=[
295
+ structpb.Value(string_value="baz"),
296
+ structpb.Value(string_value="qux"),
297
+ ]
298
+ )
299
+ )
300
+ }
301
+ )
302
+ )
303
+ }
304
+ ),
305
+ ),
306
+ ]
307
+ for case in cases:
308
+ got = resource.dict_to_struct(case.d)
309
+ self.assertEqual(case.want, got, "-want, +got")
310
+
251
311
  def test_struct_to_dict(self) -> None:
252
312
  @dataclasses.dataclass
253
313
  class TestCase:
@@ -279,6 +339,28 @@ class TestResource(unittest.TestCase):
279
339
  ),
280
340
  want={"foo": {"bar": "baz"}},
281
341
  ),
342
+ TestCase(
343
+ reason="Convert a nested struct containing ListValues to a dictionary.",
344
+ s=structpb.Struct(
345
+ fields={
346
+ "foo": structpb.Value(
347
+ struct_value=structpb.Struct(
348
+ fields={
349
+ "bar": structpb.Value(
350
+ list_value=structpb.ListValue(
351
+ values=[
352
+ structpb.Value(string_value="baz"),
353
+ structpb.Value(string_value="qux"),
354
+ ]
355
+ )
356
+ )
357
+ }
358
+ )
359
+ )
360
+ }
361
+ ),
362
+ want={"foo": {"bar": ["baz", "qux"]}},
363
+ ),
282
364
  ]
283
365
 
284
366
  for case in cases: