Compare commits

..

No commits in common. "main" and "v0.6.0" have entirely different histories.
main ... v0.6.0

12 changed files with 26 additions and 156 deletions

View File

@ -120,7 +120,7 @@ jobs:
path: "dist" path: "dist"
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4 uses: pypa/gh-action-pypi-publish@v1.12.3
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

View File

@ -1,22 +0,0 @@
# 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

View File

@ -1,18 +0,0 @@
<!--
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.

View File

@ -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.29.0 # Protobuf Python Version: 5.27.2
"""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,
29, 27,
0, 2,
'', '',
'crossplane/function/proto/v1/run_function.proto' 'crossplane/function/proto/v1/run_function.proto'
) )

View File

@ -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.71.0' GRPC_GENERATED_VERSION = '1.66.0'
GRPC_VERSION = grpc.__version__ GRPC_VERSION = grpc.__version__
_version_not_supported = False _version_not_supported = False

View File

@ -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.29.0 # Protobuf Python Version: 5.27.2
"""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,
29, 27,
0, 2,
'', '',
'crossplane/function/proto/v1beta1/run_function.proto' 'crossplane/function/proto/v1beta1/run_function.proto'
) )

View File

@ -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.71.0' GRPC_GENERATED_VERSION = '1.66.0'
GRPC_VERSION = grpc.__version__ GRPC_VERSION = grpc.__version__
_version_not_supported = False _version_not_supported = False

View File

@ -18,7 +18,6 @@ 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
@ -45,8 +44,8 @@ def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel
# apiVersion is set to its default value 's3.aws.upbound.io/v1beta2' # apiVersion is set to its default value 's3.aws.upbound.io/v1beta2'
# (and not explicitly provided during initialization), it will be # (and not explicitly provided during initialization), it will be
# excluded from the serialized output. # excluded from the serialized output.
data["apiVersion"] = source.apiVersion data['apiVersion'] = source.apiVersion
data["kind"] = source.kind data['kind'] = source.kind
r.resource.update(data) 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?
@ -66,7 +65,9 @@ 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.
""" """
return json_format.ParseDict(d, structpb.Struct()) s = structpb.Struct()
s.update(d)
return s
def struct_to_dict(s: structpb.Struct) -> dict: def struct_to_dict(s: structpb.Struct) -> dict:
@ -76,7 +77,10 @@ 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 json_format.MessageToDict(s, preserving_proto_field_name=True) return {
k: (struct_to_dict(v) if isinstance(v, structpb.Struct) else v)
for k, v in s.items()
}
@dataclasses.dataclass @dataclasses.dataclass

View File

@ -16,7 +16,6 @@
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
@ -32,8 +31,6 @@ 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.
@ -93,11 +90,6 @@ 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
@ -124,7 +116,7 @@ def serve(
try: try:
loop.run_until_complete(start()) loop.run_until_complete(start())
finally: finally:
loop.run_until_complete(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS)) loop.run_until_complete(server.stop(grace=5))
loop.close() loop.close()

View File

@ -14,15 +14,14 @@ 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.73.1", "grpcio==1.*",
"grpcio-reflection==1.*", "grpcio-reflection==1.*",
"protobuf==6.31.1", # Must be compatible with grpcio-tools. "protobuf==5.29.3",
"pydantic==2.*", "pydantic==2.*",
"structlog==25.*", "structlog==24.*",
] ]
dynamic = ["version"] dynamic = ["version"]
@ -39,16 +38,13 @@ 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==9.4.0"] dependencies = ["ipython==8.31.0"]
[tool.hatch.envs.generate] [tool.hatch.envs.generate]
type = "virtual" type = "virtual"
detached = true detached = true
path = ".venv-generate" path = ".venv-generate"
dependencies = [ dependencies = ["grpcio-tools==1.69.0"]
"grpcio-tools==1.73.1",
"protobuf==6.31.1",
]
[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"
@ -66,8 +62,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.12.5"] dependencies = ["ruff==0.9.0"]
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"

View File

@ -248,66 +248,6 @@ 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:
@ -339,28 +279,6 @@ 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: