Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multiple client generation #874

Merged
merged 13 commits into from
Sep 10, 2024
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""Utilizes command line args to create a Measurement Plug-In Client using template files."""

import pathlib
import re
import sys
from typing import Any, List, Optional

import black
Expand All @@ -21,6 +19,7 @@
get_measurement_service_stub,
get_output_metadata_by_index,
get_output_parameters_with_type,
get_available_measurements_service_class,
is_python_identifier,
remove_suffix,
to_ordered_set,
Expand Down Expand Up @@ -49,7 +48,7 @@ def _create_file(


@click.command()
@click.argument("measurement_service_class")
@click.argument("measurement_service_class", required=False, nargs=-1)
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
@click.option(
"-m",
"--module-name",
Expand All @@ -60,89 +59,112 @@ def _create_file(
"--class-name",
help="Name for the Python Measurement Plug-In Client Class in the generated module.",
)
@click.option(
"-a",
"--all",
is_flag=True,
help="Creates Python Measurement Plug-In Client for all the active measurement services.",
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
)
@click.option(
"-o",
"--directory-out",
help="Output directory for Measurement Plug-In Client files. Default: '<current_directory>/<module_name>'",
)
def create_client(
measurement_service_class: str,
measurement_service_class: List[str],
module_name: Optional[str],
class_name: Optional[str],
all: Optional[bool],
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
directory_out: Optional[str],
) -> None:
"""Generates a Python Measurement Plug-In Client module for the measurement service.

You can use the generated module to interact with the corresponding measurement service.

MEASUREMENT_SERVICE_CLASS: The service class of the measurement.
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
Provide space separated service class to generate multiple clients.
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
"""
channel_pool = GrpcChannelPool()
discovery_client = DiscoveryClient(grpc_channel_pool=channel_pool)
built_in_import_modules: List[str] = []
custom_import_modules: List[str] = []

if module_name is None or class_name is None:
base_service_class = measurement_service_class.split(".")[-1]
base_service_class = remove_suffix(base_service_class)
if not base_service_class.isidentifier():
raise click.ClickException(
"Unable to create client.\nPlease provide a valid module name or update the measurement with a valid service class."
)
if all:
measurement_service_class = get_available_measurements_service_class(discovery_client)
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
if len(measurement_service_class) < 1:
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
raise click.ClickException("No active measurements are available.")
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
else:
if not measurement_service_class:
raise click.ClickException("Measurement service class cannot be empty.")
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved

if module_name is None:
module_name = camel_to_snake_case(base_service_class) + "_client"
if class_name is None:
class_name = base_service_class.replace("_", "") + "Client"
if not any(ch.isupper() for ch in base_service_class):
print(
f"Warning: The service class '{measurement_service_class}' does not follow the recommended format."
if directory_out is None:
directory_out_path = pathlib.Path.cwd()
else:
directory_out_path = pathlib.Path(directory_out)

if not directory_out_path.exists():
raise click.ClickException(f"No such directory '{directory_out}' found.")
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved

is_multiple_client_generation = len(measurement_service_class) > 1
for service_class in measurement_service_class:
if is_multiple_client_generation or module_name is None or class_name is None:
base_service_class = service_class.split(".")[-1]
base_service_class = remove_suffix(base_service_class)
if not base_service_class.isidentifier():
raise click.ClickException(
"Unable to create client.\nPlease provide a valid module name or update the measurement with a valid service class."
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
)

if not module_name.isidentifier():
raise click.ClickException(
f"The module name '{module_name}' is not a valid Python identifier."
if is_multiple_client_generation or module_name is None:
module_name = camel_to_snake_case(base_service_class) + "_client"
if is_multiple_client_generation or class_name is None:
class_name = base_service_class.replace("_", "") + "Client"
if not any(ch.isupper() for ch in base_service_class):
print(
f"Warning: The service class '{service_class}' does not follow the recommended format."
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
)

if not is_python_identifier(module_name):
raise click.ClickException(
f"The module name '{module_name}' is not a valid Python identifier."
)
if not is_python_identifier(class_name):
raise click.ClickException(
f"The class name '{class_name}' is not a valid Python identifier."
)

measurement_service_stub = get_measurement_service_stub(
discovery_client, channel_pool, service_class
)
if not is_python_identifier(class_name):
raise click.ClickException(
f"The class name '{class_name}' is not a valid Python identifier."
metadata = measurement_service_stub.GetMetadata(
v2_measurement_service_pb2.GetMetadataRequest()
)
configuration_metadata = get_configuration_metadata_by_index(metadata, service_class)
output_metadata = get_output_metadata_by_index(metadata)

measurement_service_stub = get_measurement_service_stub(
discovery_client, channel_pool, measurement_service_class
)
metadata = measurement_service_stub.GetMetadata(v2_measurement_service_pb2.GetMetadataRequest())
configuration_metadata = get_configuration_metadata_by_index(
metadata, measurement_service_class
)
output_metadata = get_output_metadata_by_index(metadata)

configuration_parameters_with_type_and_default_values, measure_api_parameters = (
get_configuration_parameters_with_type_and_default_values(
configuration_metadata, built_in_import_modules
configuration_parameters_with_type_and_default_values, measure_api_parameters = (
get_configuration_parameters_with_type_and_default_values(
configuration_metadata, built_in_import_modules
)
)
output_parameters_with_type = get_output_parameters_with_type(
output_metadata, built_in_import_modules, custom_import_modules
)
)
output_parameters_with_type = get_output_parameters_with_type(
output_metadata, built_in_import_modules, custom_import_modules
)

if directory_out is None:
directory_out_path = pathlib.Path.cwd()
else:
directory_out_path = pathlib.Path(directory_out)
_create_file(
template_name="measurement_plugin_client.py.mako",
file_name=f"{module_name}.py",
directory_out=directory_out_path,
class_name=class_name,
display_name=metadata.measurement_details.display_name,
configuration_metadata=configuration_metadata,
output_metadata=output_metadata,
service_class=service_class,
configuration_parameters_with_type_and_default_values=configuration_parameters_with_type_and_default_values,
measure_api_parameters=measure_api_parameters,
output_parameters_with_type=output_parameters_with_type,
built_in_import_modules=to_ordered_set(built_in_import_modules),
custom_import_modules=to_ordered_set(custom_import_modules),
)

_create_file(
template_name="measurement_plugin_client.py.mako",
file_name=f"{module_name}.py",
directory_out=directory_out_path,
class_name=class_name,
display_name=metadata.measurement_details.display_name,
configuration_metadata=configuration_metadata,
output_metadata=output_metadata,
service_class=measurement_service_class,
configuration_parameters_with_type_and_default_values=configuration_parameters_with_type_and_default_values,
measure_api_parameters=measure_api_parameters,
output_parameters_with_type=output_parameters_with_type,
built_in_import_modules=to_ordered_set(built_in_import_modules),
custom_import_modules=to_ordered_set(custom_import_modules),
)
print(f"Client has been created successfully for '{service_class}'.")
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import re
import sys
from typing import AbstractSet, Dict, Iterable, List, Tuple, TypeVar
from typing import AbstractSet, Dict, Iterable, List, Optional, Tuple, TypeVar

import click
import grpc
Expand Down Expand Up @@ -82,6 +82,17 @@ def get_measurement_service_stub(
return v2_measurement_service_pb2_grpc.MeasurementServiceStub(channel)


def get_available_measurements_service_class(discovery_client: DiscoveryClient) -> List[str]:
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
"""Returns the service classes of all the available measurement services."""
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
available_measurement_services = discovery_client.enumerate_services(
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
_V2_MEASUREMENT_SERVICE_INTERFACE
)
service_class_list = [
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
measurement_service.service_class for measurement_service in available_measurement_services
]
return service_class_list


def get_configuration_metadata_by_index(
metadata: v2_measurement_service_pb2.GetMetadataResponse, service_class: str
) -> Dict[int, ParameterMetadata]:
Expand Down Expand Up @@ -245,8 +256,10 @@ def remove_suffix(string: str) -> str:
return string


def is_python_identifier(input_string: str) -> bool:
def is_python_identifier(input_string: Optional[str]) -> bool:
mshafer-NI marked this conversation as resolved.
Show resolved Hide resolved
"""Validates whether the given string is a valid Python identifier."""
if input_string is None:
return False
pattern = r"^[a-zA-Z_][a-zA-Z0-9_]*$"
return re.fullmatch(pattern, input_string) is not None

Expand Down
33 changes: 29 additions & 4 deletions packages/generator/tests/acceptance/test_client_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test___command_line_args___create_client___render_without_error(
measurement_service: MeasurementService,
) -> None:
temp_directory = tmp_path_factory.mktemp("measurement_plugin_client_files")
module_name = "test_measurement_client"
module_name = "non_streaming_data_measurement_client"
golden_path = test_assets_directory / "example_renders" / "measurement_plugin_client"
filename = f"{module_name}.py"

Expand All @@ -28,7 +28,32 @@ def test___command_line_args___create_client___render_without_error(
"--module-name",
module_name,
"--class-name",
"TestMeasurement",
"NonStreamingDataMeasurementClient",
"--directory-out",
temp_directory,
]
)

assert not exc_info.value.code
_assert_equal(
golden_path / filename,
temp_directory / filename,
)


def test___command_line_args___create_client_for_all_active_measurement___render_without_error(
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved
test_assets_directory: pathlib.Path,
tmp_path_factory: pytest.TempPathFactory,
measurement_service: MeasurementService,
) -> None:
temp_directory = tmp_path_factory.mktemp("measurement_plugin_client_files")
golden_path = test_assets_directory / "example_renders" / "measurement_plugin_client"
filename = "non_streaming_data_measurement_client.py"
Jotheeswaran-Nandagopal marked this conversation as resolved.
Show resolved Hide resolved

with pytest.raises(SystemExit) as exc_info:
create_client(
[
"--all",
"--directory-out",
temp_directory,
]
Expand All @@ -46,7 +71,7 @@ def test___command_line_args___create_client___render_with_proper_line_ending(
measurement_service: MeasurementService,
) -> None:
temp_directory = tmp_path_factory.mktemp("measurement_plugin_client_files")
module_name = "test_measurement_client"
module_name = "non_streaming_data_measurement_client"
filename = f"{module_name}.py"

with pytest.raises(SystemExit) as exc_info:
Expand All @@ -56,7 +81,7 @@ def test___command_line_args___create_client___render_with_proper_line_ending(
"--module-name",
module_name,
"--class-name",
"TestMeasurement",
"NonStreamingDataMeasurementClient",
"--directory-out",
temp_directory,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Output(NamedTuple):
xy_data_out: DoubleXYData


class TestMeasurement:
class NonStreamingDataMeasurementClient:
"""Client to interact with the measurement plug-in."""

def __init__(
Expand Down