Skip to content

Commit 33ee9e1

Browse files
authored
Merge pull request #98 from negz/pydants
Add a `resource.update` convenience function
2 parents b57a5cc + df655ca commit 33ee9e1

File tree

8 files changed

+1225
-2
lines changed

8 files changed

+1225
-2
lines changed

crossplane/function/resource.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,39 @@
1717
import dataclasses
1818
import datetime
1919

20+
import pydantic
2021
from google.protobuf import struct_pb2 as structpb
2122

23+
import crossplane.function.proto.v1.run_function_pb2 as fnv1
24+
2225
# TODO(negz): Do we really need dict_to_struct and struct_to_dict? They don't do
2326
# much, but are perhaps useful for discoverability/"documentation" purposes.
2427

2528

29+
def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel):
30+
"""Update a composite or composed resource.
31+
32+
Use update to add or update the supplied resource. If the resource doesn't
33+
exist, it'll be added. If the resource does exist, it'll be updated. The
34+
update method semantics are the same as a dictionary's update method. Fields
35+
that don't exist will be added. Fields that exist will be overwritten.
36+
37+
The source can be a dictionary, a protobuf Struct, or a Pydantic model.
38+
"""
39+
match source:
40+
case pydantic.BaseModel():
41+
r.resource.update(source.model_dump(exclude_defaults=True, warnings=False))
42+
case structpb.Struct():
43+
# TODO(negz): Use struct_to_dict and update to match other semantics?
44+
r.resource.MergeFrom(source)
45+
case dict():
46+
r.resource.update(source)
47+
case _:
48+
t = type(source)
49+
msg = f"Unsupported type: {t}"
50+
raise TypeError(msg)
51+
52+
2653
def dict_to_struct(d: dict) -> structpb.Struct:
2754
"""Create a Struct well-known type from the supplied dict.
2855

pyproject.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ classifiers = [
1616
"Programming Language :: Python :: 3.11",
1717
]
1818

19-
dependencies = ["grpcio==1.*", "grpcio-reflection==1.*", "protobuf==5.27.2", "structlog==24.*"]
19+
dependencies = [
20+
"grpcio==1.*",
21+
"grpcio-reflection==1.*",
22+
"protobuf==5.27.2",
23+
"pydantic==2.*",
24+
"structlog==24.*",
25+
]
2026

2127
dynamic = ["version"]
2228

@@ -73,7 +79,7 @@ packages = ["crossplane"]
7379

7480
[tool.ruff]
7581
target-version = "py311"
76-
exclude = ["crossplane/function/proto/*"]
82+
exclude = ["crossplane/function/proto/*", "tests/testdata/*"]
7783
lint.select = [
7884
"A",
7985
"ARG",

tests/test_resource.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,95 @@
1616
import datetime
1717
import unittest
1818

19+
import pydantic
20+
from google.protobuf import json_format
1921
from google.protobuf import struct_pb2 as structpb
2022

23+
import crossplane.function.proto.v1.run_function_pb2 as fnv1
2124
from crossplane.function import logging, resource
2225

26+
from .testdata.models.io.upbound.aws.s3 import v1beta2
27+
2328

2429
class TestResource(unittest.TestCase):
2530
def setUp(self) -> None:
2631
logging.configure(level=logging.Level.DISABLED)
2732

33+
def test_add(self) -> None:
34+
@dataclasses.dataclass
35+
class TestCase:
36+
reason: str
37+
r: fnv1.Resource
38+
source: dict | structpb.Struct | pydantic.BaseModel
39+
want: fnv1.Resource
40+
41+
cases = [
42+
TestCase(
43+
reason="Updating from a dict should work.",
44+
r=fnv1.Resource(),
45+
source={"apiVersion": "example.org", "kind": "Resource"},
46+
want=fnv1.Resource(
47+
resource=resource.dict_to_struct(
48+
{"apiVersion": "example.org", "kind": "Resource"}
49+
),
50+
),
51+
),
52+
TestCase(
53+
reason="Updating an existing resource from a dict should work.",
54+
r=fnv1.Resource(
55+
resource=resource.dict_to_struct(
56+
{"apiVersion": "example.org", "kind": "Resource"}
57+
),
58+
),
59+
source={
60+
"metadata": {"name": "cool"},
61+
},
62+
want=fnv1.Resource(
63+
resource=resource.dict_to_struct(
64+
{
65+
"apiVersion": "example.org",
66+
"kind": "Resource",
67+
"metadata": {"name": "cool"},
68+
}
69+
),
70+
),
71+
),
72+
TestCase(
73+
reason="Updating from a struct should work.",
74+
r=fnv1.Resource(),
75+
source=resource.dict_to_struct(
76+
{"apiVersion": "example.org", "kind": "Resource"}
77+
),
78+
want=fnv1.Resource(
79+
resource=resource.dict_to_struct(
80+
{"apiVersion": "example.org", "kind": "Resource"}
81+
),
82+
),
83+
),
84+
TestCase(
85+
reason="Updating from a Pydantic model should work.",
86+
r=fnv1.Resource(),
87+
source=v1beta2.Bucket(
88+
spec=v1beta2.Spec(
89+
forProvider=v1beta2.ForProvider(region="us-west-2"),
90+
),
91+
),
92+
want=fnv1.Resource(
93+
resource=resource.dict_to_struct(
94+
{"spec": {"forProvider": {"region": "us-west-2"}}}
95+
),
96+
),
97+
),
98+
]
99+
100+
for case in cases:
101+
resource.update(case.r, case.source)
102+
self.assertEqual(
103+
json_format.MessageToDict(case.want),
104+
json_format.MessageToDict(case.r),
105+
"-want, +got",
106+
)
107+
28108
def test_get_condition(self) -> None:
29109
@dataclasses.dataclass
30110
class TestCase:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: <stdin>
3+
# timestamp: 2024-10-04T21:01:52+00:00
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: <stdin>
3+
# timestamp: 2024-10-04T21:01:52+00:00

0 commit comments

Comments
 (0)