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"
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4
uses: pypa/gh-action-pypi-publish@v1.12.3
with:
# Note that this is currently being pushed to the 'crossplane' PyPI
# 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!
# NO CHECKED-IN PROTOBUF GENCODE
# source: crossplane/function/proto/v1/run_function.proto
# Protobuf Python Version: 5.29.0
# Protobuf Python Version: 5.27.2
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
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.Domain.PUBLIC,
5,
29,
0,
27,
2,
'',
'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
GRPC_GENERATED_VERSION = '1.71.0'
GRPC_GENERATED_VERSION = '1.66.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False

View File

@ -2,7 +2,7 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: crossplane/function/proto/v1beta1/run_function.proto
# Protobuf Python Version: 5.29.0
# Protobuf Python Version: 5.27.2
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
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.Domain.PUBLIC,
5,
29,
0,
27,
2,
'',
'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
GRPC_GENERATED_VERSION = '1.71.0'
GRPC_GENERATED_VERSION = '1.66.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False

View File

@ -18,7 +18,6 @@ import dataclasses
import datetime
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
@ -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'
# (and not explicitly provided during initialization), it will be
# excluded from the serialized output.
data["apiVersion"] = source.apiVersion
data["kind"] = source.kind
data['apiVersion'] = source.apiVersion
data['kind'] = source.kind
r.resource.update(data)
case structpb.Struct():
# 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
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:
@ -76,7 +77,10 @@ def struct_to_dict(s: structpb.Struct) -> dict:
protobuf struct. This function makes it possible to convert resources to a
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

View File

@ -16,7 +16,6 @@
import asyncio
import os
import signal
import grpc
from grpc_reflection.v1alpha import reflection
@ -32,8 +31,6 @@ SERVICE_NAMES = (
fnv1beta1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name,
)
SHUTDOWN_GRACE_PERIOD_SECONDS = 5
def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials:
"""Load TLS credentials for a composition function gRPC server.
@ -93,11 +90,6 @@ def serve(
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)
grpcv1beta1.add_FunctionRunnerServiceServicer_to_server(
BetaFunctionRunner(wrapped=function), server
@ -124,7 +116,7 @@ def serve(
try:
loop.run_until_complete(start())
finally:
loop.run_until_complete(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS))
loop.run_until_complete(server.stop(grace=5))
loop.close()

View File

@ -14,15 +14,14 @@ classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.11",
"Typing :: Typed",
]
dependencies = [
"grpcio==1.73.1",
"grpcio==1.*",
"grpcio-reflection==1.*",
"protobuf==6.31.1", # Must be compatible with grpcio-tools.
"protobuf==5.29.3",
"pydantic==2.*",
"structlog==25.*",
"structlog==24.*",
]
dynamic = ["version"]
@ -39,16 +38,13 @@ validate-bump = false # Allow going from 0.0.0.dev0+x to 0
[tool.hatch.envs.default]
type = "virtual"
path = ".venv-default"
dependencies = ["ipython==9.4.0"]
dependencies = ["ipython==8.31.0"]
[tool.hatch.envs.generate]
type = "virtual"
detached = true
path = ".venv-generate"
dependencies = [
"grpcio-tools==1.73.1",
"protobuf==6.31.1",
]
dependencies = ["grpcio-tools==1.69.0"]
[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"
@ -66,8 +62,8 @@ packages = ["crossplane"]
# This special environment is used by hatch fmt.
[tool.hatch.envs.hatch-static-analysis]
dependencies = ["ruff==0.12.5"]
config-path = "none" # Disable Hatch's default Ruff config.
dependencies = ["ruff==0.9.0"]
config-path = "none" # Disable Hatch's default Ruff config.
[tool.ruff]
target-version = "py311"

View File

@ -248,66 +248,6 @@ class TestResource(unittest.TestCase):
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:
@dataclasses.dataclass
class TestCase:
@ -339,28 +279,6 @@ class TestResource(unittest.TestCase):
),
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: