crossplane-function-sdk-python 0.11.0__tar.gz → 0.13.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 (45) hide show
  1. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/.github/workflows/ci.yml +7 -7
  2. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/PKG-INFO +4 -4
  3. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/__version__.py +1 -1
  4. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/resource.py +92 -8
  5. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/response.py +38 -0
  6. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/pyproject.toml +3 -3
  7. crossplane_function_sdk_python-0.13.0/tests/test_resource.py +654 -0
  8. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/test_response.py +66 -0
  9. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/k8s/api/__init__.py +2 -0
  10. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/k8s/api/resource/__init__.py +2 -0
  11. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/k8s/api/resource/v1.py +66 -0
  12. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/upbound/m/__init__.py +0 -0
  13. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/upbound/m/aws/__init__.py +0 -0
  14. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/upbound/m/aws/iam/__init__.py +0 -0
  15. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/upbound/m/aws/iam/accountalias/__init__.py +0 -0
  16. crossplane_function_sdk_python-0.13.0/tests/testdata/models/io/upbound/m/aws/iam/accountalias/v1beta1.py +166 -0
  17. crossplane_function_sdk_python-0.11.0/tests/test_resource.py +0 -329
  18. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  21. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/.gitignore +0 -0
  22. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/CODEOWNERS +0 -0
  23. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/LICENSE +0 -0
  24. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/OWNERS.md +0 -0
  25. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/README.md +0 -0
  26. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/logging.py +0 -0
  27. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1/run_function.proto +0 -0
  28. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1/run_function_pb2.py +0 -0
  29. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1/run_function_pb2.pyi +0 -0
  30. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1/run_function_pb2_grpc.py +0 -0
  31. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1beta1/run_function.proto +0 -0
  32. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1beta1/run_function_pb2.py +0 -0
  33. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1beta1/run_function_pb2.pyi +0 -0
  34. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/proto/v1beta1/run_function_pb2_grpc.py +0 -0
  35. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/py.typed +0 -0
  36. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/request.py +0 -0
  37. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/crossplane/function/runtime.py +0 -0
  38. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/renovate.json +0 -0
  39. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/test_request.py +0 -0
  40. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/test_runtime.py +0 -0
  41. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/__init__.py +0 -0
  42. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/__init__.py +0 -0
  43. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/v1.py +0 -0
  44. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/testdata/models/io/upbound/aws/s3/__init__.py +0 -0
  45. {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.13.0}/tests/testdata/models/io/upbound/aws/s3/v1beta2.py +0 -0
@@ -28,7 +28,7 @@ concurrency:
28
28
  env:
29
29
  # Common versions
30
30
  PYTHON_VERSION: '3.11'
31
- HATCH_VERSION: '1.12.0'
31
+ HATCH_VERSION: '1.16.5'
32
32
 
33
33
  # The PyPi project version to push. The default is v0.0.0+gitdate-gitsha.
34
34
  PYPI_VERSION: ${{ inputs.version }}
@@ -98,7 +98,7 @@ jobs:
98
98
  run: hatch build
99
99
 
100
100
  - name: Upload Sdist and Wheel to GitHub
101
- uses: actions/upload-artifact@v6
101
+ uses: actions/upload-artifact@v7
102
102
  with:
103
103
  name: dist
104
104
  path: "dist/*"
@@ -114,13 +114,13 @@ jobs:
114
114
  runs-on: ubuntu-24.04
115
115
  steps:
116
116
  - name: Download Sdist and Wheel from GitHub
117
- uses: actions/download-artifact@v7
117
+ uses: actions/download-artifact@v8
118
118
  with:
119
119
  name: dist
120
120
  path: "dist"
121
121
 
122
122
  - name: Publish to PyPI
123
- uses: pypa/gh-action-pypi-publish@v1.13.0
123
+ uses: pypa/gh-action-pypi-publish@v1.14.0
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
@@ -150,13 +150,13 @@ jobs:
150
150
  run: hatch run docs:pdoc -d google crossplane/function -o docs
151
151
 
152
152
  - name: Setup Pages
153
- uses: actions/configure-pages@v5
153
+ uses: actions/configure-pages@v6
154
154
 
155
155
  - name: Upload artifact
156
- uses: actions/upload-pages-artifact@v4
156
+ uses: actions/upload-pages-artifact@v5
157
157
  with:
158
158
  path: docs
159
159
 
160
160
  - name: Deploy to GitHub Pages
161
161
  id: deployment
162
- uses: actions/deploy-pages@v4
162
+ uses: actions/deploy-pages@v5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossplane-function-sdk-python
3
- Version: 0.11.0
3
+ Version: 0.13.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
@@ -14,10 +14,10 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Typing :: Typed
15
15
  Requires-Python: >=3.11
16
16
  Requires-Dist: grpcio-reflection==1.*
17
- Requires-Dist: grpcio==1.76.0
18
- Requires-Dist: protobuf==6.33.5
17
+ Requires-Dist: grpcio==1.81.0
18
+ Requires-Dist: protobuf==7.35.0
19
19
  Requires-Dist: pydantic==2.*
20
- Requires-Dist: structlog==25.*
20
+ Requires-Dist: structlog==26.*
21
21
  Description-Content-Type: text/markdown
22
22
 
23
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.11.0"
18
+ __version__ = "0.13.0"
@@ -16,6 +16,7 @@
16
16
 
17
17
  import dataclasses
18
18
  import datetime
19
+ import hashlib
19
20
 
20
21
  import pydantic
21
22
  from google.protobuf import json_format
@@ -39,12 +40,21 @@ def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel
39
40
  """
40
41
  match source:
41
42
  case pydantic.BaseModel():
42
- data = source.model_dump(exclude_defaults=True, warnings=False)
43
- # In Pydantic, exclude_defaults=True in model_dump excludes fields
44
- # that have their value equal to the default. If a field like
45
- # apiVersion is set to its default value 's3.aws.upbound.io/v1beta2'
46
- # (and not explicitly provided during initialization), it will be
47
- # excluded from the serialized output.
43
+ # exclude_unset emits only the fields the caller explicitly set.
44
+ # Crossplane treats desired resources as server-side apply intent,
45
+ # so a function should own exactly the fields it has an opinion
46
+ # about and leave the rest to the API server.
47
+ #
48
+ # by_alias emits each field under its alias, which is its real
49
+ # wire name. datamodel-code-generator aliases fields whose KRM
50
+ # name collides with a Python keyword or builtin (e.g. it emits a
51
+ # bool_ attribute aliased to bool, continue_ aliased to continue).
52
+ # Without by_alias those fields serialize under the Python name and
53
+ # don't match the resource's schema. It's a no-op for ordinary
54
+ # fields, which have no alias.
55
+ data = source.model_dump(exclude_unset=True, by_alias=True, warnings=False)
56
+ # apiVersion and kind identify the resource but are rarely passed
57
+ # as kwargs, so they're usually unset. Add them back explicitly.
48
58
  data["apiVersion"] = source.apiVersion
49
59
  data["kind"] = source.kind
50
60
  r.resource.update(data)
@@ -59,6 +69,26 @@ def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel
59
69
  raise TypeError(msg)
60
70
 
61
71
 
72
+ def update_status(
73
+ r: fnv1.Resource,
74
+ status: dict | pydantic.BaseModel,
75
+ ) -> None:
76
+ """Update a resource's status.
77
+
78
+ Args:
79
+ r: A composite or composed resource to update.
80
+ status: The status to set, as a dictionary or Pydantic model.
81
+
82
+ Sets ``r.resource.status`` from the supplied status. When the status
83
+ is a Pydantic model, fields the caller didn't explicitly set are
84
+ excluded and aliased fields are emitted under their wire names,
85
+ matching the behavior of :func:`update`.
86
+ """
87
+ if isinstance(status, pydantic.BaseModel):
88
+ status = status.model_dump(exclude_unset=True, by_alias=True, warnings=False)
89
+ update(r, {"status": status})
90
+
91
+
62
92
  def dict_to_struct(d: dict) -> structpb.Struct:
63
93
  """Create a Struct well-known type from the supplied dict.
64
94
 
@@ -99,11 +129,17 @@ class Condition:
99
129
  last_transition_time: datetime.time | None = None
100
130
 
101
131
 
102
- def get_condition(resource: structpb.Struct, typ: str) -> Condition:
132
+ def get_condition(
133
+ resource: structpb.Struct | fnv1.Resource | None,
134
+ typ: str,
135
+ ) -> Condition:
103
136
  """Get the supplied status condition of the supplied resource.
104
137
 
105
138
  Args:
106
- resource: A Crossplane resource.
139
+ resource: A Crossplane resource. Can be a protobuf Struct (the raw
140
+ resource), an fnv1.Resource wrapper, or None. When an
141
+ fnv1.Resource is supplied, the Struct is extracted automatically.
142
+ When None is supplied, an unknown condition is returned.
107
143
  typ: The type of status condition to get (e.g. Ready).
108
144
 
109
145
  Returns:
@@ -111,9 +147,23 @@ def get_condition(resource: structpb.Struct, typ: str) -> Condition:
111
147
 
112
148
  A status condition is always returned. If the status condition isn't present
113
149
  in the supplied resource, a condition with status "Unknown" is returned.
150
+
151
+ Accepting fnv1.Resource and None makes it safe to pass the result of a
152
+ protobuf map ``.get()`` call directly. This avoids auto-vivification, which
153
+ silently inserts a default entry when using bracket access on a missing
154
+ key::
155
+
156
+ # Safe — .get() returns None without mutating the map.
157
+ c = get_condition(req.observed.resources.get("bucket"), "Ready")
158
+
159
+ # Unsafe — bracket access auto-vivifies an empty Resource.
160
+ c = get_condition(req.observed.resources["bucket"].resource, "Ready")
114
161
  """
115
162
  unknown = Condition(typ=typ, status="Unknown")
116
163
 
164
+ if isinstance(resource, fnv1.Resource):
165
+ resource = resource.resource
166
+
117
167
  if not resource or "status" not in resource:
118
168
  return unknown
119
169
 
@@ -140,3 +190,37 @@ def get_condition(resource: structpb.Struct, typ: str) -> Condition:
140
190
  return condition
141
191
 
142
192
  return unknown
193
+
194
+
195
+ _DNS_LABEL_MAX = 63
196
+ _HASH_LEN = 5
197
+
198
+
199
+ def child_name(*parts: str, sep: str = "-") -> str:
200
+ """Build a deterministic, DNS-label-safe name for a child resource.
201
+
202
+ Args:
203
+ *parts: Name components to join (e.g. parent name, suffix).
204
+ sep: Separator between parts. Defaults to "-".
205
+
206
+ Returns:
207
+ A name that is at most 63 characters long.
208
+
209
+ Composition functions often derive child resource names from a parent
210
+ name and a discriminator. The resulting name must be a valid DNS label
211
+ (at most 63 characters). This function joins the parts, appends a
212
+ deterministic 5-character hash suffix for uniqueness, and truncates
213
+ the prefix to fit within the limit.
214
+
215
+ The hash suffix is always appended, even for short names, so that
216
+ names are visually consistent regardless of length::
217
+
218
+ child_name("my-xr", "bucket") # "my-xr-bucket-a1b2c"
219
+ child_name("my-very-long-xr-name",
220
+ "with-a-very-long-suffix") # truncated to 63 chars
221
+ """
222
+ full = sep.join(parts)
223
+ h = hashlib.sha256(full.encode()).hexdigest()[:_HASH_LEN]
224
+ max_prefix = _DNS_LABEL_MAX - _HASH_LEN - 1
225
+ prefix = full[:max_prefix].rstrip(sep)
226
+ return f"{prefix}{sep}{h}"
@@ -81,6 +81,44 @@ def fatal(rsp: fnv1.RunFunctionResponse, message: str) -> None:
81
81
  )
82
82
 
83
83
 
84
+ _STATUS_MAP = {
85
+ "True": fnv1.STATUS_CONDITION_TRUE,
86
+ "False": fnv1.STATUS_CONDITION_FALSE,
87
+ "Unknown": fnv1.STATUS_CONDITION_UNKNOWN,
88
+ }
89
+
90
+
91
+ def set_conditions(
92
+ rsp: fnv1.RunFunctionResponse,
93
+ *conditions: resource.Condition,
94
+ ) -> None:
95
+ """Set one or more conditions on the composite resource (XR).
96
+
97
+ Args:
98
+ rsp: The RunFunctionResponse to update.
99
+ *conditions: The conditions to set.
100
+
101
+ Each condition is appended to ``rsp.conditions``. Crossplane uses the
102
+ conditions returned by a function to set custom status conditions on
103
+ the composite resource.
104
+
105
+ The ``last_transition_time`` field of each condition is ignored.
106
+ Crossplane sets the transition time itself.
107
+
108
+ Do not set the ``Ready`` condition type. Crossplane manages it based
109
+ on resource readiness.
110
+ """
111
+ for condition in conditions:
112
+ c = fnv1.Condition(
113
+ type=condition.typ,
114
+ status=_STATUS_MAP.get(condition.status, fnv1.STATUS_CONDITION_UNKNOWN),
115
+ reason=condition.reason or "",
116
+ )
117
+ if condition.message:
118
+ c.message = condition.message
119
+ rsp.conditions.append(c)
120
+
121
+
84
122
  def set_output(rsp: fnv1.RunFunctionResponse, output: dict | structpb.Struct) -> None:
85
123
  """Set the output field in a RunFunctionResponse for operation functions.
86
124
 
@@ -18,11 +18,11 @@ classifiers = [
18
18
  ]
19
19
 
20
20
  dependencies = [
21
- "grpcio==1.76.0",
21
+ "grpcio==1.81.0",
22
22
  "grpcio-reflection==1.*",
23
- "protobuf==6.33.5", # Must be compatible with grpcio-tools.
23
+ "protobuf==7.35.0", # Must be compatible with grpcio-tools.
24
24
  "pydantic==2.*",
25
- "structlog==25.*",
25
+ "structlog==26.*",
26
26
  ]
27
27
 
28
28
  dynamic = ["version"]