Skip to content

Commit

Permalink
Merge pull request #98 from negz/pydants
Browse files Browse the repository at this point in the history
Add a `resource.update` convenience function
  • Loading branch information
negz authored Oct 10, 2024
2 parents b57a5cc + df655ca commit 33ee9e1
Show file tree
Hide file tree
Showing 8 changed files with 1,225 additions and 2 deletions.
27 changes: 27 additions & 0 deletions crossplane/function/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,39 @@
import dataclasses
import datetime

import pydantic
from google.protobuf import struct_pb2 as structpb

import crossplane.function.proto.v1.run_function_pb2 as fnv1

# TODO(negz): Do we really need dict_to_struct and struct_to_dict? They don't do
# much, but are perhaps useful for discoverability/"documentation" purposes.


def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel):
"""Update a composite or composed resource.
Use update to add or update the supplied resource. If the resource doesn't
exist, it'll be added. If the resource does exist, it'll be updated. The
update method semantics are the same as a dictionary's update method. Fields
that don't exist will be added. Fields that exist will be overwritten.
The source can be a dictionary, a protobuf Struct, or a Pydantic model.
"""
match source:
case pydantic.BaseModel():
r.resource.update(source.model_dump(exclude_defaults=True, warnings=False))
case structpb.Struct():
# TODO(negz): Use struct_to_dict and update to match other semantics?
r.resource.MergeFrom(source)
case dict():
r.resource.update(source)
case _:
t = type(source)
msg = f"Unsupported type: {t}"
raise TypeError(msg)


def dict_to_struct(d: dict) -> structpb.Struct:
"""Create a Struct well-known type from the supplied dict.
Expand Down
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ classifiers = [
"Programming Language :: Python :: 3.11",
]

dependencies = ["grpcio==1.*", "grpcio-reflection==1.*", "protobuf==5.27.2", "structlog==24.*"]
dependencies = [
"grpcio==1.*",
"grpcio-reflection==1.*",
"protobuf==5.27.2",
"pydantic==2.*",
"structlog==24.*",
]

dynamic = ["version"]

Expand Down Expand Up @@ -73,7 +79,7 @@ packages = ["crossplane"]

[tool.ruff]
target-version = "py311"
exclude = ["crossplane/function/proto/*"]
exclude = ["crossplane/function/proto/*", "tests/testdata/*"]
lint.select = [
"A",
"ARG",
Expand Down
80 changes: 80 additions & 0 deletions tests/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,95 @@
import datetime
import unittest

import pydantic
from google.protobuf import json_format
from google.protobuf import struct_pb2 as structpb

import crossplane.function.proto.v1.run_function_pb2 as fnv1
from crossplane.function import logging, resource

from .testdata.models.io.upbound.aws.s3 import v1beta2


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

def test_add(self) -> None:
@dataclasses.dataclass
class TestCase:
reason: str
r: fnv1.Resource
source: dict | structpb.Struct | pydantic.BaseModel
want: fnv1.Resource

cases = [
TestCase(
reason="Updating from a dict should work.",
r=fnv1.Resource(),
source={"apiVersion": "example.org", "kind": "Resource"},
want=fnv1.Resource(
resource=resource.dict_to_struct(
{"apiVersion": "example.org", "kind": "Resource"}
),
),
),
TestCase(
reason="Updating an existing resource from a dict should work.",
r=fnv1.Resource(
resource=resource.dict_to_struct(
{"apiVersion": "example.org", "kind": "Resource"}
),
),
source={
"metadata": {"name": "cool"},
},
want=fnv1.Resource(
resource=resource.dict_to_struct(
{
"apiVersion": "example.org",
"kind": "Resource",
"metadata": {"name": "cool"},
}
),
),
),
TestCase(
reason="Updating from a struct should work.",
r=fnv1.Resource(),
source=resource.dict_to_struct(
{"apiVersion": "example.org", "kind": "Resource"}
),
want=fnv1.Resource(
resource=resource.dict_to_struct(
{"apiVersion": "example.org", "kind": "Resource"}
),
),
),
TestCase(
reason="Updating from a Pydantic model should work.",
r=fnv1.Resource(),
source=v1beta2.Bucket(
spec=v1beta2.Spec(
forProvider=v1beta2.ForProvider(region="us-west-2"),
),
),
want=fnv1.Resource(
resource=resource.dict_to_struct(
{"spec": {"forProvider": {"region": "us-west-2"}}}
),
),
),
]

for case in cases:
resource.update(case.r, case.source)
self.assertEqual(
json_format.MessageToDict(case.want),
json_format.MessageToDict(case.r),
"-want, +got",
)

def test_get_condition(self) -> None:
@dataclasses.dataclass
class TestCase:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: <stdin>
# timestamp: 2024-10-04T21:01:52+00:00
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: <stdin>
# timestamp: 2024-10-04T21:01:52+00:00
Loading

0 comments on commit 33ee9e1

Please sign in to comment.