Compare commits
89 Commits
Author | SHA1 | Date |
---|---|---|
|
6e65599e58 | |
|
a5cde9d9ee | |
|
f99b12c0de | |
|
27e990f6aa | |
|
cf67e6d0fe | |
|
14b6502f33 | |
|
7a915c2df7 | |
|
3931d503fc | |
|
82e60b0596 | |
|
57df02ae00 | |
|
b1e9695250 | |
|
1f3d442276 | |
|
f0bb336ba5 | |
|
47180d9798 | |
|
869fbfeb3d | |
|
9f55079121 | |
|
ed95373a01 | |
|
0262a24330 | |
|
8895e9d9af | |
|
1e0c20eb61 | |
|
cdaf6697c5 | |
|
fcec201f01 | |
|
88d97d3cab | |
|
e2606cff9f | |
|
f295c0144b | |
|
325c37d344 | |
|
e6b771c3a6 | |
|
68cbf5f53e | |
|
c8bcded971 | |
|
e0b964055b | |
|
5c1e33baf4 | |
|
466945254d | |
|
4fb63f221e | |
|
26bdc926e8 | |
|
1e0f18263e | |
|
4ad65f2934 | |
|
aa09bd0632 | |
|
0f188bcbde | |
|
de5a11c59f | |
|
afa27a3121 | |
|
aaf3d08879 | |
|
aff844707c | |
|
fb5d6fa5ab | |
|
8525cebb57 | |
|
597f5ee437 | |
|
ce63739286 | |
|
dbc0d0176b | |
|
d3bad9135d | |
|
b6d553bdaf | |
|
cd38d99a5c | |
|
0932de3e5d | |
|
47d0d54b26 | |
|
54d6e39371 | |
|
8551576b1f | |
|
f1ae43e725 | |
|
192b1d0b7a | |
|
a82962a952 | |
|
0651813b59 | |
|
8f57f81ea1 | |
|
2c8b642afd | |
|
479335d1e3 | |
|
84cf34e482 | |
|
81df679319 | |
|
773346f3f8 | |
|
77f1f6e8dd | |
|
b8dd9ec256 | |
|
6b3e0cbcc4 | |
|
d9e26f5b03 | |
|
5139481ee5 | |
|
a8999723f4 | |
|
9ef55d251c | |
|
064ffe7458 | |
|
d4b3506ec7 | |
|
70e86c0d30 | |
|
685918f02d | |
|
9c44291da2 | |
|
36e5600356 | |
|
5cf7fffbf1 | |
|
c632b0c1ad | |
|
25e49cf3bd | |
|
7452119ba8 | |
|
870edfcd76 | |
|
fc8796d42b | |
|
569af1d6d3 | |
|
41eca588a1 | |
|
50086a69b2 | |
|
1bf5f444e4 | |
|
5a3e1536ee | |
|
6a256486e2 |
|
@ -120,7 +120,7 @@ jobs:
|
||||||
path: "dist"
|
path: "dist"
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.10.3
|
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||||
with:
|
with:
|
||||||
# Note that this is currently being pushed to the 'crossplane' PyPI
|
# Note that this is currently being pushed to the 'crossplane' PyPI
|
||||||
# user (not org). See @negz if you need access - PyPI requires 2FA to
|
# user (not org). See @negz if you need access - PyPI requires 2FA to
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
# SPDX-FileCopyrightText: 2025 The Crossplane Authors <https://crossplane.io>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
# This file controls automatic PR reviewer assignment. See the following docs:
|
||||||
|
#
|
||||||
|
# * https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
# * https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team
|
||||||
|
#
|
||||||
|
# The goal of this file is for most PRs to automatically and fairly have one
|
||||||
|
# maintainer set as PR reviewers. All maintainers have permission to approve
|
||||||
|
# and merge PRs. All PRs must be approved by at least one maintainer before being merged.
|
||||||
|
#
|
||||||
|
# Where possible, prefer explicitly specifying a maintainer who is a subject
|
||||||
|
# matter expert for a particular part of the codebase rather than using fallback
|
||||||
|
# owners. Fallback owners are listed at the bottom of this file.
|
||||||
|
#
|
||||||
|
# See also OWNERS.md for governance details
|
||||||
|
|
||||||
|
# Fallback owners
|
||||||
|
* @negz @bobh66
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2025 The Crossplane Authors <https://crossplane.io>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC-BY-4.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# OWNERS
|
||||||
|
|
||||||
|
This page lists all maintainers for **this** repository. Each repository in the
|
||||||
|
[Crossplane Contrib organization](https://github.com/crossplane-contrib/) will list their
|
||||||
|
repository maintainers in their own `OWNERS.md` file.
|
||||||
|
|
||||||
|
## Maintainers
|
||||||
|
* Nic Cope <negz@upbound.com> ([negz](https://github.com/negz))
|
||||||
|
* Bob Haddleton <bob.haddleton@nokia.com> ([bobh66](https://github.com/bobh66))
|
||||||
|
|
||||||
|
|
||||||
|
See [CODEOWNERS](./CODEOWNERS) for automatic PR assignment.
|
|
@ -2,7 +2,7 @@
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
# NO CHECKED-IN PROTOBUF GENCODE
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
# source: crossplane/function/proto/v1/run_function.proto
|
# source: crossplane/function/proto/v1/run_function.proto
|
||||||
# Protobuf Python Version: 5.27.2
|
# Protobuf Python Version: 5.29.0
|
||||||
"""Generated protocol buffer code."""
|
"""Generated protocol buffer code."""
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
||||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||||
_runtime_version.Domain.PUBLIC,
|
_runtime_version.Domain.PUBLIC,
|
||||||
5,
|
5,
|
||||||
27,
|
29,
|
||||||
2,
|
0,
|
||||||
'',
|
'',
|
||||||
'crossplane/function/proto/v1/run_function.proto'
|
'crossplane/function/proto/v1/run_function.proto'
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import warnings
|
||||||
|
|
||||||
from crossplane.function.proto.v1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2
|
from crossplane.function.proto.v1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2
|
||||||
|
|
||||||
GRPC_GENERATED_VERSION = '1.66.0'
|
GRPC_GENERATED_VERSION = '1.71.0'
|
||||||
GRPC_VERSION = grpc.__version__
|
GRPC_VERSION = grpc.__version__
|
||||||
_version_not_supported = False
|
_version_not_supported = False
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
# NO CHECKED-IN PROTOBUF GENCODE
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
# source: crossplane/function/proto/v1beta1/run_function.proto
|
# source: crossplane/function/proto/v1beta1/run_function.proto
|
||||||
# Protobuf Python Version: 5.27.2
|
# Protobuf Python Version: 5.29.0
|
||||||
"""Generated protocol buffer code."""
|
"""Generated protocol buffer code."""
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
||||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||||
_runtime_version.Domain.PUBLIC,
|
_runtime_version.Domain.PUBLIC,
|
||||||
5,
|
5,
|
||||||
27,
|
29,
|
||||||
2,
|
0,
|
||||||
'',
|
'',
|
||||||
'crossplane/function/proto/v1beta1/run_function.proto'
|
'crossplane/function/proto/v1beta1/run_function.proto'
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import warnings
|
||||||
|
|
||||||
from crossplane.function.proto.v1beta1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2
|
from crossplane.function.proto.v1beta1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2
|
||||||
|
|
||||||
GRPC_GENERATED_VERSION = '1.66.0'
|
GRPC_GENERATED_VERSION = '1.71.0'
|
||||||
GRPC_VERSION = grpc.__version__
|
GRPC_VERSION = grpc.__version__
|
||||||
_version_not_supported = False
|
_version_not_supported = False
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
|
from google.protobuf import json_format
|
||||||
from google.protobuf import struct_pb2 as structpb
|
from google.protobuf import struct_pb2 as structpb
|
||||||
|
|
||||||
import crossplane.function.proto.v1.run_function_pb2 as fnv1
|
import crossplane.function.proto.v1.run_function_pb2 as fnv1
|
||||||
|
@ -38,7 +39,15 @@ def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel
|
||||||
"""
|
"""
|
||||||
match source:
|
match source:
|
||||||
case pydantic.BaseModel():
|
case pydantic.BaseModel():
|
||||||
r.resource.update(source.model_dump(exclude_defaults=True, warnings=False))
|
data = source.model_dump(exclude_defaults=True, warnings=False)
|
||||||
|
# In Pydantic, exclude_defaults=True in model_dump excludes fields
|
||||||
|
# that have their value equal to the default. If a field like
|
||||||
|
# apiVersion is set to its default value 's3.aws.upbound.io/v1beta2'
|
||||||
|
# (and not explicitly provided during initialization), it will be
|
||||||
|
# excluded from the serialized output.
|
||||||
|
data["apiVersion"] = source.apiVersion
|
||||||
|
data["kind"] = source.kind
|
||||||
|
r.resource.update(data)
|
||||||
case structpb.Struct():
|
case structpb.Struct():
|
||||||
# TODO(negz): Use struct_to_dict and update to match other semantics?
|
# TODO(negz): Use struct_to_dict and update to match other semantics?
|
||||||
r.resource.MergeFrom(source)
|
r.resource.MergeFrom(source)
|
||||||
|
@ -57,9 +66,7 @@ def dict_to_struct(d: dict) -> structpb.Struct:
|
||||||
function makes it possible to work with a Python dict, then convert it to a
|
function makes it possible to work with a Python dict, then convert it to a
|
||||||
struct in a RunFunctionResponse.
|
struct in a RunFunctionResponse.
|
||||||
"""
|
"""
|
||||||
s = structpb.Struct()
|
return json_format.ParseDict(d, structpb.Struct())
|
||||||
s.update(d)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def struct_to_dict(s: structpb.Struct) -> dict:
|
def struct_to_dict(s: structpb.Struct) -> dict:
|
||||||
|
@ -69,10 +76,7 @@ def struct_to_dict(s: structpb.Struct) -> dict:
|
||||||
protobuf struct. This function makes it possible to convert resources to a
|
protobuf struct. This function makes it possible to convert resources to a
|
||||||
dictionary.
|
dictionary.
|
||||||
"""
|
"""
|
||||||
return {
|
return json_format.MessageToDict(s, preserving_proto_field_name=True)
|
||||||
k: (struct_to_dict(v) if isinstance(v, structpb.Struct) else v)
|
|
||||||
for k, v in s.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
|
@ -110,10 +114,10 @@ def get_condition(resource: structpb.Struct, typ: str) -> Condition:
|
||||||
"""
|
"""
|
||||||
unknown = Condition(typ=typ, status="Unknown")
|
unknown = Condition(typ=typ, status="Unknown")
|
||||||
|
|
||||||
if "status" not in resource:
|
if not resource or "status" not in resource:
|
||||||
return unknown
|
return unknown
|
||||||
|
|
||||||
if "conditions" not in resource["status"]:
|
if not resource["status"] or "conditions" not in resource["status"]:
|
||||||
return unknown
|
return unknown
|
||||||
|
|
||||||
for c in resource["status"]["conditions"]:
|
for c in resource["status"]["conditions"]:
|
||||||
|
@ -149,9 +153,9 @@ class Credentials:
|
||||||
def get_credentials(req: structpb.Struct, name: str) -> Credentials:
|
def get_credentials(req: structpb.Struct, name: str) -> Credentials:
|
||||||
"""Get the supplied credentials."""
|
"""Get the supplied credentials."""
|
||||||
empty = Credentials(type="data", data={})
|
empty = Credentials(type="data", data={})
|
||||||
if "credentials" not in req:
|
if not req or "credentials" not in req:
|
||||||
return empty
|
return empty
|
||||||
if name not in req["credentials"]:
|
if not req["credentials"] or name not in req["credentials"]:
|
||||||
return empty
|
return empty
|
||||||
return Credentials(
|
return Credentials(
|
||||||
type=req["credentials"][name]["type"],
|
type=req["credentials"][name]["type"],
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
from grpc_reflection.v1alpha import reflection
|
from grpc_reflection.v1alpha import reflection
|
||||||
|
@ -31,6 +32,8 @@ SERVICE_NAMES = (
|
||||||
fnv1beta1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name,
|
fnv1beta1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SHUTDOWN_GRACE_PERIOD_SECONDS = 5
|
||||||
|
|
||||||
|
|
||||||
def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials:
|
def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials:
|
||||||
"""Load TLS credentials for a composition function gRPC server.
|
"""Load TLS credentials for a composition function gRPC server.
|
||||||
|
@ -90,6 +93,11 @@ def serve(
|
||||||
|
|
||||||
server = grpc.aio.server()
|
server = grpc.aio.server()
|
||||||
|
|
||||||
|
loop.add_signal_handler(
|
||||||
|
signal.SIGTERM,
|
||||||
|
lambda: asyncio.ensure_future(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS)),
|
||||||
|
)
|
||||||
|
|
||||||
grpcv1.add_FunctionRunnerServiceServicer_to_server(function, server)
|
grpcv1.add_FunctionRunnerServiceServicer_to_server(function, server)
|
||||||
grpcv1beta1.add_FunctionRunnerServiceServicer_to_server(
|
grpcv1beta1.add_FunctionRunnerServiceServicer_to_server(
|
||||||
BetaFunctionRunner(wrapped=function), server
|
BetaFunctionRunner(wrapped=function), server
|
||||||
|
@ -116,7 +124,7 @@ def serve(
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(start())
|
loop.run_until_complete(start())
|
||||||
finally:
|
finally:
|
||||||
loop.run_until_complete(server.stop(grace=5))
|
loop.run_until_complete(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS))
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,14 +14,15 @@ classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Typing :: Typed",
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"grpcio==1.*",
|
"grpcio==1.71.0",
|
||||||
"grpcio-reflection==1.*",
|
"grpcio-reflection==1.*",
|
||||||
"protobuf==5.27.2",
|
"protobuf==6.30.2", # Must be compatible with grpcio-tools.
|
||||||
"pydantic==2.*",
|
"pydantic==2.*",
|
||||||
"structlog==24.*",
|
"structlog==25.*",
|
||||||
]
|
]
|
||||||
|
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
@ -38,13 +39,16 @@ validate-bump = false # Allow going from 0.0.0.dev0+x to 0
|
||||||
[tool.hatch.envs.default]
|
[tool.hatch.envs.default]
|
||||||
type = "virtual"
|
type = "virtual"
|
||||||
path = ".venv-default"
|
path = ".venv-default"
|
||||||
dependencies = ["ipython==8.28.0"]
|
dependencies = ["ipython==9.2.0"]
|
||||||
|
|
||||||
[tool.hatch.envs.generate]
|
[tool.hatch.envs.generate]
|
||||||
type = "virtual"
|
type = "virtual"
|
||||||
detached = true
|
detached = true
|
||||||
path = ".venv-generate"
|
path = ".venv-generate"
|
||||||
dependencies = ["grpcio-tools==1.66.2"]
|
dependencies = [
|
||||||
|
"grpcio-tools==1.71.0",
|
||||||
|
"protobuf==6.30.2",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.hatch.envs.generate.scripts]
|
[tool.hatch.envs.generate.scripts]
|
||||||
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"
|
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"]
|
||||||
|
|
||||||
# This special environment is used by hatch fmt.
|
# This special environment is used by hatch fmt.
|
||||||
[tool.hatch.envs.hatch-static-analysis]
|
[tool.hatch.envs.hatch-static-analysis]
|
||||||
dependencies = ["ruff==0.6.9"]
|
dependencies = ["ruff==0.11.9"]
|
||||||
config-path = "none" # Disable Hatch's default Ruff config.
|
config-path = "none" # Disable Hatch's default Ruff config.
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
|
|
|
@ -90,7 +90,11 @@ class TestResource(unittest.TestCase):
|
||||||
),
|
),
|
||||||
want=fnv1.Resource(
|
want=fnv1.Resource(
|
||||||
resource=resource.dict_to_struct(
|
resource=resource.dict_to_struct(
|
||||||
{"spec": {"forProvider": {"region": "us-west-2"}}}
|
{
|
||||||
|
"apiVersion": "s3.aws.upbound.io/v1beta2",
|
||||||
|
"kind": "Bucket",
|
||||||
|
"spec": {"forProvider": {"region": "us-west-2"}},
|
||||||
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -244,6 +248,66 @@ class TestResource(unittest.TestCase):
|
||||||
dataclasses.asdict(case.want), dataclasses.asdict(got), "-want, +got"
|
dataclasses.asdict(case.want), dataclasses.asdict(got), "-want, +got"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_dict_to_struct(self) -> None:
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class TestCase:
|
||||||
|
reason: str
|
||||||
|
d: dict
|
||||||
|
want: structpb.Struct
|
||||||
|
|
||||||
|
cases = [
|
||||||
|
TestCase(
|
||||||
|
reason="Convert an empty dictionary to a struct.",
|
||||||
|
d={},
|
||||||
|
want=structpb.Struct(),
|
||||||
|
),
|
||||||
|
TestCase(
|
||||||
|
reason="Convert a dictionary with a single field to a struct.",
|
||||||
|
d={"foo": "bar"},
|
||||||
|
want=structpb.Struct(
|
||||||
|
fields={"foo": structpb.Value(string_value="bar")}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TestCase(
|
||||||
|
reason="Convert a nested dictionary to a struct.",
|
||||||
|
d={"foo": {"bar": "baz"}},
|
||||||
|
want=structpb.Struct(
|
||||||
|
fields={
|
||||||
|
"foo": structpb.Value(
|
||||||
|
struct_value=structpb.Struct(
|
||||||
|
fields={"bar": structpb.Value(string_value="baz")}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TestCase(
|
||||||
|
reason="Convert a nested dictionary containing lists to a struct.",
|
||||||
|
d={"foo": {"bar": ["baz", "qux"]}},
|
||||||
|
want=structpb.Struct(
|
||||||
|
fields={
|
||||||
|
"foo": structpb.Value(
|
||||||
|
struct_value=structpb.Struct(
|
||||||
|
fields={
|
||||||
|
"bar": structpb.Value(
|
||||||
|
list_value=structpb.ListValue(
|
||||||
|
values=[
|
||||||
|
structpb.Value(string_value="baz"),
|
||||||
|
structpb.Value(string_value="qux"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for case in cases:
|
||||||
|
got = resource.dict_to_struct(case.d)
|
||||||
|
self.assertEqual(case.want, got, "-want, +got")
|
||||||
|
|
||||||
def test_struct_to_dict(self) -> None:
|
def test_struct_to_dict(self) -> None:
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class TestCase:
|
class TestCase:
|
||||||
|
@ -275,6 +339,28 @@ class TestResource(unittest.TestCase):
|
||||||
),
|
),
|
||||||
want={"foo": {"bar": "baz"}},
|
want={"foo": {"bar": "baz"}},
|
||||||
),
|
),
|
||||||
|
TestCase(
|
||||||
|
reason="Convert a nested struct containing ListValues to a dictionary.",
|
||||||
|
s=structpb.Struct(
|
||||||
|
fields={
|
||||||
|
"foo": structpb.Value(
|
||||||
|
struct_value=structpb.Struct(
|
||||||
|
fields={
|
||||||
|
"bar": structpb.Value(
|
||||||
|
list_value=structpb.ListValue(
|
||||||
|
values=[
|
||||||
|
structpb.Value(string_value="baz"),
|
||||||
|
structpb.Value(string_value="qux"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
want={"foo": {"bar": ["baz", "qux"]}},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
for case in cases:
|
for case in cases:
|
||||||
|
|
|
@ -759,11 +759,11 @@ class Status(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Bucket(BaseModel):
|
class Bucket(BaseModel):
|
||||||
apiVersion: Optional[str] = None
|
apiVersion: Optional[str] = 's3.aws.upbound.io/v1beta2'
|
||||||
"""
|
"""
|
||||||
APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
"""
|
"""
|
||||||
kind: Optional[str] = None
|
kind: Optional[str] = 'Bucket'
|
||||||
"""
|
"""
|
||||||
Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue