Skip to content

Commit

Permalink
Add interactive mode for client generation (#883)
Browse files Browse the repository at this point in the history
* Feat: Add interactive mode for client generation

* Fix: Lint errors

* Fix: Minor .message improvements

* Refractor: craeting class and module names methods

* Client: Use click group as script command

* Fix: Add interactive mode in existing command

* Fix: Remove command name

* Client: List display name instead of service class

* Client: Generate default class and module name for interactive mode

* Client: Refractor display messages

* Client: Capiltialize the noun python

* Client: Spacing in options help context

* Client: Fix method signatures

* Client; Fix lint errors

* Client: Update directory_out default value to None

* Client: Update exit message

* Client: Update parameter names

* Client: Use optgroup to group different modes

* Client: Refractor create client method

* Client: Update directory out creation

* Client: Update directory out in all mode

* Client: Update interactive exit message

* Client: Create class & module name for multiple clients

* Client: Remove exception for no service class

* Packages: Add click-option-group

* Client: Update context help

* Client: extract create_client into helper functions

* Client: Fix myPy errors

* Packages: Update click-option-group version

* Client: Update exit prompt message

* Packages: Update lock file

* Client: Update -s option case to kebab case

* Client: Rearrange command signature

* Client: Update create client command docstring

* Client: Update prompt messages

* Refractor; precise variable names

* Client: Update plural varaible names

* Client: Add sub helper methods

* Client: Moved helper methods to support.py

* Tests: update create client command in tests

* Test: Update tests

* Client: Add missing required argument
  • Loading branch information
MounikaBattu17 authored Sep 17, 2024
1 parent 09b40b0 commit e3ca88b
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 138 deletions.
318 changes: 207 additions & 111 deletions packages/generator/ni_measurement_plugin_sdk_generator/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import black
import click
from click_option_group import optgroup, RequiredMutuallyExclusiveOptionGroup
from mako.template import Template
from ni_measurement_plugin_sdk_service._internal.stubs.ni.measurementlink.measurement.v2 import (
measurement_service_pb2 as v2_measurement_service_pb2,
Expand All @@ -15,16 +16,20 @@
from ni_measurement_plugin_sdk_service.grpc.channelpool import GrpcChannelPool

from ni_measurement_plugin_sdk_generator.client._support import (
camel_to_snake_case,
create_class_name,
create_module_name,
extract_base_service_class,
get_configuration_metadata_by_index,
get_configuration_parameters_with_type_and_default_values,
get_measurement_service_stub,
get_output_metadata_by_index,
get_output_parameters_with_type,
get_all_registered_measurement_service_classes,
is_python_identifier,
remove_suffix,
get_all_registered_measurement_info,
get_selected_measurement_service_class,
to_ordered_set,
resolve_output_directory,
validate_identifier,
validate_measurement_service_classes,
)


Expand Down Expand Up @@ -55,32 +60,212 @@ def _create_file(
file.write(formatted_output)


def _create_client(
discovery_client: DiscoveryClient,
channel_pool: GrpcChannelPool,
measurement_service_class: str,
module_name: str,
class_name: str,
directory_out: pathlib.Path,
) -> None:
built_in_import_modules: List[str] = []
custom_import_modules: List[str] = []
enum_values_by_type: Dict[Type[Enum], Dict[str, int]] = {}

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, enum_values_by_type
)
output_metadata = get_output_metadata_by_index(metadata, enum_values_by_type)

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

_create_file(
template_name="measurement_plugin_client.py.mako",
file_name=f"{module_name}.py",
directory_out=directory_out,
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),
enum_by_class_name=enum_values_by_type,
)

print(
f"The measurement plug-in client for the service class '{measurement_service_class}' is created successfully."
)


def _create_all_clients(directory_out: Optional[str]) -> None:
channel_pool = GrpcChannelPool()
discovery_client = DiscoveryClient(grpc_channel_pool=channel_pool)

directory_out_path = resolve_output_directory(directory_out)
measurement_service_classes, _ = get_all_registered_measurement_info(discovery_client)
validate_measurement_service_classes(measurement_service_classes)

for service_class in measurement_service_classes:
base_service_class = extract_base_service_class(service_class)
module_name = create_module_name(base_service_class)
class_name = create_class_name(base_service_class)
validate_identifier(module_name, "module")
validate_identifier(class_name, "class")

_create_client(
channel_pool=channel_pool,
discovery_client=discovery_client,
measurement_service_class=service_class,
module_name=module_name,
class_name=class_name,
directory_out=directory_out_path,
)


def _create_clients_interactively() -> None:
print("Creating the Python Measurement Plug-In Client in interactive mode...")
channel_pool = GrpcChannelPool()
discovery_client = DiscoveryClient(grpc_channel_pool=channel_pool)
directory_out_path = resolve_output_directory()

while True:
measurement_service_classes, measurement_display_names = (
get_all_registered_measurement_info(discovery_client)
)
validate_measurement_service_classes(measurement_service_classes)

print("\nList of registered measurements:")
for index, display_name in enumerate(measurement_display_names, start=1):
print(f"{index}. {display_name}")

selection = click.prompt(
"\nSelect a measurement to generate a client (x to exit)",
type=str,
)
if selection.lower() == "x":
break
service_class = get_selected_measurement_service_class(
int(selection), measurement_service_classes
)

base_service_class = extract_base_service_class(service_class)
default_module_name = create_module_name(base_service_class)
module_name = click.prompt(
"Enter a name for the Python client module, or press Enter to use the default name.",
type=str,
default=default_module_name,
)
validate_identifier(module_name, "module")
default_class_name = create_class_name(base_service_class)
class_name = click.prompt(
"Enter a name for the Python client class, or press Enter to use the default name.",
type=str,
default=default_class_name,
)
validate_identifier(class_name, "class")

_create_client(
channel_pool=channel_pool,
discovery_client=discovery_client,
measurement_service_class=service_class,
module_name=module_name,
class_name=class_name,
directory_out=directory_out_path,
)


def _create_clients(
measurement_service_classes: List[str],
module_name: Optional[str],
class_name: Optional[str],
directory_out: Optional[str],
) -> None:
channel_pool = GrpcChannelPool()
discovery_client = DiscoveryClient(grpc_channel_pool=channel_pool)
directory_out_path = resolve_output_directory(directory_out)

has_multiple_service_classes = len(measurement_service_classes) > 1
for service_class in measurement_service_classes:
base_service_class = extract_base_service_class(service_class)
if has_multiple_service_classes or module_name is None:
module_name = create_module_name(base_service_class)
if has_multiple_service_classes or class_name is None:
class_name = create_class_name(base_service_class)
validate_identifier(module_name, "module")
validate_identifier(class_name, "class")

_create_client(
channel_pool=channel_pool,
discovery_client=discovery_client,
measurement_service_class=service_class,
module_name=module_name,
class_name=class_name,
directory_out=directory_out_path,
)


@click.command()
@click.argument("measurement_service_class", nargs=-1)
@click.option(
@optgroup.group(
"all-modes",
cls=RequiredMutuallyExclusiveOptionGroup,
help="The different modes to create Python measurement client.",
)
@optgroup.option(
"-s",
"--measurement-service-class",
help="Creates Python Measurement Plug-In Client for the given measurement services.",
multiple=True,
)
@optgroup.option(
"-a",
"--all",
is_flag=True,
help="Creates Python Measurement Plug-In Clients for all registered measurement services.",
)
@optgroup.option(
"-i",
"--interactive",
is_flag=True,
help="Creates Python Measurement Plug-In Clients interactively.",
)
@optgroup.group(
"optional parameters",
help="Recommended parameters when using measurement service class mode.",
)
@optgroup.option(
"-m",
"--module-name",
help="Name for the Python Measurement Plug-In Client module.",
)
@click.option(
@optgroup.option(
"-c",
"--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 registered measurement services.",
)
@click.option(
@optgroup.option(
"-o",
"--directory-out",
help="Output directory for Measurement Plug-In Client files. Default: '<current_directory>/<module_name>'",
)
def create_client(
measurement_service_class: List[str],
all: bool,
interactive: bool,
module_name: Optional[str],
class_name: Optional[str],
directory_out: Optional[str],
Expand All @@ -90,105 +275,16 @@ def create_client(
You can use the generated module to interact with the corresponding measurement service.
MEASUREMENT_SERVICE_CLASS: Accepts one or more measurement service classes.
Separate each service class with a space.
Provide each service class separately.
"""
channel_pool = GrpcChannelPool()
discovery_client = DiscoveryClient(grpc_channel_pool=channel_pool)
built_in_import_modules: List[str] = []
custom_import_modules: List[str] = []

if all:
measurement_service_class = get_all_registered_measurement_service_classes(discovery_client)
if len(measurement_service_class) == 0:
raise click.ClickException("No registered measurements.")
else:
if not measurement_service_class:
raise click.ClickException(
"The measurement service class cannot be empty. Either provide a measurement service class or use the 'all' flag to generate clients for all registered measurements."
)

if directory_out is None:
directory_out_path = pathlib.Path.cwd()
_create_all_clients(directory_out)
elif interactive:
_create_clients_interactively()
else:
directory_out_path = pathlib.Path(directory_out)

if not directory_out_path.exists():
raise click.ClickException(f"The specified directory '{directory_out}' was not found.")

is_multiple_client_generation = len(measurement_service_class) > 1
for service_class in measurement_service_class:
enum_values_by_type: Dict[Type[Enum], Dict[str, int]] = {}
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(
"Client creation failed.\nEither provide a module name or update the measurement with a valid service class."
)
if not any(ch.isupper() for ch in base_service_class):
print(
f"Warning: The service class '{service_class}' does not adhere to the recommended format."
)

if is_multiple_client_generation:
module_name = camel_to_snake_case(base_service_class) + "_client"
class_name = base_service_class.replace("_", "") + "Client"
else:
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 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
)
metadata = measurement_service_stub.GetMetadata(
v2_measurement_service_pb2.GetMetadataRequest()
)
configuration_metadata = get_configuration_metadata_by_index(
metadata, service_class, enum_values_by_type
)
output_metadata = get_output_metadata_by_index(metadata, enum_values_by_type)

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

_create_file(
template_name="measurement_plugin_client.py.mako",
file_name=f"{module_name}.py",
directory_out=directory_out_path,
_create_clients(
measurement_service_classes=measurement_service_class,
module_name=module_name,
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),
enum_by_class_name=enum_values_by_type,
)

print(
f"The measurement plug-in client for the service class '{service_class}' has been created successfully."
directory_out=directory_out,
)
Loading

0 comments on commit e3ca88b

Please sign in to comment.