crossplane-function-sdk-python 0.11.0__tar.gz → 0.12.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.
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/.github/workflows/ci.yml +7 -7
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/PKG-INFO +3 -3
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/__version__.py +1 -1
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/resource.py +76 -2
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/response.py +38 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/pyproject.toml +2 -2
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/test_resource.py +135 -1
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/test_response.py +66 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/.gitignore +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/CODEOWNERS +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/LICENSE +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/OWNERS.md +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/README.md +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/logging.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1/run_function.proto +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1/run_function_pb2.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1/run_function_pb2.pyi +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1/run_function_pb2_grpc.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1beta1/run_function.proto +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1beta1/run_function_pb2.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1beta1/run_function_pb2.pyi +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/proto/v1beta1/run_function_pb2_grpc.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/py.typed +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/request.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/crossplane/function/runtime.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/renovate.json +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/test_request.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/test_runtime.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/__init__.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/__init__.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/testdata/models/io/k8s/apimachinery/pkg/apis/meta/v1.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/tests/testdata/models/io/upbound/aws/s3/__init__.py +0 -0
- {crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.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.
|
|
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@
|
|
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@
|
|
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.
|
|
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@
|
|
153
|
+
uses: actions/configure-pages@v6
|
|
154
154
|
|
|
155
155
|
- name: Upload artifact
|
|
156
|
-
uses: actions/upload-pages-artifact@
|
|
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@
|
|
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.
|
|
3
|
+
Version: 0.12.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,8 +14,8 @@ 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.
|
|
18
|
-
Requires-Dist: protobuf==
|
|
17
|
+
Requires-Dist: grpcio==1.80.0
|
|
18
|
+
Requires-Dist: protobuf==7.35.0
|
|
19
19
|
Requires-Dist: pydantic==2.*
|
|
20
20
|
Requires-Dist: structlog==25.*
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
@@ -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
|
|
@@ -59,6 +60,25 @@ def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel
|
|
|
59
60
|
raise TypeError(msg)
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
def update_status(
|
|
64
|
+
r: fnv1.Resource,
|
|
65
|
+
status: dict | pydantic.BaseModel,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Update a resource's status.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
r: A composite or composed resource to update.
|
|
71
|
+
status: The status to set, as a dictionary or Pydantic model.
|
|
72
|
+
|
|
73
|
+
Sets ``r.resource.status`` from the supplied status. When the status
|
|
74
|
+
is a Pydantic model, fields set to their default value are excluded,
|
|
75
|
+
matching the behavior of :func:`update`.
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(status, pydantic.BaseModel):
|
|
78
|
+
status = status.model_dump(exclude_defaults=True, warnings=False)
|
|
79
|
+
update(r, {"status": status})
|
|
80
|
+
|
|
81
|
+
|
|
62
82
|
def dict_to_struct(d: dict) -> structpb.Struct:
|
|
63
83
|
"""Create a Struct well-known type from the supplied dict.
|
|
64
84
|
|
|
@@ -99,11 +119,17 @@ class Condition:
|
|
|
99
119
|
last_transition_time: datetime.time | None = None
|
|
100
120
|
|
|
101
121
|
|
|
102
|
-
def get_condition(
|
|
122
|
+
def get_condition(
|
|
123
|
+
resource: structpb.Struct | fnv1.Resource | None,
|
|
124
|
+
typ: str,
|
|
125
|
+
) -> Condition:
|
|
103
126
|
"""Get the supplied status condition of the supplied resource.
|
|
104
127
|
|
|
105
128
|
Args:
|
|
106
|
-
resource: A Crossplane resource.
|
|
129
|
+
resource: A Crossplane resource. Can be a protobuf Struct (the raw
|
|
130
|
+
resource), an fnv1.Resource wrapper, or None. When an
|
|
131
|
+
fnv1.Resource is supplied, the Struct is extracted automatically.
|
|
132
|
+
When None is supplied, an unknown condition is returned.
|
|
107
133
|
typ: The type of status condition to get (e.g. Ready).
|
|
108
134
|
|
|
109
135
|
Returns:
|
|
@@ -111,9 +137,23 @@ def get_condition(resource: structpb.Struct, typ: str) -> Condition:
|
|
|
111
137
|
|
|
112
138
|
A status condition is always returned. If the status condition isn't present
|
|
113
139
|
in the supplied resource, a condition with status "Unknown" is returned.
|
|
140
|
+
|
|
141
|
+
Accepting fnv1.Resource and None makes it safe to pass the result of a
|
|
142
|
+
protobuf map ``.get()`` call directly. This avoids auto-vivification, which
|
|
143
|
+
silently inserts a default entry when using bracket access on a missing
|
|
144
|
+
key::
|
|
145
|
+
|
|
146
|
+
# Safe — .get() returns None without mutating the map.
|
|
147
|
+
c = get_condition(req.observed.resources.get("bucket"), "Ready")
|
|
148
|
+
|
|
149
|
+
# Unsafe — bracket access auto-vivifies an empty Resource.
|
|
150
|
+
c = get_condition(req.observed.resources["bucket"].resource, "Ready")
|
|
114
151
|
"""
|
|
115
152
|
unknown = Condition(typ=typ, status="Unknown")
|
|
116
153
|
|
|
154
|
+
if isinstance(resource, fnv1.Resource):
|
|
155
|
+
resource = resource.resource
|
|
156
|
+
|
|
117
157
|
if not resource or "status" not in resource:
|
|
118
158
|
return unknown
|
|
119
159
|
|
|
@@ -140,3 +180,37 @@ def get_condition(resource: structpb.Struct, typ: str) -> Condition:
|
|
|
140
180
|
return condition
|
|
141
181
|
|
|
142
182
|
return unknown
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_DNS_LABEL_MAX = 63
|
|
186
|
+
_HASH_LEN = 5
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def child_name(*parts: str, sep: str = "-") -> str:
|
|
190
|
+
"""Build a deterministic, DNS-label-safe name for a child resource.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
*parts: Name components to join (e.g. parent name, suffix).
|
|
194
|
+
sep: Separator between parts. Defaults to "-".
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
A name that is at most 63 characters long.
|
|
198
|
+
|
|
199
|
+
Composition functions often derive child resource names from a parent
|
|
200
|
+
name and a discriminator. The resulting name must be a valid DNS label
|
|
201
|
+
(at most 63 characters). This function joins the parts, appends a
|
|
202
|
+
deterministic 5-character hash suffix for uniqueness, and truncates
|
|
203
|
+
the prefix to fit within the limit.
|
|
204
|
+
|
|
205
|
+
The hash suffix is always appended, even for short names, so that
|
|
206
|
+
names are visually consistent regardless of length::
|
|
207
|
+
|
|
208
|
+
child_name("my-xr", "bucket") # "my-xr-bucket-a1b2c"
|
|
209
|
+
child_name("my-very-long-xr-name",
|
|
210
|
+
"with-a-very-long-suffix") # truncated to 63 chars
|
|
211
|
+
"""
|
|
212
|
+
full = sep.join(parts)
|
|
213
|
+
h = hashlib.sha256(full.encode()).hexdigest()[:_HASH_LEN]
|
|
214
|
+
max_prefix = _DNS_LABEL_MAX - _HASH_LEN - 1
|
|
215
|
+
prefix = full[:max_prefix].rstrip(sep)
|
|
216
|
+
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
|
|
{crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/pyproject.toml
RENAMED
|
@@ -18,9 +18,9 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
dependencies = [
|
|
21
|
-
"grpcio==1.
|
|
21
|
+
"grpcio==1.80.0",
|
|
22
22
|
"grpcio-reflection==1.*",
|
|
23
|
-
"protobuf==
|
|
23
|
+
"protobuf==7.35.0", # Must be compatible with grpcio-tools.
|
|
24
24
|
"pydantic==2.*",
|
|
25
25
|
"structlog==25.*",
|
|
26
26
|
]
|
|
@@ -29,6 +29,56 @@ class TestResource(unittest.TestCase):
|
|
|
29
29
|
def setUp(self) -> None:
|
|
30
30
|
logging.configure(level=logging.Level.DISABLED)
|
|
31
31
|
|
|
32
|
+
def test_update_status(self) -> None:
|
|
33
|
+
@dataclasses.dataclass
|
|
34
|
+
class TestCase:
|
|
35
|
+
reason: str
|
|
36
|
+
r: fnv1.Resource
|
|
37
|
+
status: dict | pydantic.BaseModel
|
|
38
|
+
want: dict
|
|
39
|
+
|
|
40
|
+
cases = [
|
|
41
|
+
TestCase(
|
|
42
|
+
reason="Setting status from a dict should work.",
|
|
43
|
+
r=fnv1.Resource(
|
|
44
|
+
resource=resource.dict_to_struct(
|
|
45
|
+
{"apiVersion": "example.org", "kind": "XR"}
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
status={"ready": True},
|
|
49
|
+
want={
|
|
50
|
+
"apiVersion": "example.org",
|
|
51
|
+
"kind": "XR",
|
|
52
|
+
"status": {"ready": True},
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
TestCase(
|
|
56
|
+
reason="Setting status from a Pydantic model should work.",
|
|
57
|
+
r=fnv1.Resource(
|
|
58
|
+
resource=resource.dict_to_struct(
|
|
59
|
+
{"apiVersion": "example.org", "kind": "XR"}
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
status=v1beta2.ForProvider(region="us-west-2"),
|
|
63
|
+
want={
|
|
64
|
+
"apiVersion": "example.org",
|
|
65
|
+
"kind": "XR",
|
|
66
|
+
"status": {"region": "us-west-2"},
|
|
67
|
+
},
|
|
68
|
+
),
|
|
69
|
+
TestCase(
|
|
70
|
+
reason="Setting status on an empty resource should work.",
|
|
71
|
+
r=fnv1.Resource(),
|
|
72
|
+
status={"replicas": 3},
|
|
73
|
+
want={"status": {"replicas": 3}},
|
|
74
|
+
),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
for case in cases:
|
|
78
|
+
resource.update_status(case.r, case.status)
|
|
79
|
+
got = resource.struct_to_dict(case.r.resource)
|
|
80
|
+
self.assertEqual(case.want, got, case.reason)
|
|
81
|
+
|
|
32
82
|
def test_add(self) -> None:
|
|
33
83
|
@dataclasses.dataclass
|
|
34
84
|
class TestCase:
|
|
@@ -112,11 +162,17 @@ class TestResource(unittest.TestCase):
|
|
|
112
162
|
@dataclasses.dataclass
|
|
113
163
|
class TestCase:
|
|
114
164
|
reason: str
|
|
115
|
-
res: structpb.Struct
|
|
165
|
+
res: structpb.Struct | fnv1.Resource | None
|
|
116
166
|
typ: str
|
|
117
167
|
want: resource.Condition
|
|
118
168
|
|
|
119
169
|
cases = [
|
|
170
|
+
TestCase(
|
|
171
|
+
reason="Return an unknown condition if the resource is None.",
|
|
172
|
+
res=None,
|
|
173
|
+
typ="Ready",
|
|
174
|
+
want=resource.Condition(typ="Ready", status="Unknown"),
|
|
175
|
+
),
|
|
120
176
|
TestCase(
|
|
121
177
|
reason="Return an unknown condition if the resource has no status.",
|
|
122
178
|
res=resource.dict_to_struct({}),
|
|
@@ -197,6 +253,31 @@ class TestResource(unittest.TestCase):
|
|
|
197
253
|
),
|
|
198
254
|
),
|
|
199
255
|
),
|
|
256
|
+
TestCase(
|
|
257
|
+
reason="Unwrap an fnv1.Resource to get the condition from its Struct.",
|
|
258
|
+
res=fnv1.Resource(
|
|
259
|
+
resource=resource.dict_to_struct(
|
|
260
|
+
{
|
|
261
|
+
"status": {
|
|
262
|
+
"conditions": [
|
|
263
|
+
{
|
|
264
|
+
"type": "Ready",
|
|
265
|
+
"status": "True",
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
),
|
|
272
|
+
typ="Ready",
|
|
273
|
+
want=resource.Condition(typ="Ready", status="True"),
|
|
274
|
+
),
|
|
275
|
+
TestCase(
|
|
276
|
+
reason="Return an unknown condition from an empty fnv1.Resource.",
|
|
277
|
+
res=fnv1.Resource(),
|
|
278
|
+
typ="Ready",
|
|
279
|
+
want=resource.Condition(typ="Ready", status="Unknown"),
|
|
280
|
+
),
|
|
200
281
|
]
|
|
201
282
|
|
|
202
283
|
for case in cases:
|
|
@@ -324,6 +405,59 @@ class TestResource(unittest.TestCase):
|
|
|
324
405
|
got = resource.struct_to_dict(case.s)
|
|
325
406
|
self.assertEqual(case.want, got, "-want, +got")
|
|
326
407
|
|
|
408
|
+
def test_child_name(self) -> None:
|
|
409
|
+
@dataclasses.dataclass
|
|
410
|
+
class TestCase:
|
|
411
|
+
reason: str
|
|
412
|
+
parts: list[str]
|
|
413
|
+
want: str
|
|
414
|
+
|
|
415
|
+
cases = [
|
|
416
|
+
TestCase(
|
|
417
|
+
reason="A short name should be joined with a hash suffix.",
|
|
418
|
+
parts=["my-xr", "bucket"],
|
|
419
|
+
want="my-xr-bucket-05ecb",
|
|
420
|
+
),
|
|
421
|
+
TestCase(
|
|
422
|
+
reason="A single part should get a hash suffix.",
|
|
423
|
+
parts=["my-xr"],
|
|
424
|
+
want="my-xr-9d53f",
|
|
425
|
+
),
|
|
426
|
+
TestCase(
|
|
427
|
+
reason="A long name should be truncated to fit within 63 characters.",
|
|
428
|
+
parts=["a" * 40, "b" * 40],
|
|
429
|
+
want="a" * 40 + "-" + "b" * 16 + "-" + "f5e42",
|
|
430
|
+
),
|
|
431
|
+
TestCase(
|
|
432
|
+
reason="A name that would end with a trailing separator "
|
|
433
|
+
"after truncation should have the separator stripped.",
|
|
434
|
+
parts=["a" * 56 + "-", "x"],
|
|
435
|
+
# Without stripping, this would be "aaa..a--<hash>".
|
|
436
|
+
# The trailing separator from the truncation is stripped.
|
|
437
|
+
want="a" * 56 + "-" + "995eb",
|
|
438
|
+
),
|
|
439
|
+
TestCase(
|
|
440
|
+
reason="The same inputs should always produce the same name.",
|
|
441
|
+
parts=["parent", "child"],
|
|
442
|
+
want="parent-child-2f0c9",
|
|
443
|
+
),
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
for case in cases:
|
|
447
|
+
got = resource.child_name(*case.parts)
|
|
448
|
+
self.assertEqual(case.want, got, case.reason)
|
|
449
|
+
self.assertLessEqual(len(got), 63, case.reason)
|
|
450
|
+
|
|
451
|
+
def test_child_name_deterministic(self) -> None:
|
|
452
|
+
a = resource.child_name("parent", "child")
|
|
453
|
+
b = resource.child_name("parent", "child")
|
|
454
|
+
self.assertEqual(a, b)
|
|
455
|
+
|
|
456
|
+
def test_child_name_unique(self) -> None:
|
|
457
|
+
a = resource.child_name("parent", "child-a")
|
|
458
|
+
b = resource.child_name("parent", "child-b")
|
|
459
|
+
self.assertNotEqual(a, b)
|
|
460
|
+
|
|
327
461
|
|
|
328
462
|
if __name__ == "__main__":
|
|
329
463
|
unittest.main()
|
|
@@ -71,6 +71,72 @@ class TestResponse(unittest.TestCase):
|
|
|
71
71
|
"-want, +got",
|
|
72
72
|
)
|
|
73
73
|
|
|
74
|
+
def test_set_conditions(self) -> None:
|
|
75
|
+
@dataclasses.dataclass
|
|
76
|
+
class TestCase:
|
|
77
|
+
reason: str
|
|
78
|
+
conditions: list[resource.Condition]
|
|
79
|
+
want_types: list[str]
|
|
80
|
+
want_statuses: list[fnv1.Status.ValueType]
|
|
81
|
+
want_reasons: list[str]
|
|
82
|
+
want_messages: list[str]
|
|
83
|
+
|
|
84
|
+
cases = [
|
|
85
|
+
TestCase(
|
|
86
|
+
reason="A single True condition should work.",
|
|
87
|
+
conditions=[
|
|
88
|
+
resource.Condition(
|
|
89
|
+
typ="DatabaseReady",
|
|
90
|
+
status="True",
|
|
91
|
+
reason="Available",
|
|
92
|
+
message="The database is ready",
|
|
93
|
+
),
|
|
94
|
+
],
|
|
95
|
+
want_types=["DatabaseReady"],
|
|
96
|
+
want_statuses=[fnv1.STATUS_CONDITION_TRUE],
|
|
97
|
+
want_reasons=["Available"],
|
|
98
|
+
want_messages=["The database is ready"],
|
|
99
|
+
),
|
|
100
|
+
TestCase(
|
|
101
|
+
reason="Multiple conditions should all be appended.",
|
|
102
|
+
conditions=[
|
|
103
|
+
resource.Condition(
|
|
104
|
+
typ="DatabaseReady",
|
|
105
|
+
status="True",
|
|
106
|
+
reason="Available",
|
|
107
|
+
),
|
|
108
|
+
resource.Condition(
|
|
109
|
+
typ="CacheReady",
|
|
110
|
+
status="False",
|
|
111
|
+
reason="Creating",
|
|
112
|
+
),
|
|
113
|
+
resource.Condition(
|
|
114
|
+
typ="NetworkReady",
|
|
115
|
+
status="Unknown",
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
want_types=["DatabaseReady", "CacheReady", "NetworkReady"],
|
|
119
|
+
want_statuses=[
|
|
120
|
+
fnv1.STATUS_CONDITION_TRUE,
|
|
121
|
+
fnv1.STATUS_CONDITION_FALSE,
|
|
122
|
+
fnv1.STATUS_CONDITION_UNKNOWN,
|
|
123
|
+
],
|
|
124
|
+
want_reasons=["Available", "Creating", ""],
|
|
125
|
+
want_messages=["", "", ""],
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
for case in cases:
|
|
130
|
+
rsp = fnv1.RunFunctionResponse()
|
|
131
|
+
response.set_conditions(rsp, *case.conditions)
|
|
132
|
+
|
|
133
|
+
self.assertEqual(len(case.conditions), len(rsp.conditions), case.reason)
|
|
134
|
+
for i, got in enumerate(rsp.conditions):
|
|
135
|
+
self.assertEqual(case.want_types[i], got.type, case.reason)
|
|
136
|
+
self.assertEqual(case.want_statuses[i], got.status, case.reason)
|
|
137
|
+
self.assertEqual(case.want_reasons[i], got.reason, case.reason)
|
|
138
|
+
self.assertEqual(case.want_messages[i], got.message, case.reason)
|
|
139
|
+
|
|
74
140
|
def test_set_output(self) -> None:
|
|
75
141
|
@dataclasses.dataclass
|
|
76
142
|
class TestCase:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crossplane_function_sdk_python-0.11.0 → crossplane_function_sdk_python-0.12.0}/renovate.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|